From 35214fd859abbb96d794bd10b23b00e13878587b Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 16 Jun 2026 02:40:48 -0400 Subject: [PATCH 1/5] When formatting, replace inline ASCII quotes with typographic quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref https://github.com/tc39/ecma262/pull/3861#pullrequestreview-4412001453 Ref #173 Ref #317 Note that this processing cannot be scoped to individual text nodes because e.g. `a "binary64 value"` should get rewritten into `a “binary64 value”` (but the same would not be true if the medial element were block-level rather than inline). ASCII quotes are not replaced inside of HTML comments, ``/`` elements, backtick spans (e.g., ``` `code` ```), asterisk spans (e.g., `*"string"*`), or after equals signs (as in HTML element attributes). --- src/formatter/ecmarkup.ts | 67 ++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/src/formatter/ecmarkup.ts b/src/formatter/ecmarkup.ts index 7622ee37..2ec9978c 100644 --- a/src/formatter/ecmarkup.ts +++ b/src/formatter/ecmarkup.ts @@ -15,6 +15,7 @@ const RAW_CONTENT_ELEMENTS = new Set([ 'script', 'style', 'code', + 'emu-val', ]); // https://html.spec.whatwg.org/multipage/syntax.html#void-elements @@ -84,7 +85,7 @@ export async function printDocument(src: string): Promise { output.appendLine(``); for (const comment of leadingComments) { - output.append(await printChildNodes(src, [comment], false, false, 0)); + output.append(await printChildNodes(src, [comment], 'block', false, false, 0)); output.linebreak(); } @@ -119,8 +120,8 @@ export async function printDocument(src: string): Promise { output.append(await printElement(src, head, 0)); output.append(await printElement(src, body, 0)); } else { - output.append(await printChildNodes(src, head.childNodes, false, false, 0)); - output.append(await printChildNodes(src, body.childNodes, false, false, 0)); + output.append(await printChildNodes(src, head.childNodes, 'block', false, false, 0)); + output.append(await printChildNodes(src, body.childNodes, 'block', false, false, 0)); } while (output.lines[0] === '') { output.lines.shift(); @@ -166,7 +167,7 @@ export async function printElement( if (PARAGRAPH_LIKE_ELEMENTS.has(node.tagName)) { output.firstLineIsPartial = false; output.appendText(printStartTag(node)); - const body = await printChildNodes(src, childNodes, false, false, indent + 1); + const body = await printChildNodes(src, childNodes, 'block', false, false, indent + 1); body.trim(); if (body.lines.length > 1) { output.linebreak(); @@ -288,7 +289,14 @@ export async function printElement( const type = node.attrs.find(a => a.name === 'type')?.value ?? null; const printedHeader = printHeader(parseResult, type, indent + 2); output.append( - await printChildNodes(src, childNodes.slice(0, maybeH1Index), true, true, indent + 1), + await printChildNodes( + src, + childNodes.slice(0, maybeH1Index), + 'block', + true, + true, + indent + 1, + ), ); if (output.last !== '') { output.linebreak(); @@ -301,7 +309,9 @@ export async function printElement( dropLeadingLinebreaks = false; } } - output.append(await printChildNodes(src, childNodes, dropLeadingLinebreaks, true, indent + 1)); + output.append( + await printChildNodes(src, childNodes, 'block', dropLeadingLinebreaks, true, indent + 1), + ); --output.indent; output.appendLine(``); @@ -360,13 +370,13 @@ export async function printElement( if (block) { output.appendLine(printStartTag(node)); ++output.indent; - output.append(await printChildNodes(src, childNodes, true, true, indent + 1)); + output.append(await printChildNodes(src, childNodes, 'block', true, true, indent + 1)); --output.indent; output.appendLine(``); } else { output.appendText(printStartTag(node)); ++output.indent; - output.append(await printChildNodes(src, childNodes, false, true, indent + 1)); + output.append(await printChildNodes(src, childNodes, 'inline', false, true, indent + 1)); --output.indent; const trailingSpace = output.last.endsWith(' '); if (trailingSpace) { @@ -383,12 +393,14 @@ export async function printElement( async function printChildNodes( src: string, nodes: Node[], + flowContext: 'block' | 'inline', dropLeadingLinebreaks: boolean, dropTrailingLinebreaks: boolean, indent: number, ): Promise { const output = new LineBuilder(indent); let skipNextElement = false; + let inlineRunFirstLine = 0; for (let i = 0; i < nodes.length; ++i) { const node = nodes[i]; if (node.nodeName === '#comment') { @@ -464,14 +476,53 @@ async function printChildNodes( } } } else { + const inlineRunEnded = flowContext === 'block' && isBlockElement(ele); + if (inlineRunEnded) { + fixAsciiQuotes(output.lines, inlineRunFirstLine); + } output.append(await printElement(src, ele, indent)); + if (inlineRunEnded) { + inlineRunFirstLine = output.lines.length; + } } } } + if (flowContext === 'block') { + fixAsciiQuotes(output.lines, inlineRunFirstLine); + } return output; } +function fixAsciiQuotes(lines: string[], i: number) { + for (let inComment = false; i < lines.length; i++) { + let line = lines[i]; + const preservedPrefix = inComment ? line.match(/^.*?-->/)?.[0] || line : ''; + if (preservedPrefix) { + inComment = false; + line = line.substring(preservedPrefix.length); + } else if (inComment) { + continue; + } + const placeholders: string[] = []; + line = line.replace(//g, c => { + placeholders.push(c); + return ''; + }); + const preservedSuffix = line.replace(/^.*/, '').match(/|`(?:[^`]|\\.)*`|<(code|emu-val)>.*?<\/\1>|=".*?"|\*".*?"\*|"(.*?)"/gi, + (m, _tag, asciiQuoted) => (asciiQuoted === undefined ? m : `“${asciiQuoted}”`), + ); + lines[i] = + preservedPrefix + line.replace(//g, () => placeholders.shift()!) + preservedSuffix; + } +} + function isBlockElement(element: Element) { if (ALWAYS_BLOCK_ELEMENTS.has(element.tagName)) { return true; From 6eba17febebded1e6cd47b7836d1beed5b32ad15 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 18 Jun 2026 11:14:55 -0400 Subject: [PATCH 2/5] npm run format-spec --- spec/index.html | 78 ++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/spec/index.html b/spec/index.html index 3dabceb6..3e9fbf92 100644 --- a/spec/index.html +++ b/spec/index.html @@ -51,8 +51,8 @@

