JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
mostly working text-run cleanup
authorJason Woofenden <jason@jasonwoof.com>
Tue, 12 Apr 2016 06:18:09 +0000 (02:18 -0400)
committerJason Woofenden <jason@jasonwoof.com>
Tue, 12 Apr 2016 06:18:09 +0000 (02:18 -0400)
editor.coffee

index 60ea832..4b26b31 100644 (file)
@@ -59,15 +59,19 @@ ws_props =
                space: true
                newline: true
                wrap: false
+               to_collapse: 'nowrap'
        'pre-wrap':
                space: true
                newline: true
                wrap: true
+               to_collapse: 'normal'
 
 # xml 1.0 spec, chromium and firefox accept these, plus lots of unicode chars
 valid_attr_regex = new RegExp '^[a-zA-Z_:][-a-zA-Z0-9_:.]*$'
 # html5 spec is much more lax, but chromium won't let me make at attribute with the name "4"
 js_attr_regex = new RegExp '^[oO][nN].'
+# html5 spec says that only these characters are collapsable
+multi_sp_regex = new RegExp '[\u0020\u0009\u000a\u000c\u000d][\u0020\u0009\u000a\u000c\u000d]'
 
 debug_dot_at = (doc, x, y) ->
        return # disabled
@@ -318,8 +322,8 @@ first_cursor_position = (tree) ->
                        cursor = new_cursor_position n: node, i: 0
                        if cursor?
                                found = cursor
-                               return true
-               return false
+                               return true # done traversing
+               return false # not done traversing
        return found # maybe null
 
 # this will fail when text has non-locatable cursor positions
@@ -335,10 +339,10 @@ find_next_cursor_position = (tree, cursor) ->
                        new_cursor = new_cursor_position n: node, i: 0
                        if new_cursor?
                                found = new_cursor
-                               return true
+                               return true # done traversing
                if node is cursor.n
                        state_before = false
-               return false
+               return false # not done traversing
        if found?
                return found
        return null
@@ -350,7 +354,7 @@ last_cursor_position = (tree) ->
                        cursor = new_cursor_position n: node, i: node.text.length
                        if cursor?
                                found = cursor
-               return false
+               return false # not done traversing
        return found # maybe null
 
 # this will fail when text has non-locatable cursor positions
@@ -364,12 +368,12 @@ find_prev_cursor_position = (tree, cursor) ->
        traverse_tree tree, (node) ->
                if node is cursor.n
                        found = found_prev # maybe null
-                       return true
+                       return true # done traversing
                if node.type is 'text'
                        new_cursor = new_cursor_position n: node, i: node.text.length
                        if new_cursor?
                                found_prev = new_cursor
-               return false
+               return false # not done traversing
        return found # maybe null
 
 find_up_cursor_position = (tree, cursor, ideal_x) ->
@@ -504,7 +508,7 @@ tree_remove_empty_text_nodes = (tree) ->
                if n.type is 'text'
                        if n.text.length is 0
                                empties.unshift n
-               return false
+               return false # not done traversing
        for n in empties
                # don't completely empty the tree
                if tree.length is 1
@@ -1039,9 +1043,31 @@ class PeachHTML5Editor
                loop
                        n = n.parent
                        return null unless n?
-                       return n if is_display_block n.el
+                       return n if @is_display_block n
                        return n if n is @tree_parent
                return null
+       # return a flat array of nodes (text, <br>, and later also inline-block)
+       # that are flowing/wrapping together. n can be the containing block, or any
+       # element inside it.
+       get_text_run: (n) ->
+               if @is_display_block n
+                       block = n
+               else
+                       block = @find_block_parent n
+                       return unless block?
+               ret = []
+               traverse_tree n.children, (n) =>
+                       if n.type is 'text'
+                               ret.push n
+                       else if n.type is 'tag'
+                               if n.name is 'br'
+                                       ret.push n
+                               else
+                                       disp = @computed_style n
+                                       if disp is 'inline-block'
+                                               ret.push n
+                       return false # not done traversing
+               return ret
        on_key_backspace: (e) ->
                return false unless @cursor?
                if @is_lone_space @cursor.n # false if it's not in a tag
@@ -1143,7 +1169,6 @@ class PeachHTML5Editor
                        while block.children.length > 0
                                n = block.children[block.children.length - 1]
                                @move_node n, dest, before
-                               block.children.pop()
                                before = n
                        @remove_node block
                        @text_cleanup dest
@@ -1314,6 +1339,8 @@ class PeachHTML5Editor
        #
        # text inside child tags are not consulted. Child tags are expected to have
        # this function applied to them when their content changes.
+       #
+       # FIXME stop using this and delete it. use @text_cleanup instead
        adjust_whitespace_style: (n) ->
                loop
                        break if @is_display_block n
@@ -1397,13 +1424,146 @@ class PeachHTML5Editor
        remove_character: (n, i) ->
                n.text = n.text.substr(0, i) + n.text.substr(i + 1)
                n.el.nodeValue = n.text
