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
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
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
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
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) ->
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
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
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
#
# 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
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()
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