Options

`--watch`Rebuild when files change. `--verbose`Print verbose logging info. `--write-biblio`Emit a biblio file to the specified path. - `--assets``assets`"none" for no CSS/JS/etc, "inline" for inline CSS/JS/etc, "external" for external CSS/JS/etc. - `--assets-dir``assetsDir`Directory in which to place assets when using `--assets=external`. Defaults to "assets". + `--assets``assets`“none” for no CSS/JS/etc, “inline” for inline CSS/JS/etc, “external” for external CSS/JS/etc. + `--assets-dir``assetsDir`Directory in which to place assets when using `--assets=external`. Defaults to “assets”. `--lint-spec``lintSpec`Enforce some style and correctness checks. `--error-formatter`The eslint formatter to be used for printing warnings and errors when using `--verbose`. Either the name of a built-in eslint formatter or the package name of an installed eslint compatible formatter. `--max-clause-depth N`Warn when clauses exceed a nesting depth of N, and cause those clauses to be numbered by incrementing their parent clause's number rather than by nesting a new number within their parent clause. @@ -66,29 +66,29 @@

Options

Document options - - - - - - + + + + + + - - + + - - + +
Command Line OptionFront-Matter KeyDescription
`title`Document title, for example "ECMAScript 2016" or "Async Functions".
`status`Document status. Can be "proposal", "draft", or "standard". Defaults to "proposal".
`stage`TC39 proposal stage. If present and `status` is "proposal", `version` defaults to "Stage stage Draft".
`version`Document version, for example "6<sup>th</sup> Edition" (which renders like "6th Edition") or "Draft 1".
`date`Timestamp of document rendering, used for various pieces of boilerplate. Defaults to the value of the `SOURCE_DATE_EPOCH` environment variable (as a number of second since the POSIX epoch) if it exists, otherwise to the current date and time. Required for documents with "standard" status, should reflect adoption date.
`shortname`Document shortname, for example "ECMA-262". If present and `status` is "draft", `version` defaults to "Draft shortname".
`title`Document title, for example “ECMAScript 2016” or “Async Functions”.
`status`Document status. Can be “proposal”, “draft”, or “standard”. Defaults to “proposal”.
`stage`TC39 proposal stage. If present and `status` is “proposal”, `version` defaults to “Stage stage Draft”.
`version`Document version, for example “6<sup>th</sup> Edition” (which renders like “6th Edition”) or “Draft 1”.
`date`Timestamp of document rendering, used for various pieces of boilerplate. Defaults to the value of the `SOURCE_DATE_EPOCH` environment variable (as a number of second since the POSIX epoch) if it exists, otherwise to the current date and time. Required for documents with “standard” status, should reflect adoption date.
`shortname`Document shortname, for example “ECMA-262”. If present and `status` is “draft”, `version` defaults to “Draft shortname”.
`description`Brief description to be used for link previews in social media and the like.
`location`URL of this document. Used in conjunction with the biblio file to support inbound references from other documents.
`copyright`Emit copyright and software license information. Boolean, default true.
`contributors`Contributors to this specification, i.e. those who own the copyright. If your proposal includes text from any Ecma specification, this should include "Ecma International".
`committee`Ecma Technical Committee number. Required for documents with "standard" status, expects e.g. "39". Appends adoption information to <emu-intro> element.
`contributors`Contributors to this specification, i.e. those who own the copyright. If your proposal includes text from any Ecma specification, this should include “Ecma International”.
`committee`Ecma Technical Committee number. Required for documents with “standard” status, expects e.g. “39”. Appends adoption information to <emu-intro> element.
`--no-toc``toc`Emit table of contents. Boolean, default true.
`--printable``printable`Make the output suitable for printing. Boolean, default false.
`--load-biblio``extraBiblios`Extra biblio.json file(s) to load. This should contain either an object as exported by `--write-biblio` or an array of such objects.
`boilerplate`An object with `address` and/or `copyright` and/or `license` fields containing paths to files containing the corresponding content for populating an element with class "copyright-and-software-license" (or if not found, an appended with that id) when `copyright` is true or set to `alternative`. Absent fields are assumed to reference files in a "boilerplate" directory sibling of the directory containing the ecmarkup executable.
`--mark-effects``markEffects`Propagate and render effects like "user code".
`boilerplate`An object with `address` and/or `copyright` and/or `license` fields containing paths to files containing the corresponding content for populating an element with class “copyright-and-software-license” (or if not found, an appended with that id) when `copyright` is true or set to `alternative`. Absent fields are assumed to reference files in a “boilerplate” directory sibling of the directory containing the ecmarkup executable.
`--mark-effects``markEffects`Propagate and render effects like “user code”.

