{"id":18447092,"url":"https://github.com/zot/domcursor","last_synced_at":"2025-04-15T02:45:59.277Z","repository":{"id":20053123,"uuid":"23321603","full_name":"zot/DOMCursor","owner":"zot","description":"Filtered cursoring on DOM documents.  DOMCursors can move forwards or backwards, by node or by character, with settable filters that can seamlessly skip over parts of the DOM.","archived":false,"fork":false,"pushed_at":"2014-08-27T10:57:16.000Z","size":228,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-15T02:45:55.945Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"CoffeeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zot.png","metadata":{"files":{"readme":"README.litcoffee","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-08-25T17:02:30.000Z","updated_at":"2023-09-11T16:31:58.000Z","dependencies_parsed_at":"2022-08-21T17:40:49.255Z","dependency_job_id":null,"html_url":"https://github.com/zot/DOMCursor","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2FDOMCursor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2FDOMCursor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2FDOMCursor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2FDOMCursor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zot","download_url":"https://codeload.github.com/zot/DOMCursor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248997087,"owners_count":21195797,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T07:11:55.162Z","updated_at":"2025-04-15T02:45:59.262Z","avatar_url":"https://github.com/zot.png","language":"CoffeeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"DOMCursor\n=========\n\nFiltered cursoring on DOM trees.  DOMCursors can move forwards or backwards, by node or by character, with settable filters that can seamlessly skip over parts of the DOM.\n\nThis readme file is also the code.\n\nHere are some examples (I'm wrapping them in a -\u003e to make a no-op that gets syntax highlighting in viewers that support it).\n\n    -\u003e\n\nIn Leisure, I use it like this, to retrieve text from the page (scroll down to see docs on these methods, by the way):\n\n      DOMCursor.prototype.filterOrg = -\u003e\n        @addFilter (n)-\u003e !n.hasAttribute('data-nonorg') || 'skip'\n\n      domCursor = (node, pos)-\u003e new DOMCursor(node, pos).filterOrg()\n\n      # full text for node\n      getOrgText = (node)-\u003e\n        domCursor node.firstChild, 0\n          .mutable()\n          .filterTextNodes()\n          .filterParent node\n          .getText()\n\nAnd like this for cursor movement.  Once I have the cursor, I can use forwardChar, backwardChar, forwardLine, backwardLine to move it around:\n\n      domCursorForCaret = -\u003e\n        sel = getSelection()\n        parent = parentForNode sel.focusNode\n        n = domCursor sel.focusNode, sel.focusOffset\n          .mutable()\n          .filterVisibleTextNodes()\n          .filterParent parent\n          .firstText()\n        if n.pos \u003c n.node.length then n else n.next()\n\nDOMCursor Class\n---------------\n\nDOMCursors are immutable -- operations on them return new DOMCursers.\nThere are two ways to get mutabile cursors, sending @mutable() or\nsending @withMutations (m)-\u003e ...\n\nA DOMCursor has a node, a position, a filter, and a type.\n\n- node: like with ranges, a DOM node\n- position: like with ranges, either the index of a child, for elements, or the index of a character, for text nodes.\n- filter: a function used by @next() and @prev() to skip over portions of DOM. It returns\n  - truthy: to accept a node but its children are still filtered\n  - falsey: to reject a node but its children are still filtered\n  - 'skip': to skip a node and its children\n  - 'quit': to end to make @next() or @prev() return an empty DOMCursor\n- type: 'empty', 'text', or 'element'\n\nThe class...\n\n    class DOMCursor\n      constructor: (@node, @pos, filter)-\u003e\n        @pos = @pos ? 0\n        @filter = filter || -\u003e true\n        @computeType()\n      computeType: -\u003e\n        @type = if !@node then 'empty'\n        else if @node.nodeType == Node.TEXT_NODE then 'text'\n        else 'element'\n        this\n      newPos: (node, pos)-\u003e new DOMCursor node, pos, @filter\n\n**isEmpty** returns true if the cursor is empty\n\n      isEmpty: -\u003e @type == 'empty'\n\n**setFilter** sets the filter\n\n      setFilter: (f)-\u003e new DOMCursor @node, @pos, f\n\n**addFilter** adds a filter\n\n      addFilter: (filt)-\u003e\n        oldFilt = @filter\n        @setFilter (n)-\u003e\n          (((r1 = oldFilt n) in ['quit', 'skip']) \u0026\u0026 r1) || (((r2 = filt n) in ['quit', 'skip']) \u0026\u0026 r2) || (r1 \u0026\u0026 r2)\n\n**next** moves to the next filtered node\n\n      next: (up)-\u003e\n        saved = @save()\n        n = @nodeAfter up\n        while !n.isEmpty()\n          switch res = @filter n\n            when 'skip'\n              n = n.nodeAfter true\n              continue\n            when 'quit' then break\n            else\n              if res then return n\n          n = n.nodeAfter()\n        @restore(saved).emptyNext()\n\n**prev** moves to the next filtered node\n\n      prev: (up)-\u003e\n        saved = @save()\n        n = @nodeBefore up\n        while !n.isEmpty()\n          switch res = @filter n\n            when 'skip'\n              n = n.nodeBefore true\n              continue\n            when 'quit' then break\n            else\n              if res then return n\n          n = n.nodeBefore()\n        @restore(saved).emptyPrev()\n\n**moveCaret** move the document selection to the current position\n\n      moveCaret: (r)-\u003e\n        if !r then r = document.createRange()\n        r.setStart @node, @pos\n        r.collapse true\n        selectRange r\n        this\n\n**firstText** find the first text node (the 'backwards' argument is optional and if true,\nindicates to find the first text node behind the cursor).\n\n      firstText: (backwards)-\u003e\n        n = this\n        while !n.isEmpty() \u0026\u0026 n.type != 'text'\n          n = (if backwards then n.prev() else n.next())\n        n\n\n**countChars** count the characters in the filtered nodes until we get to (node, pos)\n\nInclude (node, 0) up to but not including (node, pos)\n\n      countChars: (node, pos)-\u003e\n        n = this\n        tot = 0\n        while !n.isEmpty() \u0026\u0026 n.node != node\n          if n.type == 'text' then tot += n.node.length\n          n = n.next()\n        if n.isEmpty() || n.node != node then -1\n        else if n.type == 'text' then tot + pos\n        else tot\n\n**forwardChars** moves the cursor forward by count characters\n\nif contain is true and the final location is 0 then go to the end of\nthe previous text node (node, node.length)\n\n      forwardChars: (count, contain)-\u003e\n        n = this\n        while !n.isEmpty() \u0026\u0026 0 \u003c= count\n          if n.type == 'text'\n            if count \u003c n.node.length\n              if count == 0 \u0026\u0026 contain\n                n = n.prev()\n                while n.type != 'text' then n = n.prev()\n                return n.newPos n.node, n.node.length\n              else return n.newPos n.node, count\n            count -= n.node.length\n          n = n.next()\n        n.emptyNext()\n\n**hasAttribute** returns true if the node is an element and has the attribute\n\n      hasAttribute: (a)-\u003e @node?.nodeType == Node.ELEMENT_NODE \u0026\u0026 @node.hasAttribute a\n\n**getAttribute** returns the attribute if the node is an element and has the attribute\n\n      getAttribute: (a)-\u003e @node?.nodeType == Node.ELEMENT_NODE \u0026\u0026 @node.getAttribute a\n\n**filterTextNodes** adds text node filtering to the current filter; the cursor will only find text nodes\n\n      filterTextNodes: -\u003e @addFilter (n)-\u003e n.type == 'text'\n\n**filterTextNodes** adds visible text node filtering to the current filter; the cursor will only find visible text nodes\n\n      filterVisibleTextNodes: -\u003e @filterTextNodes().addFilter (n)-\u003e !isCollapsed n.node\n\n**filterParent** adds parent filtering to the current filter; the cursor will only find nodes that are contained in the parent (or equal to it)\n\n      filterParent: (parent)-\u003e\n        if !parent then @setFilter -\u003e 'quit'\n        else @addFilter (n)-\u003e parent.contains(n.node) || 'quit'\n\n**filterRange** adds range filtering to the current filter; the cursor will only find nodes that are contained in the range\n\n      filterRange: (startContainer, startOffset, endContainer, endOffset)-\u003e\n        if !startOffset?\n          if startContainer instanceof Range\n            r = startContainer\n            startContainer = r.startContainer\n            startOffset = r.startOffset\n            endContainer = r.endContainer\n            endOffset = r.endOffset\n          else return this\n        @addFilter (n)-\u003e\n          startPos = startContainer.compareDocumentPosition n.node\n          (if startPos == 0 then startOffset \u003c= n.pos \u003c= endOffset\n          else if startPos \u0026 Node.DOCUMENT_POSITION_FOLLOWING\n            endPos = endContainer.compareDocumentPosition n.node\n            if endPos == 0 then n.pos \u003c= endOffset\n            else endPos \u0026 Node.DOCUMENT_POSITION_PRECEDING) || 'quit'\n\n**getText** gets all of the text at or after the cursor (useful with filtering; see above)\n\n      getText: -\u003e\n        n = @mutable().firstText()\n        if n.isEmpty() then ''\n        else\n          t = n.node.data.substring n.pos\n          while !n.next().isEmpty()\n            if n.type == 'text' then t += n.node.data.substring n.pos\n          if t.length\n            while n.type != 'text'\n              n.prev()\n            n.pos = n.node.length\n            while n.pos \u003e 0 \u0026\u0026 reject n.filter n\n              n.pos--\n            t.substring 0, t.length - n.node.length + n.pos\n          else ''\n\n**isNL** returns whether the current character is a newline\n\n      isNL: -\u003e @type == 'text' \u0026\u0026 @node.data[@pos] == '\\n'\n\n**endsInNL** returns whether the current node ends with a newline\n\n      endsInNL: -\u003e @type == 'text' \u0026\u0026 @node.data[@node.length - 1] == '\\n'\n\n**moveToStart** moves to the beginning of the node\n\n      moveToStart: -\u003e @newPos @node, 0\n\n**moveToNextStart** moves to the beginning of the next node\n\n      moveToNextStart: -\u003e @next().moveToStart()\n\n**moveToEnd** moves to the textual end the node (1 before the end if the node\nends in a newline)\n\n      moveToEnd: -\u003e\n        end = @node.length - (if @endsInNL() then 1 else 0)\n        @newPos @node, end\n\n**moveToPrevEnd** moves to the textual end the previous node (1 before\nthe end if the node ends in a newline)\n\n      moveToPrevEnd: -\u003e @prev().moveToEnd()\n\n**forwardLine** moves to the next line, trying to keep the current screen pixel column.  Optionally takes a goalFunc that takes the position's screen pixel column as input and returns -1, 0, or 1 from comparing the input to the an goal column\n\n      forwardLine: (goalFunc)-\u003e\n        if !goalFunc then goalFunc = -\u003e -1\n        r = @charRect()\n        bottom = r.bottom\n        line = 0\n        n = this\n        while n = n.forwardChar()\n          if n.isEmpty() then return n.backwardChar()\n          r = n.charRect()\n          if r.bottom != bottom\n            bottom = r.bottom\n            line++\n          if line == 1 \u0026\u0026 goalFunc(r.left) \u003e -1 then return n\n          if line == 2 then return n.backwardChar()\n\n**backwardLine** moves to the previous line, trying to keep the current screen pixel column.  Optionally takes a goalFunc that takes the position's screen pixel column as input and returns -1, 0, or 1 from comparing the input to an internal goal column\n\n      backwardLine: (goalFunc)-\u003e\n        # optional goalFunc takes the position's screen pixel column as input\n        # It returns -1, 0, or 1, comparing the input to the internal goal column\n        if !goalFunc then goalFunc = -\u003e -1\n        r = @charRect()\n        prevTop = top = r.top\n        line = 0\n        n = this\n        while n = n.backwardChar()\n          if n.isEmpty() then return n.forwardChar()\n          r = n.charRect()\n          if r.top != top\n            top = r.top\n            line++\n          if line == 1\n            switch goalFunc r.left\n              when 0 then return n\n              when -1 then return (if prevTop == top then n.forwardChar() else n)\n          if line == 2 then return n.forwardChar()\n          prevTop = top\n\n**forwardChar** move forward by one character (using the filter)\n\n      forwardChar: -\u003e\n        r = stubbornCharRectNext(@node, @pos)\n        left = r?.left\n        bottom = r?.bottom\n        n = this\n        while n = (if n.pos + 1 \u003c n.node.length then n.newPos n.node, n.pos + 1 else n.next())\n          if n.isEmpty() || ((r = stubbornCharRectNext(n.node, n.pos)) \u0026\u0026 (left != r?.left || bottom != r?.bottom)) then return n\n\n**backwardChar** move backward by one character (using the filter)\n\n      backwardChar: -\u003e\n        r = stubbornCharRectPrev @node, @pos\n        n = this\n        while r \u0026\u0026 n = (if n.pos \u003e 0 then n.newPos n.node, n.pos  - 1 else n.prev())\n          if n.isEmpty() || n.moved(r) then return n\n        n\n\n**show** scroll the position into view.  Optionally takes a rectangle representing a toolbar at the top of the page (sorry, this is a bit limited at the moment)\n\n      show: (topRect)-\u003e\n        posRect = @charRect()\n        top = if topRect?.width \u0026\u0026 topRect.top == 0 then topRect.bottom else 0\n        if posRect.bottom \u003e window.innerHeight then window.scrollBy 0, posRect.bottom - window.innerHeight\n        else if posRect.top \u003c top then window.scrollBy 0, posRect.top - top\n        this\n\n**immutable** return an immutable version of this cursor\n\n      immutable: -\u003e this\n\n**withMutations** call a function with a mutable version of this cursor\n\n      withMutations: (func)-\u003e func @copy().mutable()\n\n**mutable** return a mutable version of this cursor\n\n      mutable: -\u003e new MutableDOMCursor @node, @pos, @filter\n\n**save** generate a memento which can be used to restore the state (used by mutable cursors)\n\n      save: -\u003e this\n\n**restore** restore the state from a memento (used by mutable cursors)\n\n      restore: (n)-\u003e n.immutable()\n\n**copy** return a copy of this cursor\n\n      copy: -\u003e this\n\n**nodeAfter** low level method that moves to the unfiltered node after the current one\n\n      nodeAfter: (up)-\u003e\n        node = @node\n        while node\n          if node.nodeType == Node.ELEMENT_NODE \u0026\u0026 !up \u0026\u0026 node.childNodes.length\n            return @newPos node.childNodes[0], 0\n          else if node.nextSibling\n            return @newPos node.nextSibling, 0\n          else\n            up = true\n            node = node.parentNode\n        @emptyNext()\n\n**emptyNext** returns an empty cursor whose prev is the current node\n\n      emptyNext: -\u003e\n        # return an empty next node where\n        #   prev returns this node\n        #   next returns the same empty node\n        __proto__: emptyDOMCursor\n        filter: @filter\n        prev: (up)=\u003e if up then @prev up else this\n        nodeBefore: (up)=\u003e if up then @nodeBefore up else this\n\n**nodeBefore** low level method that moves to the unfiltered node before the current one\n\n      nodeBefore: (up)-\u003e\n        node = @node\n        while node\n          if node.nodeType == Node.ELEMENT_NODE \u0026\u0026 !up \u0026\u0026 node.childNodes.length\n            newNode = node.childNodes[node.childNodes.length - 1]\n          else if node.previousSibling then newNode = node.previousSibling\n          else\n            up = true\n            node = node.parentNode\n            continue\n          return @newPos newNode, newNode.length\n        @emptyPrev()\n\n**emptyPrev** returns an empty cursor whose next is the current node\n\n      emptyPrev: -\u003e\n        # return an empty prev node where\n        #   next returns this node\n        #   prev returns the same empty node\n        __proto__: emptyDOMCursor\n        filter: @filter\n        next: (up)=\u003e if up then @next up else this\n        nodeAfter: (up)=\u003e if up then @nodeAfter up else this\n\n**moved** return whether a rectangle is at a different position than the current character\n\n      moved: (rec)-\u003e\n        (@node.length \u003e @pos) \u0026\u0026 (r2 = stubbornCharRectPrev @node, @pos) \u0026\u0026 (rec.top != r2.top || rec.left != r2.left)\n      charRect: (r, prev)-\u003e\n        if prev\n          stubbornCharRectPrev(@node, @pos, r) || stubbornCharRectNext(@node, @pos, r)\n        else stubbornCharRect @node, @pos, r\n\nEmptyDOMCursor Class\n--------------------\n\nAn empty cursor\n\n    class EmptyDOMCursor extends DOMCursor\n      moveCaret: -\u003e this\n      show: -\u003e this\n      nodeAfter: -\u003e this\n      nodeBefore: -\u003e this\n      next: -\u003e this\n      prev: -\u003e this\n\n    #singleton empty node cursor\n    emptyDOMCursor = new EmptyDOMCursor()\n\nMutableDOMCursor Class\n----------------------\n\nA mutable cursor -- cursor movement, filter changes, etc. change the cursor instead of returning a new one.\n\n    class MutableDOMCursor extends DOMCursor\n      constructor: (@node, @pos, @filter)-\u003e super node, pos, filter\n      setFilter: (@filter)-\u003e this\n      newPos: (@node, @pos)-\u003e @computeType()\n      copy: -\u003e new MutableDOMCursor @node, @pos, @filter\n      mutable: -\u003e this\n      immutable: -\u003e new DOMCursor @node, @pos, @filter\n      save: -\u003e @immutable()\n      restore: (np)-\u003e\n        @node = np.node\n        @pos = np.pos\n        @filter = np.filter\n        this\n      emptyPrev: -\u003e\n        @type = 'empty'\n        @next = (up)-\u003e\n          @revertEmpty()\n          if up then @next up else this\n        @nodeAfter = (up)-\u003e\n          @computeType()\n          if up then @nodeAfter up else this\n        @prev = -\u003e this\n        @nodeBefore = -\u003e this\n        this\n      revertEmpty: -\u003e\n        @computeType()\n        delete @next\n        delete @prev\n        delete @nodeAfter\n        delete @nodeBefore\n        this\n      emptyNext: -\u003e\n        @type = 'empty'\n        @prev = (up)-\u003e\n          @revertEmpty()\n          if up then @prev up else this\n        @nodeBefore = (up)-\u003e\n          @computeType()\n          if up then @nodeBefore up else this\n        @next = -\u003e this\n        @nodeAfter = -\u003e this\n        this\n\nUtility functions\n-----------------\n\nThese are available as properties on DOMCursor.\n\n    # Thanks to rangy for this: https://github.com/timdown/rangy\n    isCollapsed = (node)-\u003e\n      if node\n        type = node.nodeType\n        type == 7 || # PROCESSING_INSTRUCTION\n        type == 8 || # COMMENT\n        (type == Node.TEXT_NODE \u0026\u0026 (node.data == '' || isCollapsed(node.parentNode))) ||\n        /^(script|style)$/i.test(node.nodeName) ||\n        #(type == Node.ELEMENT_NODE \u0026\u0026 (node.offsetWidth == 0 || node.offsetHeight == 0))\n        (type == Node.ELEMENT_NODE \u0026\u0026 node.offsetHeight == 0)\n\n    selectRange = (r)-\u003e\n      sel = getSelection()\n      sel.removeAllRanges()\n      sel.addRange r\n\n    reject = (filterResult)-\u003e !filterResult || (filterResult in ['quit', 'skip'])\n\n    # charRect returns null for newlines when not using pre\n    stubbornCharRect = (node, pos, r)-\u003e\n      stubbornCharRectNext(node, pos, r) || stubbornCharRectPrev(node, pos, r)\n\n    stubbornCharRectNext = (node, pos, r)-\u003e\n      r = r || document.createRange()\n      for i in [pos ... node.length] by 1\n        if rec = charRect node, i, r then return rec\n      null\n\n    stubbornCharRectPrev = (node, pos, r)-\u003e\n      r = r || document.createRange()\n      for i in [pos .. 0] by -1\n        if rec = charRect node, i, r then return rec\n      null\n\n    charRect = (node, pos, r)-\u003e\n      r = r || document.createRange()\n      r.setStart node, pos\n      r.collapse true\n      rects = r.getClientRects()\n      rects[rects.length - 1]\n\n    DOMCursor.MutableDOMCursor = MutableDOMCursor\n    DOMCursor.emptyDOMCursor = emptyDOMCursor\n    DOMCursor.isCollapsed = isCollapsed\n    DOMCursor.selectRange = selectRange\n\n    @DOMCursor = DOMCursor\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzot%2Fdomcursor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzot%2Fdomcursor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzot%2Fdomcursor/lists"}