diff --git a/deno.json b/deno.json index d0ee3b9..cf8e7f4 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@dbushell/hyperless", - "version": "0.23.0", + "version": "0.24.0", "exports": { ".": "./mod.ts" }, diff --git a/mod.ts b/mod.ts index 55de15b..237e413 100644 --- a/mod.ts +++ b/mod.ts @@ -1,12 +1,12 @@ -export {escape, unescape} from './src/html-utils.ts'; -export {inlineTags, opaqueTags, voidTags} from './src/html-tags.ts'; -export {AttributeMap} from './src/attribute-map.ts'; -export {parseAttributes} from './src/attribute-parser.ts'; -export {Node} from './src/html-node.ts'; +export { escape, unescape } from "./src/html-utils.ts"; +export { inlineTags, opaqueTags, voidTags } from "./src/html-tags.ts"; +export { AttributeMap } from "./src/attribute-map.ts"; +export { parseAttributes } from "./src/attribute-parser.ts"; +export { Node } from "./src/html-node.ts"; export { - type ParseOptions, getParseOptions, - parseHTML -} from './src/html-parser.ts'; -export {excerpt} from './src/excerpt.ts'; -export {stripTags} from './src/striptags.ts'; + parseHTML, + type ParseOptions, +} from "./src/html-parser.ts"; +export { excerpt } from "./src/excerpt.ts"; +export { stripTags } from "./src/striptags.ts"; diff --git a/package.json b/package.json index b909f44..d17c41a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dbushell/hyperless", - "version": "0.23.0", + "version": "0.24.0", "repository": { "type": "git", "url": "git+https://github.com/dbushell/hyperless.git" diff --git a/src/attribute-map.ts b/src/attribute-map.ts index 46171b2..c52b50b 100644 --- a/src/attribute-map.ts +++ b/src/attribute-map.ts @@ -1,4 +1,4 @@ -import {escape, unescape} from './html-utils.ts'; +import { escape, unescape } from "./html-utils.ts"; /** * HTML attributes map @@ -7,12 +7,12 @@ import {escape, unescape} from './html-utils.ts'; */ export class AttributeMap extends Map { constructor( - iterable?: Iterable | null | undefined + iterable?: Iterable | null | undefined, ) { super( iterable instanceof AttributeMap ? Array.from(iterable, ([k, v]) => [k, escape(v)]) - : iterable + : iterable, ); } override [Symbol.iterator](): MapIterator<[string, string]> { @@ -49,7 +49,7 @@ export class AttributeMap extends Map { } override forEach( callbackfn: (value: string, key: string, map: Map) => void, - thisArg?: unknown + thisArg?: unknown, ): void { super.forEach((value, key, map) => callbackfn.call(thisArg, unescape(value), key, map) @@ -58,8 +58,8 @@ export class AttributeMap extends Map { override toString(): string { const entries = Array.from(super.entries()); const attr = entries.map(([k, v]) => - v === '' ? k : v.indexOf('"') === -1 ? `${k}="${v}"` : `${k}='${v}'` + v === "" ? k : v.indexOf('"') === -1 ? `${k}="${v}"` : `${k}='${v}'` ); - return attr.join(' '); + return attr.join(" "); } } diff --git a/src/attribute-parser.ts b/src/attribute-parser.ts index fa0ec25..809f15d 100644 --- a/src/attribute-parser.ts +++ b/src/attribute-parser.ts @@ -2,22 +2,22 @@ * Parse HTML Attributes * {@link https://html.spec.whatwg.org/#before-attribute-name-state} */ -import {AttributeMap} from './attribute-map.ts'; +import { AttributeMap } from "./attribute-map.ts"; import { + ASCII_WHITESPACE, INVALID_ATTRIBUTE_NAME, INVALID_ATTRIBUTE_VALUE, - ASCII_WHITESPACE -} from './constants.ts'; +} from "./constants.ts"; /** Attribute parser state */ type ParseState = - | 'BEFORE_NAME' - | 'NAME' - | 'AFTER_NAME' - | 'BEFORE_VALUE' - | 'DOUBLE_QUOTED' - | 'SINGLE_QUOTED' - | 'UNQUOTED'; + | "BEFORE_NAME" + | "NAME" + | "AFTER_NAME" + | "BEFORE_VALUE" + | "DOUBLE_QUOTED" + | "SINGLE_QUOTED" + | "UNQUOTED"; /** * Return a map of HTML attributes @@ -27,88 +27,88 @@ type ParseState = export const parseAttributes = (attributes: string): AttributeMap => { const map: AttributeMap = new AttributeMap(); - let state: ParseState = 'BEFORE_NAME'; - let name = ''; - let value = ''; + let state: ParseState = "BEFORE_NAME"; + let name = ""; + let value = ""; for (let i = 0; i < attributes.length; i++) { const char = attributes[i]; // Handle case where closing HTML tag is included to avoid error - if (state === 'BEFORE_NAME' || state === 'NAME' || state === 'UNQUOTED') { - if (char === '/' || char === '>') { + if (state === "BEFORE_NAME" || state === "NAME" || state === "UNQUOTED") { + if (char === "/" || char === ">") { break; } } switch (state) { - case 'BEFORE_NAME': + case "BEFORE_NAME": if (ASCII_WHITESPACE.has(char)) { continue; } else if (INVALID_ATTRIBUTE_NAME.has(char)) { throw new Error(`Invalid attribute name at character ${i}`); } name = char; - value = ''; - state = 'NAME'; + value = ""; + state = "NAME"; continue; - case 'NAME': - if (char === '=') { - state = 'BEFORE_VALUE'; + case "NAME": + if (char === "=") { + state = "BEFORE_VALUE"; } else if (ASCII_WHITESPACE.has(char)) { - state = 'AFTER_NAME'; + state = "AFTER_NAME"; } else if (INVALID_ATTRIBUTE_NAME.has(char)) { throw new Error(`invalid name at ${i}`); } else { name += char; } continue; - case 'AFTER_NAME': - if (char === '=') { - state = 'BEFORE_VALUE'; + case "AFTER_NAME": + if (char === "=") { + state = "BEFORE_VALUE"; } else if (ASCII_WHITESPACE.has(char) === false) { // End of empty attribute - map.set(name, ''); + map.set(name, ""); // Rewind state to match new name i--; - state = 'BEFORE_NAME'; + state = "BEFORE_NAME"; } continue; - case 'BEFORE_VALUE': + case "BEFORE_VALUE": if (char === "'") { - state = 'SINGLE_QUOTED'; + state = "SINGLE_QUOTED"; } else if (char === '"') { - state = 'DOUBLE_QUOTED'; + state = "DOUBLE_QUOTED"; } else if (ASCII_WHITESPACE.has(char)) { continue; } else if (INVALID_ATTRIBUTE_VALUE.has(char)) { throw new Error(`Invalid unquoted attribute value at character ${i}`); } else { value += char; - state = 'UNQUOTED'; + state = "UNQUOTED"; } continue; - case 'DOUBLE_QUOTED': + case "DOUBLE_QUOTED": if (char === '"') { // End of double quoted attribute map.set(name, value); - state = 'BEFORE_NAME'; + state = "BEFORE_NAME"; } else { value += char; } continue; - case 'SINGLE_QUOTED': + case "SINGLE_QUOTED": if (char === "'") { // End of single quoted attribute map.set(name, value); - state = 'BEFORE_NAME'; + state = "BEFORE_NAME"; } else { value += char; } continue; - case 'UNQUOTED': + case "UNQUOTED": if (ASCII_WHITESPACE.has(char)) { // End of unquoted attribute map.set(name, value); - state = 'BEFORE_NAME'; + state = "BEFORE_NAME"; } else if (INVALID_ATTRIBUTE_VALUE.has(char)) { throw new Error(`Invalid unquoted attribute value at character ${i}`); } else { @@ -119,10 +119,10 @@ export const parseAttributes = (attributes: string): AttributeMap => { } // Handle end state switch (state) { - case 'NAME': - map.set(name, ''); + case "NAME": + map.set(name, ""); break; - case 'UNQUOTED': + case "UNQUOTED": map.set(name, value); break; } diff --git a/src/constants.ts b/src/constants.ts index e5d2258..429dd37 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,11 +3,11 @@ * {@link https://infra.spec.whatwg.org/#ascii-whitespace} */ export const ASCII_WHITESPACE = new Set([ - '\t', // U+0009 TAB - '\n', // U+000A LF (Line Feed) - '\f', // U+000C FF (Form Feed) - '\r', // U+000D CR (Carriage Return) - ' ' // U+0020 SPACE + "\t", // U+0009 TAB + "\n", // U+000A LF (Line Feed) + "\f", // U+000C FF (Form Feed) + "\r", // U+000D CR (Carriage Return) + " ", // U+0020 SPACE ]); /** @@ -36,10 +36,10 @@ export const INVALID_ATTRIBUTE_NAME = new Set(CONTROLS); [ '"', // U+0022 Quotation Mark "'", // U+0027 Apostrophe - '=', // U+003D Equals Sign - '>', // U+003C Less-Than Sign - '<', // U+003E Greater-Than Sign - '/' // U+002F Solidus + "=", // U+003D Equals Sign + ">", // U+003C Less-Than Sign + "<", // U+003E Greater-Than Sign + "/", // U+002F Solidus ].forEach((char) => INVALID_ATTRIBUTE_NAME.add(char)); /** @@ -48,5 +48,5 @@ export const INVALID_ATTRIBUTE_NAME = new Set(CONTROLS); */ export const INVALID_ATTRIBUTE_VALUE = new Set(INVALID_ATTRIBUTE_NAME); [ - '`' // U+0060 Grave Accent + "`", // U+0060 Grave Accent ].forEach((char) => INVALID_ATTRIBUTE_VALUE.add(char)); diff --git a/src/excerpt.ts b/src/excerpt.ts index c600a59..c609b88 100644 --- a/src/excerpt.ts +++ b/src/excerpt.ts @@ -1,4 +1,4 @@ -import {stripTags} from './striptags.ts'; +import { stripTags } from "./striptags.ts"; /** * Generate a text excerpt from HTML content. @@ -14,8 +14,8 @@ import {stripTags} from './striptags.ts'; export const excerpt = ( html: string, maxLength = 300, - trunateSuffix = '[…]', - endChars = ['.', '!', '?'] + trunateSuffix = "[…]", + endChars = [".", "!", "?"], ): string => { // Remove HTML const text = stripTags(html); @@ -31,11 +31,11 @@ export const excerpt = ( const excerpt = text.substring(0, offsets.at(-1)! + 1); // Ensure excerpt is not too short if (maxLength - excerpt.length < maxLength * 0.2) { - return (excerpt + ' ' + trunateSuffix).trim(); + return (excerpt + " " + trunateSuffix).trim(); } } // Fallback to using words - let excerpt = ''; + let excerpt = ""; const words = text.split(/\s+/); while (words.length) { // Concatenate words until maximum length is reached @@ -43,7 +43,7 @@ export const excerpt = ( if (excerpt.length + word.length > maxLength) { break; } - excerpt += word + ' '; + excerpt += word + " "; } return (excerpt + trunateSuffix).trim(); }; diff --git a/src/html-node.ts b/src/html-node.ts index 7c5adde..a91a8ed 100644 --- a/src/html-node.ts +++ b/src/html-node.ts @@ -1,20 +1,20 @@ -import {AttributeMap} from './attribute-map.ts'; -import {parseAttributes} from './attribute-parser.ts'; -import {anyTag} from './regexp.ts'; +import { AttributeMap } from "./attribute-map.ts"; +import { parseAttributes } from "./attribute-parser.ts"; +import { anyTag } from "./regexp.ts"; /** Node.type values */ export type NodeType = - | 'COMMENT' - | 'ELEMENT' - | 'INVISIBLE' - | 'OPAQUE' - | 'ROOT' - | 'STRAY' - | 'TEXT' - | 'VOID'; + | "COMMENT" + | "ELEMENT" + | "INVISIBLE" + | "OPAQUE" + | "ROOT" + | "STRAY" + | "TEXT" + | "VOID"; /** Node has open/close tags */ -const renderTypes = new Set(['ELEMENT', 'OPAQUE', 'VOID']); +const renderTypes = new Set(["ELEMENT", "OPAQUE", "VOID"]); /** * HTML node @@ -29,10 +29,10 @@ export class Node { constructor( parent: Node | null = null, - type: NodeType = 'TEXT', - raw = '', - tag = '', - attributes?: AttributeMap + type: NodeType = "TEXT", + raw = "", + tag = "", + attributes?: AttributeMap, ) { parent?.append(this); this.type = type; @@ -45,8 +45,8 @@ export class Node { get attributes(): AttributeMap { // Parse on first request for performance if (this.#attributes === undefined) { - const match = this.raw.match(new RegExp(anyTag.source, 's')); - this.#attributes = parseAttributes(match?.[2] ?? ''); + const match = this.raw.match(new RegExp(anyTag.source, "s")); + this.#attributes = parseAttributes(match?.[2] ?? ""); } return this.#attributes; } @@ -80,7 +80,7 @@ export class Node { let depth = 0; let parent = this.parent; while (parent) { - if (parent.type !== 'INVISIBLE') depth++; + if (parent.type !== "INVISIBLE") depth++; parent = parent.parent; } return depth; @@ -109,21 +109,21 @@ export class Node { /** Formatted opening tag with parsed attributes */ get tagOpen(): string { if (renderTypes.has(this.type) === false) { - return ''; + return ""; } - let out = '<' + this.tag; + let out = "<" + this.tag; const attr = this.attributes.toString(); - if (attr.length) out += ' ' + attr; - if (this.type === 'VOID') out += '/'; - return out + '>'; + if (attr.length) out += " " + attr; + if (this.type === "VOID") out += "/"; + return out + ">"; } /** Formatted closing tag */ get tagClose(): string { - if (renderTypes.has(this.type) && this.type !== 'VOID') { + if (renderTypes.has(this.type) && this.type !== "VOID") { return ``; } - return ''; + return ""; } /** Append one or more child nodes */ @@ -148,7 +148,7 @@ export class Node { this.type, this.raw, this.#tag, - this.#attributes + this.#attributes, ); if (deep) { for (const child of this.children) { @@ -231,13 +231,13 @@ export class Node { /** Render node to HTML string */ toString(): string { - if (this.type === 'COMMENT') return ''; - if (this.type === 'STRAY') return ''; + if (this.type === "COMMENT") return ""; + if (this.type === "STRAY") return ""; let out = this.tagOpen || this.raw; for (const node of this.#children) { out += node.toString(); } - out += this.tagClose ?? ''; + out += this.tagClose ?? ""; return out; } } diff --git a/src/html-parser.ts b/src/html-parser.ts index da4423f..60e8b12 100644 --- a/src/html-parser.ts +++ b/src/html-parser.ts @@ -1,15 +1,15 @@ -import {Node} from './html-node.ts'; -import {inlineTags, opaqueTags, voidTags} from './html-tags.ts'; -import {anyTag, comment, customName} from './regexp.ts'; +import { Node } from "./html-node.ts"; +import { inlineTags, opaqueTags, voidTags } from "./html-tags.ts"; +import { anyTag, comment, customName } from "./regexp.ts"; /** Regular expression to match HTML comment or tag */ const commentTag = new RegExp(`^${comment.source}|^${anyTag.source}`); /** List of valid
  • parents */ -const listTags = new Set(['ol', 'ul', 'menu']); +const listTags = new Set(["ol", "ul", "menu"]); /** HTML parser state */ -type ParseState = 'DATA' | 'RAWTEXT'; +type ParseState = "DATA" | "RAWTEXT"; /** `parseHTML` configuration */ export type ParseOptions = { @@ -20,10 +20,10 @@ export type ParseOptions = { }; export const getParseOptions = (): ParseOptions => ({ - rootTag: 'html', + rootTag: "html", inlineTags: new Set(inlineTags), opaqueTags: new Set(opaqueTags), - voidTags: new Set(voidTags) + voidTags: new Set(voidTags), }); const parseOptions: ParseOptions = getParseOptions(); @@ -35,18 +35,18 @@ const parseOptions: ParseOptions = getParseOptions(); */ export const parseHTML = (html: string, options = parseOptions): Node => { // Create current parent node - const root = new Node(null, 'ROOT', '', options.rootTag); + const root = new Node(null, "ROOT", "", options.rootTag); let parent = root; - let state: ParseState = 'DATA'; + let state: ParseState = "DATA"; // Start at first potential tag - let offset = html.indexOf('<'); + let offset = html.indexOf("<"); // Check if

    element was not closed const closeParagraph = (nextTag: string, previousTag: string): boolean => { if (customName.test(nextTag)) return false; return ( - nextTag !== 'p' && - previousTag === 'p' && + nextTag !== "p" && + previousTag === "p" && options.inlineTags.has(nextTag) === false ); }; @@ -57,10 +57,10 @@ export const parseHTML = (html: string, options = parseOptions): Node => { // Get the text skipped const text = html.substring(0, offset); // In RAWTEXT state text is concatenated otherwise append a text node - if (state === 'RAWTEXT') { + if (state === "RAWTEXT") { (parent.at(0) ?? parent).raw += text; } else { - parent.append(new Node(parent, 'TEXT', text)); + parent.append(new Node(parent, "TEXT", text)); } // Reset offset and data to parse html = html.substring(offset); @@ -70,7 +70,7 @@ export const parseHTML = (html: string, options = parseOptions): Node => { // Match next HTML comment or tag const tag = html.match(commentTag); if (tag === null) { - offset = html.substring(1).indexOf('<'); + offset = html.substring(1).indexOf("<"); // Add one to account for substring if found if (offset >= 0) offset += 1; // Next iteration ends if no tag was found @@ -79,62 +79,57 @@ export const parseHTML = (html: string, options = parseOptions): Node => { // Matched tag parts const tagText = tag[0]; - const tagRaw = tag[1] ?? ''; + const tagRaw = tag[1] ?? ""; const tagName = tagRaw.toLowerCase(); - if (state === 'RAWTEXT') { + if (state === "RAWTEXT") { // Switch state if closing tag matches - if (tagName === parent.tag && tagText.startsWith('`; parent = parent.parent; - state = 'DATA'; + state = "DATA"; } else { (parent.at(0) ?? parent).raw += tagText; } - } - // Append comment + } // Append comment else if (tagText.startsWith(` @@ -147,11 +147,11 @@ Deno.test('comments', () => {

    `; const root = parseHTML(html); - assertEquals(root.at(1)!.type, 'COMMENT'); - assertEquals(root.at(3)!.at(1)!.at(0)!.type, 'COMMENT'); + assertEquals(root.at(1)!.type, "COMMENT"); + assertEquals(root.at(3)!.at(1)!.at(0)!.type, "COMMENT"); }); -Deno.test('opaque state (svg)', () => { +Deno.test("opaque state (svg)", () => { const svg = ` @@ -160,27 +160,28 @@ Deno.test('opaque state (svg)', () => { const html = `${svg}

    After

    `; const root = parseHTML(html); assertEquals(root.size, 3); - assertEquals(root.at(1)!.type, 'OPAQUE'); + assertEquals(root.at(1)!.type, "OPAQUE"); assertEquals(root.at(1)!.size, 1); - assertEquals(root.at(1)!.at(0)!.type, 'TEXT'); + assertEquals(root.at(1)!.at(0)!.type, "TEXT"); assertEquals(root.at(1)!.toString(), svg); }); -Deno.test('script fake tag', () => { - const html = ` tag */

    after

    `; +Deno.test("script fake tag", () => { + const html = + ` tag */

    after

    `; const root = parseHTML(html); - assertEquals(root.at(1)!.raw, ' tag */'); + assertEquals(root.at(1)!.raw, " tag */"); }); -Deno.test('attributes', () => { +Deno.test("attributes", () => { const html = '
    test
    '; const root = parseHTML(html); - assertEquals(root.at(0)!.at(0)!.raw, 'test'); + assertEquals(root.at(0)!.at(0)!.raw, "test"); assertEquals(root.at(0)!.attributes.size, 11); }); -Deno.test('render', () => { +Deno.test("render", () => { const html = `

    Paragraph bold @@ -199,8 +200,8 @@ Deno.test('render', () => { assertEquals(root.toString(), render); }); -Deno.test('detach', () => { - const html = '

    1

    2

    3

    4

    5

    '; +Deno.test("detach", () => { + const html = "

    1

    2

    3

    4

    5

    "; const root = parseHTML(html); const c0 = root.at(0)!; const c1 = root.at(1)!; @@ -223,22 +224,22 @@ Deno.test('detach', () => { assertEquals(root.size, 1); }); -Deno.test('Node.replaceWith', () => { - const html = '

    1

    2

    3

    '; +Deno.test("Node.replaceWith", () => { + const html = "

    1

    2

    3

    "; const root = parseHTML(html); - const A = parseHTML('

    A

    '); - const B = parseHTML('

    B

    '); - const C = parseHTML('

    C

    '); + const A = parseHTML("

    A

    "); + const B = parseHTML("

    B

    "); + const C = parseHTML("

    C

    "); root.at(0)!.replace(A); root.at(1)!.replace(B); root.at(2)!.replace(C); - assertEquals(root.toString(), '

    A

    B

    C

    '); + assertEquals(root.toString(), "

    A

    B

    C

    "); assert(root.head === A); assert(root.tail === C); }); -Deno.test('Node.indexOf', () => { - const html = '

    1

    2

    3

    4

    5

    '; +Deno.test("Node.indexOf", () => { + const html = "

    1

    2

    3

    4

    5

    "; const root = parseHTML(html); const c0 = root.at(0)!; const c2 = root.at(2)!; @@ -249,40 +250,40 @@ Deno.test('Node.indexOf', () => { assertEquals(c0.indexOf(c4), -1); }); -Deno.test('Node.insertAt', () => { +Deno.test("Node.insertAt", () => { const root = new Node(); - const n1 = parseHTML('

    1

    '); - const n2 = parseHTML('

    2

    '); - const n3 = parseHTML('

    3

    '); + const n1 = parseHTML("

    1

    "); + const n2 = parseHTML("

    2

    "); + const n3 = parseHTML("

    3

    "); root.insertAt(n3, 0); root.insertAt(n2, 0); root.insertAt(n1, 0); - assertEquals(root.toString(), '

    1

    2

    3

    '); + assertEquals(root.toString(), "

    1

    2

    3

    "); }); -Deno.test('Node.after', () => { +Deno.test("Node.after", () => { const root = new Node(); - const n1 = parseHTML('

    1

    '); - const n2 = parseHTML('

    2

    '); - const n3 = parseHTML('

    3

    '); + const n1 = parseHTML("

    1

    "); + const n2 = parseHTML("

    2

    "); + const n3 = parseHTML("

    3

    "); root.append(n1, n2, n3); - assertEquals(root.toString(), '

    1

    2

    3

    '); + assertEquals(root.toString(), "

    1

    2

    3

    "); n2.after(n1); // 2 1 3 n1.after(n3); n2.after(n3); // 2 3 1 n1.after(n2); // 3 1 2 n2.after(n1); // 3 2 1 - assertEquals(root.toString(), '

    3

    2

    1

    '); + assertEquals(root.toString(), "

    3

    2

    1

    "); }); -Deno.test('Node.before', () => { +Deno.test("Node.before", () => { const root = new Node(); - const n1 = parseHTML('

    1

    '); - const n2 = parseHTML('

    2

    '); - const n3 = parseHTML('

    3

    '); + const n1 = parseHTML("

    1

    "); + const n2 = parseHTML("

    2

    "); + const n3 = parseHTML("

    3

    "); root.append(n1, n2, n3); - assertEquals(root.toString(), '

    1

    2

    3

    '); + assertEquals(root.toString(), "

    1

    2

    3

    "); n3.before(n1); // 2 1 3 n2.before(n3); // 3 2 1 - assertEquals(root.toString(), '

    3

    2

    1

    '); + assertEquals(root.toString(), "

    3

    2

    1

    "); }); diff --git a/test/striptags_test.ts b/test/striptags_test.ts index 8af8517..463ab0e 100644 --- a/test/striptags_test.ts +++ b/test/striptags_test.ts @@ -1,35 +1,36 @@ -import {stripTags} from '../src/striptags.ts'; -import {assertEquals} from 'jsr:@std/assert'; +import { stripTags } from "../src/striptags.ts"; +import { assertEquals } from "jsr:@std/assert"; -Deno.test('demo', () => { - const html = '

    Ceci n’est pas une paragraphe.

    '; - const expected = 'Ceci n’est pas une paragraphe. '; +Deno.test("demo", () => { + const html = "

    Ceci n’est pas une paragraphe.

    "; + const expected = "Ceci n’est pas une paragraphe. "; const text = stripTags(html); assertEquals(text, expected); }); -Deno.test('basic', () => { - const html = `

    This is HTML.

    +Deno.test("basic", () => { + const html = + `

    This is HTML.

    End of content.

    `; - const expected = 'This is HTML. \nEnd of content. '; + const expected = "This is HTML. \nEnd of content. "; const text = stripTags(html); assertEquals(text, expected); }); -Deno.test('quotes', () => { +Deno.test("quotes", () => { const html = `

    This is HTML.

    `; - const expected = '“This is HTML.” '; + const expected = "“This is HTML.” "; const text = stripTags(html); assertEquals(text, expected); }); -Deno.test('nested quotes', () => { +Deno.test("nested quotes", () => { const html = `

    Blockquote with inline quote text.

    `; - const expected = '“Blockquote with ‘inline quote’ text.” '; + const expected = "“Blockquote with ‘inline quote’ text.” "; const text = stripTags(html); assertEquals(text, expected); });