Skip to content

3.1.0: long-standing community fixes + release prep#67

Merged
adrai merged 20 commits into
masterfrom
release-3.1.0
Jul 22, 2026
Merged

3.1.0: long-standing community fixes + release prep#67
adrai merged 20 commits into
masterfrom
release-3.1.0

Conversation

@adrai

@adrai adrai commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

This is the maintenance release announced in #65 (posted July 10, no objections in the 10-day window).

Branch protection requires one approving review, and direct pushes to master are blocked, so this PR bundles the whole release. One approval from any collaborator with write access merges everything. Happy to split it up if anyone prefers.

What's inside

Merge commits for the long-standing community PRs (authorship preserved; GitHub will mark them merged automatically):

Plus one cleanup commit on top:

  • replace matchAll with an exec loop (matchAll is ES2020; keeps the ES5 API compatibility the package always had)
  • remove leftover debug logging from fix bug, when text content contains < #64
  • prettier pass on the touched files (repo's own config)
  • README: 3.1.0 changelog entry + a note that ongoing development continues at the i18next org (see Project maintenance #65)

Verification

  • full test suite passes (58/58)
  • golden-corpus diff against published 3.0.1 across 24 fixtures: 18 byte-identical, 6 diffs, each one an intended fix from the PRs above
  • react-i18next (the largest dependent) test suite: 493/493 against this tree

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prepares the 3.1.0 maintenance release by merging several long-standing community fixes (parser correctness, TypeScript types, packaging, docs) and doing release-prep cleanup to keep compatibility and improve coverage.

Changes:

  • Updates the HTML parsing logic to better handle edge cases like < appearing in text and text following comment nodes, including additional regression tests.
  • Extends attribute parsing to support multi-line attribute values.
  • Adds/ships TypeScript declarations, adds LICENSE, and updates README changelog/repo-maintenance note.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/parse.js Refactors tag scanning and adjusts text/comment handling to address multiple parsing edge cases.
src/parse-tag.js Updates attribute regex to support multi-line quoted values.
test/parse.js Adds/updates regression tests for comment-following text and <10 ... text vs tag parsing.
html-parse-stringify.d.ts Reworks TypeScript typings and publishes them via package.json.
package.json Ships .d.ts in the npm tarball and sets the types entrypoint.
README.md Adds 3.1.0 changelog entry and maintenance notice pointing to the i18next org repo.
LICENSE Adds the MIT license file to the repository/distribution.
Comments suppressed due to low confidence (1)

src/parse.js:73

  • When parsing a comment node at the root level (level < 0), the code returns early after pushing the comment, so any text immediately following the comment (e.g. <!--c-->text) will be dropped. Also, html.slice(start, html.indexOf('<', start)) truncates the last character when there is no subsequent < (because indexOf returns -1 and slice(..., -1) drops the last char). Consider pushing trailing text for both root and nested comments, and handle the -1 case the same way the trailing-text logic does.
    if (isComment) {
      const comment = parseTag(tag)

      // if we're at root, push new base node
      if (level < 0) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/parse.js Outdated
Comment thread test/parse.js Outdated
@adrai

adrai commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Both Copilot nits addressed in 25a332c (assertion messages now describe what each test actually validates).

Comment thread src/parse.js Outdated
if (!tag) return
const amountOfLts = tag.split('<').length
const amountOfGts = tag.split('>').length
if (amountOfLts > 0 && amountOfLts > amountOfGts) {

@frontmesh frontmesh Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This count includes < characters inside quoted attribute values, causing valid inputs to be corrupted.

For example:

<div title="1 < 2">Hello</div>

This parses correctly on master, but on this branch it returns a node with:

{
  name: '',
  attrs: {}
}

It then stringifies as:

<>Hello</>

Please make this detection quote-aware and add regression tests for < inside both single- and double-quoted attribute values.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, thank you! Fixed in f324559: the pre-pass now counts brackets outside quoted sections only, and comment matches are skipped entirely (comments containing < hit the same bogus split). Regression tests added for single- and double-quoted attribute values, mixed </> inside attributes, and comments containing < — plus re-verified the corpus diff against published 3.0.1 (your cases now parse identically to master) and react-i18next's full suite (493/493).

The lts/gts counting introduced in #64 also counted brackets inside quoted
attribute values, corrupting valid inputs like <div title="1 < 2"> into
<>...</>. Comments containing < hit the same bogus split. The pre-pass now
scans outside quotes only and leaves comment matches alone.

Found by @frontmesh in #67 review. Adds regression tests for single- and
double-quoted attribute values, mixed brackets, and comments containing <.
@adrai
adrai requested a review from frontmesh July 22, 2026 11:26
Comment thread html-parse-stringify.d.ts Outdated
function stringify(doc: Node[]): string;
}

export default HtmlParseStringify;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package’s CommonJS entry point exports the API directly:

module.exports = {
  parse,
  stringify
}

However, this declaration describes a default export. When TypeScript targets CommonJS without esModuleInterop, a default import is emitted as:

require('html-parse-stringify').default

The runtime module has no .default property, so this evaluates to undefined. This declaration also rejects the CommonJS-compatible TypeScript syntax:

import HTML = require('html-parse-stringify')

The previous export = HtmlParseStringify declaration matched the CommonJS runtime. Please retain that export shape or update the runtime/package exports to provide a real default export.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 274a47c: the declaration now uses export = with a namespace, matching the actual module.exports = { parse, stringify } shape, so it's correct both with and without esModuleInterop (and import HTML = require(...) works again).

Comment thread html-parse-stringify.d.ts Outdated

export interface IOptions {
components: string[]
export type Node = TagNode | TextNode | ComponentNode;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse() returns comment nodes at runtime:

{
  type: 'comment',
  comment: string
}

stringify() also handles this node type, but the new Node union excludes it. Consequently, TypeScript consumers cannot represent valid parser output as Node or pass a manually constructed comment node to stringify() without a cast.

Please add a corresponding type and include it in the union:

export interface CommentNode {
  type: 'comment';
  comment: string;
}

export type Node =
  | TagNode
  | TextNode
  | ComponentNode
  | CommentNode;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 274a47c: CommentNode added and included in the Node union.

Comment thread src/parse-tag.js
@@ -1,5 +1,5 @@
import lookup from 'void-elements'
const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new multiline pattern explicitly handles \n, but . does not match \r. As a result, attributes containing Windows-style CRLF line endings are still not parsed.

For example:

<div title="first
second">Hello</div>

when the line break is \r\n, produces attrs: {} on this branch rather than:

{
  title: 'first\r\nsecond'
}

Please use a pattern that covers every line terminator, such as quote-specific negated character classes, and add a CRLF regression test alongside the LF case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 274a47c: attrRE now uses quote-specific negated classes ("[^"]*"|'[^']*'), which covers every line terminator (and drops the backtracking-prone (.|\n)*? while at it). CRLF regression tests added for both quote styles.

Comment thread src/parse.js Outdated
indexOfPossibleContent + possibleContent.length + 1
const nextLt = html.indexOf('<', startAfterPossibleContent)
const nextGt = html.indexOf('>', startAfterPossibleContent)
if (nextLt > -1 && nextLt < nextGt) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new text-recovery logic extends the content only as far as the next <, so it handles one literal < but still truncates text containing several.

For example:

<div>1 < 2 < 3</div>

This branch produces:

{
  type: 'text',
  content: '1 < 2 '
}

The remaining < 3 is discarded. Please continue scanning until the next actual tag match rather than stopping at the second <, and add a regression test containing multiple literal less-than characters.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 274a47c: text nodes now extend to the next actual tag match instead of the next literal < (applied to all three text paths: child text, trailing text, and text after comments). Interesting side effect: this also fixed two long-standing quirks that predate this PR — unclosed trailing text dropped its last character (<div>text parsed as tex), and text was truncated at < followed by > (<div>5 < 6 and 7 > 3</div> parsed as 5 ). Regression tests added, corpus re-diffed against 3.0.1 (every diff is an intended fix), react-i18next 493/493.

- types: export = declaration matching the CommonJS module shape (works
  with and without esModuleInterop), add missing CommentNode to the union
- parse-tag: quote-specific character classes in attrRE so attribute
  values with CRLF (or any line terminator) parse correctly
- parse: text nodes now extend to the next actual tag match instead of
  the next literal <, fixing truncation of text with multiple < and,
  as a side effect, two long-standing quirks (last character dropped
  from unclosed trailing text, text truncated at < followed by >)

Regression tests for each case.
@adrai
adrai requested a review from frontmesh July 22, 2026 12:05
A 30k-case differential fuzz against 3.0.1 found one crash: the
mismatched-bracket split could produce a fragment (e.g. `< <!-->`) whose
parsed name is a comment while the walker treats the match as an open
tag, crashing on the missing children array. The split now only happens
when the remainder is itself a valid tag start.

Side effect: parse/stringify is now round-trip stable across the entire
fuzz corpus (the invalid splits were the only source of instability).
Regression tests added.
@adrai

adrai commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Extra hardening round: ran a 30,000-case seeded differential fuzz of this branch against published 3.0.1 (plus a 52-input edge-case matrix around every changed code path, timing probes on adversarial inputs, and round-trip stability checks). Found and fixed one crash on pathological input (split fragment parsing as a comment; regression tests added). After the fix the branch has zero crashes and is parse/stringify round-trip stable across the entire fuzz corpus, which 3.0.1 only achieves by silently dropping text. All timing probes flat (≤20ms on 50KB adversarial inputs), react-i18next suite still 493/493.

@frontmesh frontmesh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing all the review findings. Nice work!

Comment thread package.json Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's update the version number here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — bumped to 3.1.0 (package.json + lockfile version field).

@adrai
adrai requested a review from pconerly July 22, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

7 participants