Stylesheets and other assets

-

Ecmarkup requires CSS styles and other assets. By default all assets are inlined into the document. You can override this by setting assets to "none" (for example if you want to manually link to external assets) or "external". When using "external" the default directory for assets is `assets` in the same directory as the output file, but you can override this with `--assets-dir`.

+

Ecmarkup requires CSS styles and other assets. By default all assets are inlined into the document. You can override this by setting assets to “none” (for example if you want to manually link to external assets) or “external”. When using “external” the default directory for assets is `assets` in the same directory as the output file, but you can override this with `--assets-dir`.

@@ -188,14 +188,14 @@

Attributes

number: Optional: An explicit clause number, overriding the default auto-incrementing number. Can be a nested number, as in `number="2.1"`.

type: Optional: Type of feature described by the clause.

    -
  • "abstract operation"
  • -
  • "built-in function"
  • -
  • "concrete method"
  • -
  • "host-defined abstract operation"
  • -
  • "implementation-defined abstract operation"
  • -
  • "internal method"
  • -
  • "numeric method"
  • -
  • "sdo" or "syntax-directed operation"
  • +
  • “abstract operation”
  • +
  • “built-in function”
  • +
  • “concrete method”
  • +
  • “host-defined abstract operation”
  • +
  • “implementation-defined abstract operation”
  • +
  • “internal method”
  • +
  • “numeric method”
  • +
  • “sdo” or “syntax-directed operation”
@@ -207,16 +207,16 @@

Legacy Attributes

Structured Headers

