feat: improve hydration, claim static html elements using innerHTML instead of deopt to claiming every nodes (#7426)

Related: #7341, #7226

For purely static HTML, instead of walking the node tree and claiming every node/text etc, hydration now uses the same innerHTML optimization technique for hydration compared to normal create. It uses a new data-svelte-h attribute which is added upon server side rendering containing a hash (computed at build time), and then comparing that hash in the client to ensure it's the same node. If the hash is the same, the whole child content is expected to be the same. If the hash is different, the whole child content is replaced with innerHTML.

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/8530/head
Tan Li Hau 1 year ago committed by GitHub
parent 6f8cdf3b0f
commit df2f656557
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors';
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap';
import Tag from './nodes/shared/Tag';
interface ComponentOptions {
namespace?: string;
@ -110,6 +111,8 @@ export default class Component {
slots: Map<string, Slot> = new Map();
slot_outlets: Set<string> = new Set();
tags: Tag[] = [];
constructor(
ast: Ast,
source: string,
@ -761,6 +764,7 @@ export default class Component {
this.hoist_instance_declarations();
this.extract_reactive_declarations();
this.check_if_tags_content_dynamic();
}
post_template_walk() {
@ -1479,6 +1483,12 @@ export default class Component {
unsorted_reactive_declarations.forEach(add_declaration);
}
check_if_tags_content_dynamic() {
this.tags.forEach(tag => {
tag.check_if_content_dynamic();
});
}
warn_if_undefined(name: string, node, template_scope: TemplateScope) {
if (name[0] === '$') {
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {

@ -59,6 +59,11 @@ export default class Attribute extends Node {
return expression;
});
}
if (this.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
}
}
get_dependencies() {

@ -27,6 +27,8 @@ export default class AwaitBlock extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);

@ -33,6 +33,8 @@ export default class EachBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring

@ -15,6 +15,7 @@ import { is_name_contenteditable, get_contenteditable_attr, has_contenteditable_
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import hash from '../utils/hash';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
@ -503,6 +504,25 @@ export default class Element extends Node {
this.optimise();
component.apply_stylesheet(this);
if (this.parent) {
if (this.actions.length > 0 ||
this.animation ||
this.bindings.length > 0 ||
this.classes.length > 0 ||
this.intro || this.outro ||
this.handlers.length > 0 ||
this.styles.length > 0 ||
this.name === 'option' ||
this.is_dynamic_element ||
this.tag_expr.dynamic_dependencies().length ||
this.is_dynamic_element ||
component.compile_options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
}
validate() {
@ -1262,6 +1282,20 @@ export default class Element extends Node {
}
});
}
get can_use_textcontent() {
return this.is_static_content && this.children.every(node => node.type === 'Text' || node.type === 'MustacheTag');
}
get can_optimise_to_html_string() {
const can_use_textcontent = this.can_use_textcontent;
const is_template_with_text_content = this.name === 'template' && can_use_textcontent;
return !is_template_with_text_content && !this.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.children.length > 0;
}
hash() {
return `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
}
}
const regex_starts_with_vowel = /^[aeiou]/;

@ -15,6 +15,8 @@ export default class Head extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
if (info.attributes.length) {
component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
return;

@ -18,6 +18,8 @@ export default class IfBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.scope = scope.child();
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, this.scope, info.expression);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));

@ -28,6 +28,9 @@ export default class InlineComponent extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
const name = info.name.split('.')[0]; // accommodate namespaces
component.warn_if_undefined(name, info, scope);

@ -13,6 +13,8 @@ export default class KeyBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression);

@ -2,4 +2,9 @@ import Tag from './shared/Tag';
export default class RawMustacheTag extends Tag {
type: 'RawMustacheTag';
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
}
}

@ -60,5 +60,8 @@ export default class Slot extends Element {
}
component.slots.set(this.slot_name, this);
this.cannot_use_innerhtml();
this.not_static_content();
}
}

@ -18,6 +18,7 @@ const elements_without_text = new Set([
]);
const regex_ends_with_svg = /svg$/;
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class Text extends Node {
type: 'Text';
@ -63,4 +64,11 @@ export default class Text extends Node {
return false;
}
use_space(): boolean {
if (this.component.compile_options.preserveWhitespace) return false;
if (regex_non_whitespace_characters.test(this.data)) return false;
return !this.within_pre();
}
}

@ -197,6 +197,15 @@ export default class Expression {
});
}
dynamic_contextual_dependencies() {
return Array.from(this.contextual_dependencies).filter(name => {
return Array.from(this.template_scope.dependencies_for_name.get(name)).some(variable_name => {
const variable = this.component.var_lookup.get(variable_name);
return is_dynamic(variable);
});
});
}
// TODO move this into a render-dom wrapper?
manipulate(block?: Block, ctx?: string | void) {
// TODO ideally we wouldn't end up calling this method

@ -15,6 +15,7 @@ export default class Node {
next?: INode;
can_use_innerhtml: boolean;
is_static_content: boolean;
var: string;
attributes: Attribute[];
@ -33,6 +34,9 @@ export default class Node {
value: parent
}
});
this.can_use_innerhtml = true;
this.is_static_content = true;
}
cannot_use_innerhtml() {
@ -42,6 +46,11 @@ export default class Node {
}
}
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
find_nearest(selector: RegExp) {
if (selector.test(this.type)) return this;
if (this.parent) return this.parent.find_nearest(selector);

@ -8,6 +8,9 @@ export default class Tag extends Node {
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
component.tags.push(this);
this.cannot_use_innerhtml();
this.expression = new Expression(component, this, scope, info.expression);
this.should_cache = (
@ -15,4 +18,12 @@ export default class Tag extends Node {
(this.expression.dependencies.size && scope.names.has(info.expression.name))
);
}
is_dependencies_static() {
return this.expression.dynamic_contextual_dependencies().length === 0 && this.expression.dynamic_dependencies().length === 0;
}
check_if_content_dynamic() {
if (!this.is_dependencies_static()) {
this.not_static_content();
}
}
}

@ -143,9 +143,6 @@ export default class AwaitBlockWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
block.add_dependencies(this.node.expression.dependencies);
let is_dynamic = false;

@ -38,4 +38,10 @@ export default class CommentWrapper extends Wrapper {
parent_node
);
}
text() {
if (!this.renderer.options.preserveComments) return '';
return `<!--${this.node.data}-->`;
}
}

@ -80,8 +80,6 @@ export default class EachBlockWrapper extends Wrapper {
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
const { dependencies } = node.expression;
block.add_dependencies(dependencies);

@ -36,9 +36,6 @@ export class BaseAttributeWrapper {
this.parent = parent;
if (node.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
block.add_dependencies(node.dependencies);
}
}

@ -29,6 +29,7 @@ import is_dynamic from '../shared/is_dynamic';
import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable';
import create_debugging_comment from '../shared/create_debugging_comment';
import { push_array } from '../../../../utils/push_array';
import CommentWrapper from '../Comment';
interface BindingGroup {
events: string[];
@ -288,24 +289,6 @@ export default class ElementWrapper extends Wrapper {
}
});
if (this.parent) {
if (node.actions.length > 0 ||
node.animation ||
node.bindings.length > 0 ||
node.classes.length > 0 ||
node.intro || node.outro ||
node.handlers.length > 0 ||
node.styles.length > 0 ||
this.node.name === 'option' ||
node.tag_expr.dynamic_dependencies().length ||
node.is_dynamic_element ||
renderer.options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);
this.element_data_name = block.get_unique_name(`${this.var.name}_data`);
@ -445,6 +428,7 @@ export default class ElementWrapper extends Wrapper {
render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
const { renderer } = this;
const hydratable = renderer.options.hydratable;
if (this.node.name === 'noscript') return;
@ -458,13 +442,15 @@ export default class ElementWrapper extends Wrapper {
b`${node} = ${render_statement};`
);
if (renderer.options.hydratable) {
const { can_use_textcontent, can_optimise_to_html_string } = this.node;
if (hydratable) {
if (parent_nodes) {
block.chunks.claim.push(b`
${node} = ${this.get_claim_statement(block, parent_nodes)};
${node} = ${this.get_claim_statement(block, parent_nodes, can_optimise_to_html_string)};
`);
if (!this.void && this.node.children.length > 0) {
if (!can_optimise_to_html_string && !this.void && this.node.children.length > 0) {
block.chunks.claim.push(b`
var ${nodes} = ${children};
`);
@ -502,15 +488,19 @@ export default class ElementWrapper extends Wrapper {
// insert static children with textContent or innerHTML
// skip textcontent for <template>. append nodes to TemplateElement.content instead
const can_use_textcontent = this.can_use_textcontent();
const is_template = this.node.name === 'template';
const is_template_with_text_content = is_template && can_use_textcontent;
if (!is_template_with_text_content && !this.node.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.fragment.nodes.length > 0) {
if (can_optimise_to_html_string) {
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
block.chunks.create.push(
b`${node}.textContent = ${string_literal((this.fragment.nodes[0] as TextWrapper).data)};`
);
let text: Node = string_literal((this.fragment.nodes[0] as TextWrapper).data);
if (hydratable) {
const variable = block.get_unique_name('textContent');
block.add_variable(variable, text);
text = variable;
}
block.chunks.create.push(b`${node}.textContent = ${text};`);
if (hydratable) {
block.chunks.claim.push(b`if (@get_svelte_dataset(${node}) !== "${this.node.hash()}") ${node}.textContent = ${text};`);
}
} else {
const state = {
quasi: {
@ -519,25 +509,33 @@ export default class ElementWrapper extends Wrapper {
}
};
const literal = {
let literal: Node = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
};
const can_use_raw_text = !this.can_use_innerhtml && can_use_textcontent;
to_html((this.fragment.nodes as unknown as Array<ElementWrapper | TextWrapper>), block, literal, state, can_use_raw_text);
literal.quasis.push(state.quasi);
const can_use_raw_text = !this.node.can_use_innerhtml && can_use_textcontent;
to_html((this.fragment.nodes as unknown as Array<ElementWrapper | CommentWrapper | TextWrapper>), block, literal, state, can_use_raw_text);
literal.quasis.push(state.quasi as any);
block.chunks.create.push(
b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};`
);
if (hydratable) {
const variable = block.get_unique_name('textContent');
block.add_variable(variable, literal);
literal = variable;
}
const property = this.node.can_use_innerhtml ? 'innerHTML' : 'textContent';
block.chunks.create.push(b`${node}.${property} = ${literal};`);
if (hydratable) {
block.chunks.claim.push(b`if (@get_svelte_dataset(${node}) !== "${this.node.hash()}") ${node}.${property} = ${literal};`);
}
}
} else {
this.fragment.nodes.forEach((child: Wrapper) => {
child.render(
block,
is_template ? x`${node}.content` : node,
this.node.name === 'template' ? x`${node}.content` : node,
nodes,
{ element_data_name: this.element_data_name }
);
@ -566,7 +564,7 @@ export default class ElementWrapper extends Wrapper {
this.add_styles(block);
this.add_manual_style_scoping(block);
if (nodes && this.renderer.options.hydratable && !this.void) {
if (nodes && hydratable && !this.void && !can_optimise_to_html_string) {
block.chunks.claim.push(
b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`
);
@ -582,10 +580,6 @@ export default class ElementWrapper extends Wrapper {
block.renderer.dirty(this.node.tag_expr.dynamic_dependencies());
}
can_use_textcontent() {
return this.is_static_content && this.fragment.nodes.every(node => node.node.type === 'Text' || node.node.type === 'MustacheTag');
}
get_render_statement(block: Block) {
const { name, namespace, tag_expr } = this.node;
const reference = tag_expr.manipulate(block);
@ -606,7 +600,7 @@ export default class ElementWrapper extends Wrapper {
return x`@element(${reference})`;
}
get_claim_statement(block: Block, nodes: Identifier) {
get_claim_statement(block: Block, nodes: Identifier, to_optimise_hydration: boolean) {
const attributes = this.attributes
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);
@ -624,6 +618,10 @@ export default class ElementWrapper extends Wrapper {
reference = x`(${this.node.tag_expr.manipulate(block)} || 'null').toUpperCase()`;
}
if (to_optimise_hydration) {
attributes.push(p`["data-svelte-h"]: true`);
}
if (this.node.namespace === namespaces.svg) {
return x`@claim_svg_element(${nodes}, ${reference}, { ${attributes} })`;
} else {
@ -1288,13 +1286,19 @@ export default class ElementWrapper extends Wrapper {
const regex_backticks = /`/g;
const regex_dollar_signs = /\$/g;
function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) {
function to_html(wrappers: Array<CommentWrapper | ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) {
wrappers.forEach(wrapper => {
if (wrapper instanceof TextWrapper) {
if (wrapper instanceof CommentWrapper) {
state.quasi.value.raw += wrapper.text();
} else if (wrapper instanceof TextWrapper) {
// Don't add the <pre>/<textarea> newline logic here because pre/textarea.innerHTML
// would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't
if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' ';
if (wrapper.use_space()) {
// use space instead of the text content
state.quasi.value.raw += ' ';
return;
}
const parent = wrapper.node.parent as Element;

@ -20,8 +20,6 @@ export default class HeadWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.can_use_innerhtml = false;
this.fragment = new FragmentWrapper(
renderer,
block,

@ -105,9 +105,6 @@ export default class IfBlockWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.branches = [];
const blocks: Block[] = [];

@ -44,9 +44,6 @@ export default class InlineComponentWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
if (this.node.expression) {
block.add_dependencies(this.node.expression.dependencies);
}

@ -24,9 +24,6 @@ export default class KeyBlockWrapper extends Wrapper {
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.dependencies = node.expression.dynamic_dependencies();
if (this.dependencies.length) {

@ -20,8 +20,6 @@ export default class RawMustacheTagWrapper extends Tag {
node: MustacheTag | RawMustacheTag
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
}
render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) {

@ -30,8 +30,6 @@ export default class SlotWrapper extends Wrapper {
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
if (this.node.children.length) {
this.fallback = block.child({

@ -5,11 +5,9 @@ import Wrapper from './shared/Wrapper';
import { x } from 'code-red';
import { Identifier } from 'estree';
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class TextWrapper extends Wrapper {
node: Text;
data: string;
_data: string;
skip: boolean;
var: Identifier;
@ -23,15 +21,22 @@ export default class TextWrapper extends Wrapper {
super(renderer, block, parent, node);
this.skip = this.node.should_skip();
this.data = data;
this._data = data;
this.var = (this.skip ? null : x`t`) as unknown as Identifier;
}
use_space() {
if (this.renderer.component.component_options.preserveWhitespace) return false;
if (regex_non_whitespace_characters.test(this.data)) return false;
return this.node.use_space();
}
return !this.node.within_pre();
set data(value: string) {
// when updating `this.data` during optimisation
// propagate the changes over to the underlying node
// so that the node.use_space reflects on the latest `data` value
this.node.data = this._data = value;
}
get data() {
return this._data;
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {

@ -12,18 +12,9 @@ export default class Tag extends Wrapper {
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
if (!this.is_dependencies_static()) {
this.not_static_content();
}
block.add_dependencies(node.expression.dependencies);
}
is_dependencies_static() {
return this.node.expression.contextual_dependencies.size === 0 && this.node.expression.dynamic_dependencies().length === 0;
}
rename_this_method(
block: Block,
update: ((value: Node) => (Node | Node[]))

@ -13,8 +13,6 @@ export default class Wrapper {
next: Wrapper | null;
var: Identifier;
can_use_innerhtml: boolean;
is_static_content: boolean;
constructor(
renderer: Renderer,
@ -35,22 +33,9 @@ export default class Wrapper {
}
});
this.can_use_innerhtml = !renderer.options.hydratable;
this.is_static_content = !renderer.options.hydratable;
block.wrappers.push(this);
}
cannot_use_innerhtml() {
this.can_use_innerhtml = false;
if (this.parent) this.parent.cannot_use_innerhtml();
}
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
get_or_create_anchor(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
// TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first

@ -48,6 +48,7 @@ const handlers: Record<string, Handler> = {
export interface RenderOptions extends CompileOptions{
locate: (c: number) => { line: number; column: number };
head_id?: string;
has_added_svelte_hash?: boolean;
}
export default class Renderer {

@ -158,6 +158,13 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
}
});
if (options.hydratable) {
if (node.can_optimise_to_html_string && !options.has_added_svelte_hash) {
renderer.add_string(` data-svelte-h="${node.hash()}"`);
options = { ...options, has_added_svelte_hash: true };
}
}
renderer.add_string('>');
if (node_contents !== undefined) {

@ -5,7 +5,9 @@ import Element from '../../nodes/Element';
export default function(node: Text, renderer: Renderer, _options: RenderOptions) {
let text = node.data;
if (
if (node.use_space()) {
text = ' ';
} else if (
!node.parent ||
node.parent.type !== 'Element' ||
((node.parent as Element).name !== 'script' && (node.parent as Element).name !== 'style')

@ -39,6 +39,7 @@ export default function remove_whitespace_children(children: INode[], next?: INo
continue;
}
child.data = data;
nodes.unshift(child);
link(last_child, last_child = child);
} else {

@ -1,4 +1,5 @@
export function string_literal(data: string) {
import { Literal } from 'estree';
export function string_literal(data: string): Literal {
return {
type: 'Literal',
value: data

@ -362,6 +362,10 @@ export function xlink_attr(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
export function get_svelte_dataset(node: HTMLElement) {
return node.dataset.svelteH;
}
export function get_binding_group_value(group, __value, checked) {
const value = new Set();
for (let i = 0; i < group.length; i += 1) {

@ -4,7 +4,7 @@ import glob from 'tiny-glob/sync';
import * as path from 'path';
import * as fs from 'fs';
import * as colors from 'kleur';
export const assert = (assert$1 as unknown) as typeof assert$1 & { htmlEqual: (actual, expected, message?) => void, htmlEqualWithComments: (actual, expected, message?) => void };
export const assert = (assert$1 as unknown) as typeof assert$1 & { htmlEqual: (actual: string, expected: string, message?: string) => void, htmlEqualWithOptions: (actual: string, expected: string, options: { preserveComments?: boolean, withoutNormalizeHtml?: boolean }, message?: string) => void };
// for coverage purposes, we need to test source files,
// but for sanity purposes, we need to test dist files
@ -140,11 +140,12 @@ function cleanChildren(node) {
}
}
export function normalizeHtml(window, html, preserveComments = false) {
export function normalizeHtml(window, html, { removeDataSvelte = false, preserveComments = false }: { removeDataSvelte?: boolean, preserveComments?: boolean }) {
try {
const node = window.document.createElement('div');
node.innerHTML = html
.replace(/(<!--.*?-->)/g, preserveComments ? '$1' : '')
.replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1')
.replace(/>[ \t\n\r\f]+</g, '><')
.trim();
cleanChildren(node);
@ -154,22 +155,30 @@ export function normalizeHtml(window, html, preserveComments = false) {
}
}
export function setupHtmlEqual() {
export function normalizeNewline(html: string) {
return html.replace(/\r\n/g, '\n');
}
export function setupHtmlEqual(options: { removeDataSvelte?: boolean } = {}) {
const window = env();
// eslint-disable-next-line no-import-assign
assert.htmlEqual = (actual, expected, message) => {
assert.deepEqual(
normalizeHtml(window, actual),
normalizeHtml(window, expected),
normalizeHtml(window, actual, options),
normalizeHtml(window, expected, options),
message
);
};
// eslint-disable-next-line no-import-assign
assert.htmlEqualWithComments = (actual, expected, message) => {
assert.htmlEqualWithOptions = (actual: string, expected: string, { preserveComments, withoutNormalizeHtml }: { preserveComments?: boolean, withoutNormalizeHtml?: boolean }, message?: string) => {
assert.deepEqual(
normalizeHtml(window, actual, true),
normalizeHtml(window, expected, true),
withoutNormalizeHtml
? normalizeNewline(actual).replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
: normalizeHtml(window, actual, { ...options, preserveComments }),
withoutNormalizeHtml
? normalizeNewline(expected).replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
: normalizeHtml(window, expected, { ...options, preserveComments }),
message
);
};

@ -106,6 +106,13 @@ describe('hydration', () => {
}
}
if (config.snapshot) {
const snapshot_after = config.snapshot(target);
for (const s in snapshot_after) {
assert.equal(snapshot_after[s], snapshot[s], `Expected snapshot key "${s}" to have same value/reference`);
}
}
if (config.test) {
config.test(assert, target, snapshot, component, window);
} else {

@ -1 +1 @@
<h1>Hello world!</h1>
<h1 data-svelte-h="svelte-1vv3a6r">Hello world!</h1>

@ -1 +1 @@
<h1>Hello world!</h1>
<h1 data-svelte-h="svelte-1vv3a6r">Hello world!</h1>

@ -6,12 +6,5 @@ export default {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -10,13 +10,8 @@ export default {
};
},
async test(assert, target, snapshot, component, window) {
async test(assert, target, _, component, window) {
const input = target.querySelector('input');
const p = target.querySelector('p');
assert.equal(input, snapshot.input);
assert.equal(p, snapshot.p);
input.value = 'everybody';
await input.dispatchEvent(new window.Event('input'));

@ -1 +1,3 @@
<div><!-- test1 --><!-- test2 --></div>
p
<div><!-- test1 --><!-- test2 --></div>

@ -1 +1,3 @@
<div><!-- test1 --></div>
p
<div><!-- test1 --><!-- test2 --></div>

@ -3,18 +3,8 @@ export default {
preserveComments:true
},
snapshot(target) {
const div = target.querySelector('div');
return {
div,
comment: div.childNodes[0]
div: target.querySelectorAll('div')[1]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
assert.equal(div.childNodes[0], snapshot.comment);
assert.equal(div.childNodes[1].nodeType, 8);
}
};

@ -1 +1,3 @@
<div><!-- test1 --><!-- test2 --></div>
{"p"}
<div><!-- test1 --><!-- test2 --></div>

@ -0,0 +1,8 @@
<div data-svelte-h="xxx">hello</div>
<div data-svelte-h="xxx"><div>bye</div></div>
<div data-svelte-h="xxx">
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div data-svelte-h="xxx">hello</div>
<div data-svelte-h="xxx"><div data-svelte-h="yyy">bye</div></div>
<div data-svelte-h="xxx">
<div data-svelte-h="yyy">aaa</div>
<div data-svelte-h="zzz">bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
export default {
snapshot(target) {
return {
main: target.querySelector('main'),
p: target.querySelector('p')
};
}
};

@ -1,3 +1,3 @@
<div>
<p>nested</p>
<p data-svelte-h="svelte-1x3hbnh">nested</p>
</div>

@ -1,3 +1,3 @@
<div>
<p>nested</p>
<p data-svelte-h="svelte-1x3hbnh">nested</p>
</div>

@ -8,14 +8,5 @@ export default {
p,
text: p.childNodes[0]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
const p = target.querySelector('p');
assert.equal(div, snapshot.div);
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
}
};

@ -1 +1 @@
<p>nested</p>
<p data-svelte-h="svelte-1x3hbnh">nested</p>

@ -1 +1 @@
<p>nested</p>
<p data-svelte-h="svelte-1x3hbnh">nested</p>

@ -6,12 +6,5 @@ export default {
p,
text: p.childNodes[0]
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
}
};

@ -10,12 +10,5 @@ export default {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -9,13 +9,5 @@ export default {
nullText,
undefinedText
};
},
test(assert, target, snapshot) {
const nullText = target.querySelectorAll('p')[0].textContent;
const undefinedText = target.querySelectorAll('p')[1].textContent;
assert.equal(nullText, snapshot.nullText);
assert.equal(undefinedText, snapshot.undefinedText);
}
};

@ -10,12 +10,5 @@ export default {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -15,17 +15,9 @@ export default {
return {
ul,
lis
lis0: lis[0],
lis1: lis[1],
lis2: lis[2]
};
},
test(assert, target, snapshot) {
const ul = target.querySelector('ul');
const lis = ul.querySelectorAll('li');
assert.equal(ul, snapshot.ul);
assert.equal(lis[0], snapshot.lis[0]);
assert.equal(lis[1], snapshot.lis[1]);
assert.equal(lis[2], snapshot.lis[2]);
}
};

@ -13,17 +13,9 @@ export default {
return {
ul,
lis
lis0: lis[0],
lis1: lis[1],
lis2: lis[2]
};
},
test(assert, target, snapshot) {
const ul = target.querySelector('ul');
const lis = ul.querySelectorAll('li');
assert.equal(ul, snapshot.ul);
assert.equal(lis[0], snapshot.lis[0]);
assert.equal(lis[1], snapshot.lis[1]);
assert.equal(lis[2], snapshot.lis[2]);
}
};

@ -9,11 +9,5 @@ export default {
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -9,11 +9,5 @@ export default {
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -9,11 +9,5 @@ export default {
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -5,11 +5,5 @@ export default {
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -7,13 +7,5 @@ export default {
span: p.querySelector('span'),
code: p.querySelector('code')
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
assert.equal(p.querySelector('span'), snapshot.span);
assert.equal(p.querySelector('code'), snapshot.code);
}
};

@ -1,3 +1,3 @@
<div>
<div data-svelte-h="svelte-1aqf5aj">
<p>nested</p>
</div>

@ -1,3 +1,3 @@
<div>
<div data-svelte-h="svelte-1aqf5aj">
<p>nested</p>
</div>

@ -6,12 +6,5 @@ export default {
div,
p: div.querySelector('p')
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
assert.equal(div.querySelector('p'), snapshot.p);
}
};

@ -7,10 +7,8 @@ export default {
};
},
test(assert, target, snapshot, component) {
test(assert, target, _, component) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(component.h1, h1);
}
};

@ -11,10 +11,8 @@ export default {
};
},
async test(assert, target, snapshot, component, window) {
async test(assert, target, _, component, window) {
const button = target.querySelector('button');
assert.equal(button, snapshot.button);
await button.dispatchEvent(new window.MouseEvent('click'));
assert.ok(component.clicked);

@ -7,13 +7,5 @@ export default {
text: p.childNodes[0],
span: p.querySelector('span')
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
assert.equal(p.querySelector('span'), snapshot.span);
}
};

@ -13,14 +13,5 @@ export default {
p0: ps[0],
p1: ps[1]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
const ps = target.querySelectorAll('p');
assert.equal(div, snapshot.div);
assert.equal(ps[0], snapshot.p0);
assert.equal(ps[1], snapshot.p1);
}
};

@ -9,11 +9,5 @@ export default {
return {
p
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
}
};

@ -12,10 +12,7 @@ export default {
};
},
test(assert, target, snapshot, component) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
test(assert, target, _, component) {
component.foo = false;
component.bar = true;

@ -9,11 +9,5 @@ export default {
return {
p
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
}
};

@ -14,14 +14,5 @@ export default {
p1: ps[1],
text1: ps[1].firstChild
};
},
test(assert, target, snapshot) {
const ps = target.querySelectorAll('p');
assert.equal(ps[0], snapshot.p0);
assert.equal(ps[0].firstChild, snapshot.text0);
assert.equal(ps[1], snapshot.p1);
assert.equal(ps[1].firstChild, snapshot.text1);
}
};

@ -9,17 +9,7 @@ function foo() {
}
const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => {
return `
<div class="class1 class2" style="color:red;">-</div>
<div${add_attribute("class", const1, 0)}>-</div>
<div${add_attribute("class", const1, 0)}>-</div>
<div class="${"class1 " + escape('class2', true)}">-</div>
<div class="${"class1 " + escape(const2, true)}">-</div>
<div class="${"class1 " + escape(const2, true)}"${add_attribute("style", foo(), 0)}>-</div>`;
return ` <div class="class1 class2" style="color:red;">-</div> <div${add_attribute("class", const1, 0)}>-</div> <div${add_attribute("class", const1, 0)}>-</div> <div class="${"class1 " + escape('class2', true)}">-</div> <div class="${"class1 " + escape(const2, true)}">-</div> <div class="${"class1 " + escape(const2, true)}"${add_attribute("style", foo(), 0)}>-</div>`;
});
export default Component;

@ -8,11 +8,8 @@ const Component = create_ssr_component(($$result, $$props, $$bindings, slots) =>
if ($$props.foo === void 0 && $$bindings.foo && foo !== void 0) $$bindings.foo(foo);
return `${each(things, thing => {
return `<span>${escape(thing.name)}</span>
${debug(null, 7, 2, { foo })}`;
})}
<p>foo: ${escape(foo)}</p>`;
return `<span>${escape(thing.name)}</span> ${debug(null, 7, 2, { foo })}`;
})} <p>foo: ${escape(foo)}</p>`;
});
export default Component;
export default Component;

@ -27,7 +27,6 @@ function get_each_context(ctx, list, i) {
function create_each_block(ctx) {
let div;
let strong;
let t0;
let t1;
let span;
let t2_value = /*comment*/ ctx[4].author + "";
@ -44,7 +43,7 @@ function create_each_block(ctx) {
c() {
div = element("div");
strong = element("strong");
t0 = text(/*i*/ ctx[6]);
strong.textContent = `${/*i*/ ctx[6]}`;
t1 = space();
span = element("span");
t2 = text(t2_value);
@ -60,7 +59,6 @@ function create_each_block(ctx) {
m(target, anchor) {
insert(target, div, anchor);
append(div, strong);
append(strong, t0);
append(div, t1);
append(div, span);
append(span, t2);

@ -2,9 +2,7 @@
import { create_ssr_component } from "svelte/internal";
const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => {
return `<div>content</div>
<!-- comment -->
<div>more content</div>`;
return `<div>content</div> <!-- comment --> <div>more content</div>`;
});
export default Component;
export default Component;

@ -45,7 +45,7 @@ describe('runtime', () => {
return module._compile(code, filename);
};
return setupHtmlEqual();
return setupHtmlEqual({ removeDataSvelte: true });
});
after(() => process.removeListener('unhandledRejection', unhandledRejection_handler));
@ -154,15 +154,23 @@ describe('runtime', () => {
window.SvelteComponent = SvelteComponent;
const target = window.document.querySelector('main');
let snapshot = undefined;
if (hydrate && from_ssr_html) {
// ssr into target
compileOptions.generate = 'ssr';
cleanRequireCache();
if (config.before_test) config.before_test();
const SsrSvelteComponent = require(`./samples/${dir}/main.svelte`).default;
const { html } = SsrSvelteComponent.render(config.props);
target.innerHTML = html;
if (config.snapshot) {
snapshot = config.snapshot(target);
}
delete compileOptions.generate;
if (config.after_test) config.after_test();
} else {
target.innerHTML = '';
}
@ -199,7 +207,9 @@ describe('runtime', () => {
}
if (config.html) {
assert.htmlEqual(target.innerHTML, config.html);
assert.htmlEqualWithOptions(target.innerHTML, config.html, {
withoutNormalizeHtml: config.withoutNormalizeHtml
});
}
if (config.test) {
@ -208,6 +218,7 @@ describe('runtime', () => {
component,
mod,
target,
snapshot,
window,
raf,
compileOptions

@ -5,7 +5,9 @@ export default {
<h1>tag is h1.</h1>
`,
props: {
logs
pushLogs(log) {
logs.push(log);
}
},
after_test() {
logs = [];

@ -1,12 +1,12 @@
<script>
export let logs = [];
export let pushLogs;
export let tag = "h1";
export let opt = "opt1";
function foo(node, {tag, opt}) {
logs.push(`create: ${tag},${opt}`);
pushLogs(`create: ${tag},${opt}`);
return {
update: ({tag, opt}) => logs.push(`update: ${tag},${opt}`),
destroy: () => logs.push('destroy'),
update: ({tag, opt}) => pushLogs(`update: ${tag},${opt}`),
destroy: () => pushLogs('destroy'),
};
}
</script>

@ -8,9 +8,9 @@ export default {
async test({ assert, target }) {
const firstSpanList = target.children[0];
assert.equal(firstSpanList.innerHTML, expected);
assert.htmlEqualWithOptions(firstSpanList.innerHTML, expected, { withoutNormalizeHtml: true });
const secondSpanList = target.children[1];
assert.equal(secondSpanList.innerHTML, expected);
assert.htmlEqualWithOptions(secondSpanList.innerHTML, expected, { withoutNormalizeHtml: true });
}
};

@ -1,19 +1,14 @@
export default {
test({ assert, target }) {
// Test for <pre> tag
const elementPre = target.querySelector('#pre');
// Test for non <pre> tag
const elementDiv = target.querySelector('#div');
// Test for <pre> tag in non <pre> tag
const elementDivWithPre = target.querySelector('#div-with-pre');
// Test for <pre> tag with leading newline
const elementPreWithLeadingNewline = target.querySelector('#pre-with-leading-newline');
const elementPreWithoutLeadingNewline = target.querySelector('#pre-without-leading-newline');
const elementPreWithMultipleLeadingNewline = target.querySelector('#pre-with-multiple-leading-newlines');
withoutNormalizeHtml: true,
html: get_html(false),
ssrHtml: get_html(true)
};
assert.equal(
elementPre.innerHTML,
` A
function get_html(ssr) {
// ssr rendered HTML has an extra newline prefixed within `<pre>` tag,
// if the <pre> tag starts with `\n`
// because when browser parses the SSR rendered HTML, it will ignore the 1st '\n' character
return `<pre id="pre"> A
B
<span>
C
@ -21,20 +16,12 @@ export default {
</span>
E
F
`
);
assert.equal(
elementDiv.innerHTML,
`A
</pre> <div id="div">A
B
<span>C
D</span>
E
F`
);
assert.equal(
elementDivWithPre.innerHTML,
`<pre> A
F</div> <div id="div-with-pre"><pre> A
B
<span>
C
@ -42,14 +29,9 @@ export default {
</span>
E
F
</pre>`
);
assert.equal(elementPreWithLeadingNewline.children[0].innerHTML, 'leading newline');
assert.equal(elementPreWithLeadingNewline.children[1].innerHTML, ' leading newline and spaces');
assert.equal(elementPreWithLeadingNewline.children[2].innerHTML, '\nleading newlines');
assert.equal(elementPreWithoutLeadingNewline.children[0].innerHTML, 'without spaces');
assert.equal(elementPreWithoutLeadingNewline.children[1].innerHTML, ' with spaces ');
assert.equal(elementPreWithoutLeadingNewline.children[2].innerHTML, ' \nnewline after leading space');
assert.equal(elementPreWithMultipleLeadingNewline.innerHTML, '\n\nmultiple leading newlines');
}
};
</pre></div> <div id="pre-with-leading-newline"><pre>leading newline</pre> <pre> leading newline and spaces</pre> <pre>${ssr ? '\n' : ''}
leading newlines</pre></div> <div id="pre-without-leading-newline"><pre>without spaces</pre> <pre> with spaces </pre> <pre>
newline after leading space</pre></div> <pre id="pre-with-multiple-leading-newlines">${ssr ? '\n' : ''}
multiple leading newlines</pre>`;
}

@ -1,18 +1,9 @@
export default {
compileOptions: {
preserveWhitespace: true
},
test({ assert, target }) {
// Test for <pre> tag
const elementPre = target.querySelector('#pre');
// Test for non <pre> tag
const elementDiv = target.querySelector('#div');
// Test for <pre> tag in non <pre> tag
const elementDivWithPre = target.querySelector('#div-with-pre');
},
assert.equal(
elementPre.innerHTML,
` A
html: `<pre id="pre"> A
B
<span>
C
@ -20,11 +11,9 @@ export default {
</span>
E
F
`
);
assert.equal(
elementDiv.innerHTML,
`
</pre>
<div id="div">
A
B
<span>
@ -33,11 +22,9 @@ export default {
</span>
E
F
`
);
assert.equal(
elementDivWithPre.innerHTML,
`
</div>
<div id="div-with-pre">
<pre> A
B
<span>
@ -47,7 +34,5 @@ export default {
E
F
</pre>
`
);
}
</div>`
};

@ -11,7 +11,7 @@ export default {
const p = target.querySelector('p');
component.raw = '<p>does not change</p>';
assert.equal(target.innerHTML, '<div><p>does not change</p></div>');
assert.htmlEqualWithOptions(target.innerHTML, '<div><p>does not change</p></div>', { withoutNormalizeHtml: true });
assert.strictEqual(target.querySelector('p'), p);
}
};

@ -1,4 +1,21 @@
export default {
withoutNormalizeHtml: true,
// Unable to test `html` with `<textarea>` content
// as the textarea#value will not show within `innerHtml`
ssrHtml: `<textarea id="textarea"> A
B
</textarea> <div id="div-with-textarea"><textarea> A
B
</textarea></div> <div id="textarea-with-leading-newline"><textarea>leading newline</textarea> <textarea> leading newline and spaces</textarea> <textarea>
leading newlines</textarea></div> <div id="textarea-without-leading-newline"><textarea>without spaces</textarea> <textarea> with spaces </textarea> <textarea>
newline after leading space</textarea></div> <textarea id="textarea-with-multiple-leading-newlines">
multiple leading newlines</textarea> <div id="div-with-textarea-with-multiple-leading-newlines"><textarea>
multiple leading newlines</textarea></div>`,
test({ assert, target }) {
// Test for <textarea> tag
const elementTextarea = target.querySelector('#textarea');

@ -83,13 +83,7 @@ describe('ssr', () => {
if (css.code) fs.writeFileSync(`${dir}/_actual.css`, css.code);
try {
if (config.withoutNormalizeHtml) {
assert.strictEqual(html.trim().replace(/\r\n/g, '\n'), expectedHtml.trim().replace(/\r\n/g, '\n'));
} else {
(compileOptions.preserveComments
? assert.htmlEqualWithComments
: assert.htmlEqual)(html, expectedHtml);
}
assert.htmlEqualWithOptions(html, expectedHtml, { preserveComments: compileOptions.preserveComments, withoutNormalizeHtml: config.withoutNormalizeHtml });
} catch (error) {
if (shouldUpdateExpected()) {
fs.writeFileSync(`${dir}/_expected.html`, html);
@ -117,11 +111,10 @@ describe('ssr', () => {
fs.writeFileSync(`${dir}/_actual-head.html`, head);
try {
(compileOptions.hydratable
? assert.htmlEqualWithComments
: assert.htmlEqual)(
assert.htmlEqualWithOptions(
head,
fs.readFileSync(`${dir}/_expected-head.html`, 'utf-8')
fs.readFileSync(`${dir}/_expected-head.html`, 'utf-8'),
{ preserveComments: compileOptions.hydratable }
);
} catch (error) {
if (shouldUpdateExpected()) {
@ -214,9 +207,15 @@ describe('ssr', () => {
});
if (config.ssrHtml) {
assert.htmlEqual(html, config.ssrHtml);
assert.htmlEqualWithOptions(html, config.ssrHtml, {
preserveComments: compileOptions.preserveComments,
withoutNormalizeHtml: config.withoutNormalizeHtml
});
} else if (config.html) {
assert.htmlEqual(html, config.html);
assert.htmlEqualWithOptions(html, config.html, {
preserveComments: compileOptions.preserveComments,
withoutNormalizeHtml: config.withoutNormalizeHtml
});
}
if (config.test_ssr) {

@ -1,3 +1 @@
<div>Just a dummy page.</div>
<div data-svelte-h="svelte-156ixv6">Just a dummy page.</div>

@ -1,2 +0,0 @@
[{main.svelte,_expected.html}]
trim_trailing_whitespace = unset

@ -1,3 +0,0 @@
export default {
withoutNormalizeHtml: true
};

@ -1,39 +0,0 @@
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
<div>A
B
<span>C
D
</span>
E
F
</div>
<div><pre> A
B
<span>
C
D
</span>
E
F
</pre></div>
<div id="pre-with-leading-newline"><pre>leading newline</pre>
<pre> leading newline and spaces</pre>
<pre>
leading newlines</pre></div>
<div id="pre-without-leading-newline"><pre>without spaces</pre>
<pre> with spaces </pre>
<pre>
newline after leading space</pre></div>

@ -1,51 +0,0 @@
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
</div>
<div id="pre-with-leading-newline">
<pre>
leading newline</pre>
<pre>
leading newline and spaces</pre>
<pre>
leading newlines</pre>
</div>
<div id="pre-without-leading-newline">
<pre>without spaces</pre>
<pre> with spaces </pre>
<pre>
newline after leading space</pre>
</div>

@ -1,6 +0,0 @@
export default {
withoutNormalizeHtml: true,
compileOptions: {
preserveWhitespace: true
}
};

@ -1,32 +0,0 @@
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
</div>

@ -1,34 +0,0 @@
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
</div>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save