+       computed_style: (n, prop) ->
+               if n.type is 'text'
+                       n = n.parent
+               style = @iframe.contentWindow.getComputedStyle n.el, null
+               return style.getPropertyValue prop
+       # returns the new white-space value that will preserve spaces for node n
+       preserve_space: (n, ideal_target) ->
+               if n.type is 'text'
+                       target = n.parent
+               else
+                       target = n
+               while target isnt ideal_target and not target.el.style.whiteSpace
+                       unless target?
+                               console.log "bug #967123"
+                               return
+                       target = target.parent
+               ws = ws_props[target.el.style.whiteSpace]?.to_preserve
+               ws ?= 'pre-wrap'
+               target.el.style.whiteSpace = ws
+               @update_style_from_el target
+               return ws
+       update_style_from_el: (n) ->
+               style = n.el.getAttribute 'style'
+               if style?
+                       n.attrs.style = style
+               else
+                       if n.attrs.style?
+                               delete n.attrs.style
        # call this after you insert or remove inline nodes. It will:
        #    merge consecutive text nodes
        #    remove empty text nodes
        #    adjust white-space property
        text_cleanup: (n) ->
+               if @is_display_block n
+                       block = n
+               else
+                       block = @find_block_parent n
+                       return unless block?
+               run = @get_text_run block
+               return unless run?
+               # merge consecutive text nodes
+               if run.length > 1
+                       i = 1
+                       prev = run[0]
+                       while i < run.length
+                               n = run[i]
+                               if prev.type is 'text' and n.type is 'text'
+                                       if prev.parent is n.parent
+                                               prev_i = n.parent.children.indexOf prev
+                                               n_i =    n.parent.children.indexOf n
+                                               if n_i is prev_i + 1
+                                                       prev.text = prev.text + n.text
+                                                       prev.el.textContent = prev.text
+                                                       @remove_node n
+                                                       run.splice i, 1
+                                                       continue # don't increment i or change prev
+                               i += 1
+                               prev = n
+               # remove empty text nodes
+               i = 0
+               while i < run.length
+                       n = run[i]
+                       if n.type is 'text'
+                               if n.text is ''
+                                       @remove_node n
+                                       # FIXME maybe remove parents recursively if this makes them empty
+                                       run.splice i, 1
+                                       continue # don't increment i
+                       i += 1
+               # note: inline tags can have white-space:pre-line/etc
+               # note: inline-blocks have their whitespace collapsed independantly of outer run
+               # note: inline-blocks are treated like non-whitespace char even if empty
+               if block.el.style.whiteSpace?
+                       ws = block.el.style.whiteSpace
+                       if ws_props[ws]
+                               if ws_props[ws].space
+                                       if ws_props[ws].to_collapse is 'normal'
+                                               block.el.style.whiteSpace = null
+                                       else
+                                               block.el.style.whiteSpace = ws_props[ws].to_collapse
+                                       @update_style_from_el block
+               # note: space after <br> colapses, but not space before
+               # check for spaces that would collapse without help
+               eats_start_sp = true # if the next node starts with space it collapses (unless pre)
+               prev = null
+               for n in run
+                       if n.type is 'tag'
+                               if n.name is 'br'
+                                       eats_start_sp = true
+                               else
+                                       eats_start_sp = false
+                       else # TEXT
+                               need_preserve = false
+                               if n.type isnt 'text'
+                                       console.log "bug #232308"
+                                       return
+                               if eats_start_sp
+                                       if is_space_code n.text.charCodeAt 0
+                                               need_preserve = true
+                               unless need_preserve
+                                       need_preserve = multi_sp_regex.test n.text
+                               if need_preserve
+                                       # do we have it already?
+                                       ws = @computed_style n, 'white-space' # FIXME implement this
+                                       unless ws_props[ws]?.space
+                                               # 2nd arg is ideal target for css rule
+                                               ws = @preserve_space n, block
+                                       eats_start_sp = false
+                               else
+                                       if is_space_code n.text.charCodeAt(n.text.length - 1)
+                                               ws = @computed_style n, 'white-space' # FIXME implement this
+                                               if ws_props[ws]?.space
+                                                       eats_start_sp = false
+                                               else
+                                                       eats_start_sp = true
+                                       else
+                                               eats_start_sp = false
+               # check if text ends with a collapsable space
+               if run.length > 0
+                       last = run[run.length - 1]
+                       if last.type is 'text'
+                               if eats_start_sp
+                                       @preserve_space last, block
+               return
+       css_clear: (n, prop) ->
+               return unless n.attrs.style?
+               return if n.attrs.style is ''
+               css_delimiter_regex = new RegExp('\s*;\s*', 'g') # FIXME make this global
+               styles = n.attrs.style.trim().split css_delimiter
+               return unless styles.length > 0
+               if styles[styles.length - 1] is ''
+                       styles.pop()
+                       return unless styles.length > 0
+               i = 0
+               while i < styles.length
+                       if styles[i].substr(0, 12) is 'white-space:'
+                               styles.splice i, 1
+                       else
+                               i += 1
                return
-               # FIXME implement this
        # WARNING: after calling this one or more times, you MUST:
        #    if it's inline: call @text_cleanup
        #    call @changed()
@@ -1435,7 +1595,7 @@ class PeachHTML5Editor
                else
                        new_parent.el.appendChild n.el, insert_before
                        new_parent.children.push n
-                       n.parent = new_parent
+               n.parent = new_parent
                return
        kill_cursor: -> # remove it, forget where it was
                if @cursor_visible