A clause describing an operation or method may have a structured header to document its signature and metadata. Such a structure consists of:

    -
  1. an <h1> element containing the name of the operation followed by a parenthesized list of parameters, each appearing on a comma-terminated line of its own and consisting of a name and value constraints separated by a colon and optionally preceded with "optional"
  2. +
  3. an <h1> element containing the name of the operation followed by a parenthesized list of parameters, each appearing on a comma-terminated line of its own and consisting of a name and value constraints separated by a colon and optionally preceded with “optional”
  4. - a <dl> element with class "header" and a collection of optional key-value pairs expressed using <dt> and <dd> elements: + a <dl> element with class “header” and a collection of optional key-value pairs expressed using <dt> and <dd> elements:
    • description: An explanation of the operation's behaviour.
    • -
    • effects: A list of "effects" associated with the clause (and transitively with those referencing it), separated by commas with optional whitespace. The only currently-known effect is "user-code", which indicates that the operation or method can evaluate non-implementation code (such as custom getters).
    • -
    • for: The type of value to which a clause of type "concrete method" or "internal method" applies.
    • -
    • redefinition: If "true", the name of the operation will not automatically link (i.e., it will not automatically be given an aoid).
    • -
    • skip global checks: If "true", disables consistency checks for this AO which require knowing every callsite.
    • -
    • skip return checks: If "true", disables checking that the returned values from this AO correspond to its declared return type. Adding this to an AO which does not require it will produce a warning.
    • +
    • effects: A list of “effects” associated with the clause (and transitively with those referencing it), separated by commas with optional whitespace. The only currently-known effect is “user-code”, which indicates that the operation or method can evaluate non-implementation code (such as custom getters).
    • +
    • for: The type of value to which a clause of type “concrete method” or “internal method” applies.
    • +
    • redefinition: If “true”, the name of the operation will not automatically link (i.e., it will not automatically be given an aoid).
    • +
    • skip global checks: If “true”, disables consistency checks for this AO which require knowing every callsite.
    • +
    • skip return checks: If “true”, disables checking that the returned values from this AO correspond to its declared return type. Adding this to an AO which does not require it will produce a warning.
@@ -287,13 +287,13 @@

Example

Result

Effects

-

Abstract operations can be marked as having effects that propagate to their invocation sites. Subject to the following rules, calls to such operations are given a class of the effect name prefixed with “e-”. When using `markEffects`, the CSS includes styling for the "user-code" effect via class name `e-user-code` that is off by default but can be togged by pressing u.

+

Abstract operations can be marked as having effects that propagate to their invocation sites. Subject to the following rules, calls to such operations are given a class of the effect name prefixed with “e-”. When using `markEffects`, the CSS includes styling for the “user-code” effect via class name `e-user-code` that is off by default but can be togged by pressing u.

Within an algorithm, an `emu-meta` element enclosing an operation invocation can be used to indicate either the origination of effects or the absence of effects using a list of effect names separated by commas with optional whitespace in an effects or suppress-effects attribute (respectively).


@@ -469,7 +469,7 @@ 

emu-note

Non-normative explanatory text. Comes in two types - regular notes and Editor's notes. Regular notes are intended for the implementers and end users of this specification. Editor's notes are notes to and from the Editors and will generally be removed prior to a specification being finalized and ratified.

Attributes

-

type: The type of note, either blank or "editor".

+

type: The type of note, either blank or “editor”.

Example


@@ -617,7 +617,7 @@ 

Abstract Methods

The first two columns are assumed to be the method signature and the method description respectively.

Attributes

-

type: Must be present with the value "abstract methods".

+

type: Must be present with the value “abstract methods”.

of: Must be present; the value is the name of the type for which this table defines the methods.

caption: Optional: If not present, will be generated based on the type name

@@ -871,8 +871,8 @@

Attributes

primary: Optional: Deprecated in favor of type="definition".

type: Optional: Disposition of the grammar. If absent, it is considered to reference productions defined elsewhere.

    -
  • "definition": The grammar is an authoritative source for productions which should be the target of references.
  • -
  • "example": Deprecated in favor of the example attribute.
  • +
  • “definition”: The grammar is an authoritative source for productions which should be the target of references.
  • +
  • “example”: Deprecated in favor of the example attribute.

Example

@@ -993,8 +993,8 @@

Attributes

