From: Jason Woofenden Date: Mon, 7 Mar 2016 01:06:27 +0000 (-0500) Subject: whitespace dedup works (rewrite) X-Git-Url: https://jasonwoof.com/gitweb/?p=peach-html5-editor.git;a=commitdiff_plain;h=2cd394146a6b75a69a00fdf91530fa2fe6e3a3c3 whitespace dedup works (rewrite) --- diff --git a/editor.coffee b/editor.coffee index da40866..da7b393 100644 --- a/editor.coffee +++ b/editor.coffee @@ -46,6 +46,12 @@ get_el_bounds = (el) -> h: rect.height ? (rect.top - rect.bottom) } +is_display_block = (el) -> + if el.currentStyle? + return el.currentStyle.display is 'block' + else + return window.getComputedStyle(el, null).getPropertyValue('display') is 'block' + # Warning: currently assumes you're asking about a single character # Note: chromium returns multiple bounding rects for a space at a line-break # Note: chromium's getBoundingClientRect() is broken (when zero-area client rects) @@ -332,61 +338,117 @@ is_space_code = (char_code) -> is_space = (chr) -> return is_space_code chr.charCodeAt 0 -# warning: contains browser-specific hackery -is_space_significant = (n, i) -> - range = document.createRange() - range.setStart n.el, i - range.setEnd n.el, i + 1 - rects = range.getClientRects() - bounding_rect = range.getBoundingClientRect() - if rects.length is 0 - return false - if rects.length > 1 - # chromium returns two rects in both these cases: - # 1. a space that is word-wrapped. one rect on each line. Note that - # chromium does _not_ do this for _all_ spaces that are word wrapped. - # 2. the last (insignificant) space in a sequence of collapsing spaces - # in this case the rects are identical. - if rects[1].top > rects[0].top - return true - width = rects[0].width ? (rects[0].right - rects[0].left) - if width > 0 - return true - # firefox reports the space that's word-wrapped as zero width - if n.text.length > i + 1 - range.setStart n.el, i + 1 - range.setEnd n.el, i + 2 - next_rects = range.getClientRects() - if next_rects.length > 0 - if next_rects[0].top > rects[0].top - # next character is lower on the screen, so this must be a word-wrap space - return true - else - # FIXME detect word-wrap in last character - # could be followed by an inline block with no starting space - # FIXME chromium gets here for a significant space at the begining of a - # text node that word-wraps - return false +# pass a array of nodes (from parser library, ie it should have .el and .text) +tree_dedup_space = (tree) -> + prev = cur = next = null + prev_i = cur_i = next_i = 0 + prev_pos = pos = next_pos = null + prev_px = cur_px = next_px = null + first = true + removed_char = null -# pass a node (from parser library, ie it should have .el and .text) -remove_insignificant_whitespace = (n) -> - changed = false - if n.type is TYPE_TEXT - i = 0 - while i < n.text.length - if is_space_code n.text.charCodeAt i - if is_space_significant n, i - i += 1 - else - n.el.textContent = n.text = (n.text.substr 0, i) + (n.text.substr i + 1) - changed = true + iterate = (tree, cb) -> + for n in tree + if n.type is TYPE_TEXT + i = 0 + while i < n.text.length # don't foreach, cb might remove chars + removed = cb n, i + unless removed + i += 1 + if n.type is TYPE_TAG + block = is_display_block n.el + if block + cb null + if n.children.length > 0 + iterate n.children, cb + if block + cb null + # remove cur char + remove = -> + removed_char = cur.text.charAt(cur_i) + cur.el.textContent = cur.text = (cur.text.substr 0, cur_i) + (cur.text.substr cur_i + 1) + if next is cur # in same text node + if next_i is 0 + throw "how is this possible?" + next_i -= 1 + return true + # undo remove() + put_it_back = -> + cur.el.textContent = cur.text = (cur.text.substr 0, cur_i) + removed_char + (cur.text.substr cur_i) + if next is cur # in same text node + next_i += 1 + return false + # return true if cur was removed from the dom (ie re-use same prev) + operate = -> + # cur definitately set + # prev and/or next might be null, indicating the start/end of a display:block + return false unless is_space_code cur.text.charCodeAt cur_i + bounds = text_range_bounds cur.el, cur_i, cur_i + 1 + # consistent cases: + # 1. zero rects returned by getClientRects() means collapsed space + if bounds is null + return remove() + # 2. width greater than zero means visible space + if bounds.w > 0 + return false + # now the weird edge cases... + # + # firefox and chromium both report zero width for characters at the end + # of a line where the text wraps (automatically, due to word-wrap) to + # the next line. These do not appear to be distinguishable from + # collapsed spaces via the range/bounds api, so... + # + # remove it from the dom, and if prev or next moves, put it back. + if prev? and not prev_px? + prev_px = cursor_to_xyh prev, prev_i + if next? and not next_px? + next_px = cursor_to_xyh next, next_i + #if prev is null and next is null + # parent_px = cur.parent.el.getBoundingClientRect() + remove() + if prev? + if prev_px? + new_prev_px = cursor_to_xyh prev, prev_i + if new_prev_px.x isnt prev_px.x or new_prev_px.y isnt prev_px.y + return put_it_back() else - i += 1 - if n.children.length > 0 - for c in n.children - if remove_insignificant_whitespace c - changed = true - return changed + console.log "this shouldn't happen, we remove spaces that don't locate" + if next? + if next_px? + new_next_px = cursor_to_xyh next, next_i + if new_next_px.x isnt next_px.x or new_next_px.y isnt next_px.y + return put_it_back() + #else + # console.log "removing space becase space after it is collapsed" + # if there's no prev or next (single space inside a block-level element?) check + # TODO scrapt this, or fix it so it works when there's no parent + # if prev is null and next is null + # new_parent_px = cur.parent.el.getBoundingClientRect() + # if new_parent_px.left isnt parent_px.left or new_parent_px.top isnt parent_px.top or new_parent_px.right isnt parent_px.right or new_parent_px.bottom isnt parent_px.bottom + # console.log "WEIRD: parent moved" + # return put_it_back() + # we didn't put it back + return true + # pass null at start/end of display:block + queue = (n, i) -> + next = n + next_i = i + next_px = null + if cur? + removed = operate() + else + removed = false + unless removed + prev = cur + prev_i = cur_i + prev_px = cur_px + cur = next + cur_i = next_i + cur_px = next_px + return removed + queue null + iterate tree, queue + queue null class PeachHTML5Editor constructor: (in_el, options = {}) -> @@ -539,7 +601,7 @@ class PeachHTML5Editor @tree = peach_parser.parse html, @parser_opts @clear_dom() instantiate_tree @tree, @idoc.body - remove_insignificant_whitespace type: TYPE_TAG, children: @tree + tree_dedup_space @tree @changed() changed: -> # FIXME don't export cursor placeholder (when cursor is between space characters) diff --git a/editor_tests_compiled.html b/editor_tests_compiled.html index 59fbb83..13338cf 100644 --- a/editor_tests_compiled.html +++ b/editor_tests_compiled.html @@ -9,14 +9,22 @@ box-sizing: border-box; width: 100%; } - /* test in firefox: iframe { width: 400px; } */ + /* iframe { width: 400px; } */

Peach HTML5 Editor test page (compiled version)

This version of the editor test page requires that you've compiled all the source files. (Just run make).

-

HTML view. Changes here propagate when you remove your cursor (press tab or click outside)

+

HTML view. Changes here propagate when you remove your cursor (press tab or click outside)