From 9200265e518a808e047614c036995d3f6d782fe8 Mon Sep 17 00:00:00 2001 From: bayre Date: Fri, 29 Apr 2022 15:16:38 +0100 Subject: [PATCH] DRY out whitespace handling --- .../compile/render_dom/wrappers/Fragment.ts | 59 +++--------- .../compile/render_ssr/handlers/Element.ts | 4 +- .../render_ssr/handlers/SlotTemplate.ts | 4 +- .../utils/remove_whitespace_children.ts | 73 --------------- src/compiler/compile/utils/trim.ts | 91 +++++++++++++++++++ 5 files changed, 108 insertions(+), 123 deletions(-) delete mode 100644 src/compiler/compile/render_ssr/handlers/utils/remove_whitespace_children.ts create mode 100644 src/compiler/compile/utils/trim.ts diff --git a/src/compiler/compile/render_dom/wrappers/Fragment.ts b/src/compiler/compile/render_dom/wrappers/Fragment.ts index 87ce775ca9..9dea1c1d53 100644 --- a/src/compiler/compile/render_dom/wrappers/Fragment.ts +++ b/src/compiler/compile/render_dom/wrappers/Fragment.ts @@ -18,7 +18,7 @@ import Window from './Window'; import { INode } from '../../nodes/interfaces'; import Renderer from '../Renderer'; import Block from '../Block'; -import { trim_start, trim_end } from '../../../utils/trim'; +import { trim_text_node, trim_first_text_node } from '../../utils/trim'; import { link } from '../../../utils/link'; import { Identifier } from 'estree'; @@ -43,13 +43,6 @@ const wrappers = { Window }; -function trimmable_at(child: INode, next_sibling: Wrapper): boolean { - // Whitespace is trimmable if one of the following is true: - // The child and its sibling share a common nearest each block (not at an each block boundary) - // The next sibling's previous node is an each block - return (next_sibling.node.find_nearest(/EachBlock/) === child.find_nearest(/EachBlock/)) || next_sibling.node.prev.type === 'EachBlock'; -} - export default class FragmentWrapper { nodes: Wrapper[]; @@ -64,7 +57,8 @@ export default class FragmentWrapper { this.nodes = []; let last_child: Wrapper; - let window_wrapper; + let wrapper: Wrapper; + let window_wrapper: Window; let i = nodes.length; while (i--) { @@ -86,20 +80,10 @@ export default class FragmentWrapper { } if (child.type === 'Text') { - let { data } = child; - - // We want to remove trailing whitespace inside an element/component/block, - // *unless* there is no whitespace between this node and its next sibling - if (this.nodes.length === 0) { - const should_trim = ( - next_sibling ? (next_sibling.node.type === 'Text' && /^\s/.test(next_sibling.node.data) && trimmable_at(child, next_sibling)) : !child.has_ancestor('EachBlock') - ); - - if (should_trim && !child.keep_space()) { - data = trim_end(data); - if (!data) continue; - } - } + if (child.should_skip()) continue; + + const data = trim_text_node(child, this.nodes, next_sibling); + if (!data) continue; // glue text nodes (which could e.g. be separated by comments) together if (last_child && last_child.node.type === 'Text') { @@ -107,37 +91,20 @@ export default class FragmentWrapper { continue; } - const wrapper = new Text(renderer, block, parent, child, data); - if (wrapper.skip) continue; - - this.nodes.unshift(wrapper); - - link(last_child, last_child = wrapper); + wrapper = new Text(renderer, block, parent, child, data); } else { const Wrapper = wrappers[child.type]; if (!Wrapper) continue; - const wrapper = new Wrapper(renderer, block, parent, child, strip_whitespace, last_child || next_sibling); - this.nodes.unshift(wrapper); - - link(last_child, last_child = wrapper); + wrapper = new Wrapper(renderer, block, parent, child, strip_whitespace, last_child || next_sibling); } + + this.nodes.unshift(wrapper); + link(last_child, last_child = wrapper); } if (strip_whitespace) { - const first = this.nodes[0] as Text; - - if (first && first.node.type === 'Text' && !first.node.keep_space()) { - first.data = trim_start(first.data); - if (!first.data) { - first.var = null; - this.nodes.shift(); - - if (this.nodes[0]) { - this.nodes[0].prev = null; - } - } - } + trim_first_text_node(this.nodes); } if (window_wrapper) { diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index 5957e1db0d..fce59aad2b 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -5,7 +5,7 @@ import Renderer, { RenderOptions } from '../Renderer'; import Element from '../../nodes/Element'; import { p, x } from 'code-red'; import Expression from '../../nodes/shared/Expression'; -import remove_whitespace_children from './utils/remove_whitespace_children'; +import { trim_text_nodes } from '../../utils/trim'; import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing'; import { namespaces } from '../../../utils/namespaces'; import { start_newline } from '../../../utils/patterns'; @@ -13,7 +13,7 @@ import { Expression as ESExpression } from 'estree'; export default function (node: Element, renderer: Renderer, options: RenderOptions) { - const children = remove_whitespace_children(node.children, node.next); + const children = trim_text_nodes(node.children, node.next); // awkward special case let node_contents; diff --git a/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts b/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts index 09f3293301..edaf63b49b 100644 --- a/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts +++ b/src/compiler/compile/render_ssr/handlers/SlotTemplate.ts @@ -1,6 +1,6 @@ import Renderer, { RenderOptions } from '../Renderer'; import SlotTemplate from '../../nodes/SlotTemplate'; -import remove_whitespace_children from './utils/remove_whitespace_children'; +import { trim_text_nodes } from '../../utils/trim'; import { get_slot_scope } from './shared/get_slot_scope'; import InlineComponent from '../../nodes/InlineComponent'; import { get_const_tags } from './shared/get_const_tags'; @@ -9,7 +9,7 @@ export default function(node: SlotTemplate, renderer: Renderer, options: RenderO slot_scopes: Map; }) { const parent_inline_component = node.parent as InlineComponent; - const children = remove_whitespace_children(node instanceof SlotTemplate ? node.children : [node], node.next); + const children = trim_text_nodes(node instanceof SlotTemplate ? node.children : [node], node.next); renderer.push(); renderer.render(children, options); diff --git a/src/compiler/compile/render_ssr/handlers/utils/remove_whitespace_children.ts b/src/compiler/compile/render_ssr/handlers/utils/remove_whitespace_children.ts deleted file mode 100644 index 7733c89cb8..0000000000 --- a/src/compiler/compile/render_ssr/handlers/utils/remove_whitespace_children.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { INode } from '../../../nodes/interfaces'; -import { trim_end, trim_start } from '../../../../utils/trim'; -import { link } from '../../../../utils/link'; - -// similar logic from `compile/render_dom/wrappers/Fragment` -// We want to remove trailing whitespace inside an element/component/block, -// *unless* there is no whitespace between this node and its next sibling -export default function remove_whitespace_children(children: INode[], next?: INode): INode[] { - const nodes: INode[] = []; - let last_child: INode; - let i = children.length; - while (i--) { - const child = children[i]; - - if (child.type === 'Text') { - if (child.should_skip()) { - continue; - } - - let { data } = child; - - if (nodes.length === 0) { - const should_trim = next - ? next.type === 'Text' && - /^\s/.test(next.data) && - trimmable_at(child, next) - : !child.has_ancestor('EachBlock'); - - if (should_trim && !child.keep_space()) { - data = trim_end(data); - if (!data) continue; - } - } - - // glue text nodes (which could e.g. be separated by comments) together - if (last_child && last_child.type === 'Text') { - last_child.data = data + last_child.data; - continue; - } - - nodes.unshift(child); - link(last_child, last_child = child); - } else { - nodes.unshift(child); - link(last_child, last_child = child); - } - } - - const first = nodes[0]; - if (first && first.type === 'Text' && !first.keep_space()) { - first.data = trim_start(first.data); - if (!first.data) { - first.var = null; - nodes.shift(); - - if (nodes[0]) { - nodes[0].prev = null; - } - } - } - - return nodes; -} - -function trimmable_at(child: INode, next_sibling: INode): boolean { - // Whitespace is trimmable if one of the following is true: - // The child and its sibling share a common nearest each block (not at an each block boundary) - // The next sibling's previous node is an each block - return ( - next_sibling.find_nearest(/EachBlock/) === - child.find_nearest(/EachBlock/) || next_sibling.prev.type === 'EachBlock' - ); -} diff --git a/src/compiler/compile/utils/trim.ts b/src/compiler/compile/utils/trim.ts new file mode 100644 index 0000000000..8dae1268e2 --- /dev/null +++ b/src/compiler/compile/utils/trim.ts @@ -0,0 +1,91 @@ +import { INode } from '../nodes/interfaces'; +import TextNode from '../nodes/Text'; +import Wrapper from '../render_dom/wrappers/shared/Wrapper'; +import TextWrapper from '../render_dom/wrappers/Text'; +import { trim_end, trim_start } from '../../utils/trim'; +import { link } from '../../utils/link'; + +function trimmable_at(child: INode, next_sibling: INode): boolean { + // Whitespace is trimmable if one of the following is true: + // The child and its sibling share a common nearest each block (not at an each block boundary) + // The next sibling's previous node is an each block + return ( + next_sibling.find_nearest(/EachBlock/) === child.find_nearest(/EachBlock/) || + next_sibling.prev.type === 'EachBlock' + ); +} + +export function trim_text_node( + node: TextNode, + nodes: INode[] | Wrapper[], + next: INode | Wrapper +): string { + let { data } = node; + const next_node = + next && (next as TextWrapper).node ? (next as TextWrapper).node : (next as TextNode); + // We want to remove trailing whitespace inside an element/component/block, + // *unless* there is no whitespace between this node and its next sibling + if (nodes.length === 0) { + const should_trim = next_node + ? next_node.type === 'Text' && /^\s/.test(node.data) && trimmable_at(node, next_node) + : !node.has_ancestor('EachBlock'); + if (should_trim && !node.keep_space()) { + data = trim_end(data); + } + } + return data; +} + +export function trim_first_text_node(nodes: INode[] | Wrapper[]): void { + // bail early if there's no first node + if (!nodes[0]) return; + // determine whether we're handling a node or a wrapper + const first = (nodes[0] as TextWrapper).node ? (nodes[0] as TextWrapper) : (nodes[0] as TextNode); + const first_node = (first as TextWrapper).node + ? (first as TextWrapper).node + : (first as TextNode); + // check if the (derived) node requires trimming + if (first_node.type === 'Text' && !first_node.keep_space()) { + // mutate the original data accordingly + first.data = trim_start(first.data); + if (!first.data) { + first.var = null; + nodes.shift(); + if (nodes[0]) { + nodes[0].prev = null; + } + } + } +} + +// a simplified version of the iterative logic in `compile/render_dom/wrappers/Fragment` +// for use in ssr handlers +export function trim_text_nodes(nodes: INode[], next: INode): INode[] { + const trimmed_nodes: INode[] = []; + let last_child: INode; + let i = nodes.length; + + while (i--) { + const child = nodes[i]; + + if (child.type === 'Text') { + if (child.should_skip()) continue; + + const data = trim_text_node(child, trimmed_nodes, next); + if (!data) continue; + + // glue text nodes (which could e.g. be separated by comments) together + if (last_child && last_child.type === 'Text') { + last_child.data = data + last_child.data; + continue; + } + } + + trimmed_nodes.unshift(child); + link(last_child, (last_child = child)); + } + + trim_first_text_node(trimmed_nodes); + + return trimmed_nodes; +}