primary: Optional: The production is authoritative and should be the target of references.

type: Optional: Type of production. If absent, the production is considered syntactic.

    -
  • "lexical": The production is part of a lexical grammar, and should render with the LHS and RHS separated by two colons “::”.
  • -
  • "regexp": Deprecated because the ECMAScript regular expression grammar is considered to be lexical. Productions with this type render with the LHS and RHS separated by three colons “:::”.
  • +
  • “lexical”: The production is part of a lexical grammar, and should render with the LHS and RHS separated by two colons “::”.
  • +
  • “regexp”: Deprecated because the ECMAScript regular expression grammar is considered to be lexical. Productions with this type render with the LHS and RHS separated by three colons “:::”.
@@ -1029,13 +1029,13 @@

emu-t

emu-gmod

-

Contains well-known modifiers to a right-hand side of a production. The only well-known modifier at present is the "but not" modifier. See the Identifier example above.

+

Contains well-known modifiers to a right-hand side of a production. The only well-known modifier at present is the “but not” modifier. See the Identifier example above.

emu-gann

-

Contains well-known annotations to to a right-hand side of a production. The only well-known modifiers at present are "lookahead" and "empty". See the ExpressionStatement example above. Any text inside a gann element is wrapped in square brackets.

+

Contains well-known annotations to to a right-hand side of a production. The only well-known modifiers at present are “lookahead” and “empty”. See the ExpressionStatement example above. Any text inside a gann element is wrapped in square brackets.

@@ -1071,7 +1071,7 @@

Old IDs

Indicating Changes

-

The `ins` and `del` HTML elements can be used to mark insertions and deletions respectively. When adding or removing block content (such as entire list items, paragraphs, clauses, grammar RHSes, etc.), use a class of "block".

+

The `ins` and `del` HTML elements can be used to mark insertions and deletions respectively. When adding or removing block content (such as entire list items, paragraphs, clauses, grammar RHSes, etc.), use a class of “block”.

Inline Example

ECMAScript <del>6</del><ins>2015</ins> <del>will be ratified</del><ins>was ratified</ins> in June, 2015.
From d96761788dbcfd7a77ffb11f44584e98414b8f07 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 18 Jun 2026 17:51:41 -0400 Subject: [PATCH 3/5] fixup! When formatting, replace inline ASCII quotes with typographic quotes --- src/formatter/ecmarkup.ts | 57 ++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/formatter/ecmarkup.ts b/src/formatter/ecmarkup.ts index 2ec9978c..43e62298 100644 --- a/src/formatter/ecmarkup.ts +++ b/src/formatter/ecmarkup.ts @@ -376,7 +376,8 @@ export async function printElement( } else { output.appendText(printStartTag(node)); ++output.indent; - output.append(await printChildNodes(src, childNodes, 'inline', false, true, indent + 1)); + const flowContext = node.tagName === 'emu-note' ? 'block' : 'inline'; + output.append(await printChildNodes(src, childNodes, flowContext, false, true, indent + 1)); --output.indent; const trailingSpace = output.last.endsWith(' '); if (trailingSpace) { @@ -494,9 +495,30 @@ async function printChildNodes( return output; } -function fixAsciiQuotes(lines: string[], i: number) { +// this regular expression is not perfect, but generally follows +// https://html.spec.whatwg.org/multipage/parsing.html#tokenization +// (and spec source text tends to avoid the sort of edge cases that would +// reveal its flaws) +const rHtmlTag = (() => { + const SPACE_CHAR = '[\\t\\n\\f ]'; + const TOKEN_CHAR = '[^\\t\\n\\f />]'; + const ATTR = `(?=${TOKEN_CHAR}|=)${TOKEN_CHAR}*(?:=${SPACE_CHAR}*(?:"[^"]*"|'[^']*'|(?!"|')${TOKEN_CHAR}*)?)?`; + return new RegExp( + String.raw`(`, + 'gi', + ); +})(); + +const rMaybeAsciiQuoted = new RegExp( + String.raw`${'`'}(?:[^${'`'}\\]|\\.)*${'`'}|<(${[...RAW_CONTENT_ELEMENTS].join('|')})\b[^>]*>.*?]*>|=".*?"|\*".*?"\*|"(.*?)"`, + 'gi', +); + +function fixAsciiQuotes(lines: string[], i: number = 0) { for (let inComment = false; i < lines.length; i++) { let line = lines[i]; + + // handle multi-line comments const preservedPrefix = inComment ? line.match(/^.*?-->/)?.[0] || line : ''; if (preservedPrefix) { inComment = false; @@ -504,22 +526,33 @@ function fixAsciiQuotes(lines: string[], i: number) { } else if (inComment) { continue; } + + // replace escapes/comments and tags with placeholders (in that order) const placeholders: string[] = []; - line = line.replace(//g, c => { - placeholders.push(c); - return ''; + line = line.replace(/&\d|/g, c => `&${placeholders.push(c) - 1};`); + line = line.replace(rHtmlTag, (tag, prefix, name, attrs) => { + if (RAW_CONTENT_ELEMENTS.has(name)) { + // keep the tag name but replace any attributes with a placeholder + return attrs ? `${prefix}&${placeholders.push(tag.slice(prefix.length, -1)) - 1};>` : tag; + } + return `&${placeholders.push(tag) - 1};`; }); - const preservedSuffix = line.replace(/^.*/, '').match(/|`(?:[^`]|\\.)*`|<(code|emu-val)>.*?<\/\1>|=".*?"|\*".*?"\*|"(.*?)"/gi, - (m, _tag, asciiQuoted) => (asciiQuoted === undefined ? m : `“${asciiQuoted}”`), + + // replace remaining ASCII quotes + line = line.replace(rMaybeAsciiQuoted, (m, _tagName, asciiQuoted) => + asciiQuoted === undefined ? m : `“${asciiQuoted}”`, ); - lines[i] = - preservedPrefix + line.replace(//g, () => placeholders.shift()!) + preservedSuffix; + + // replace the line, with placeholders restored in reverse order + line = line.replace(/&(\d+);/g, (_m, i) => placeholders[i]); + line = line.replace(/&(\d+);/g, (_m, i) => placeholders[i]); + lines[i] = preservedPrefix + line + preservedSuffix; } } From 268ce2544c14cff15507eaea0d6e9c1d285af586 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 18 Jun 2026 17:52:21 -0400 Subject: [PATCH 4/5] Also replace inline ASCII quotes in emu-alg steps --- src/formatter/ecmarkup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/formatter/ecmarkup.ts b/src/formatter/ecmarkup.ts index 43e62298..4c535d05 100644 --- a/src/formatter/ecmarkup.ts +++ b/src/formatter/ecmarkup.ts @@ -193,7 +193,9 @@ export async function printElement( bad(node, 'failed to parse algorithm'); } output.appendLine(printStartTag(node)); - output.append(await printAlgorithm(contents, parsed, indent + 1)); + const steps = await printAlgorithm(contents, parsed, indent + 1); + fixAsciiQuotes(steps.lines); + output.append(steps); output.appendLine(``); return output; } From 33dca45f707cd0998fa7998a8f4606b6b49c71c1 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 18 Jun 2026 18:28:04 -0400 Subject: [PATCH 5/5] Add test coverage for ASCII quote replacement --- test/formatter.ts | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/test/formatter.ts b/test/formatter.ts index 9bc083f1..d3d35b62 100644 --- a/test/formatter.ts +++ b/test/formatter.ts @@ -261,16 +261,18 @@ describe('document formatting', () => { ); }); - it('
-    content
+    "content"
 
`, ); @@ -324,6 +326,39 @@ describe('document formatting', () => { `, ); }); + + it('replaces ASCII quotes', async () => { + await assertDocFormatsAs( + ` +
+ "quotes" can span "inline elements" + + and quotes can "follow comments" +
+ + 1. [x="a"] Assert: Quotes are also "detected" in "algorithm steps". + + `, + dedentKeepingTrailingNewline` +
+ “quotes” can span “inline elements” + + and quotes can “follow comments” +
+ + 1. [x="a"] Assert: Quotes are also “detected” in “algorithm steps”. + + `, + ); + }); + + it('preserves ASCII quotes that are code', async () => { + await assertRoundTrips( + `

ASCII quotes are not replaced in "code elements", "emu-val elements", ${'`'}"backtick spans"${'`'}, or *"inline language strings"*.

\n`, + ); + }); }); describe('grammar formatting', () => {