Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/markup/src/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import type {

const NAME = /^[A-Za-z_][A-Za-z0-9_.:-]*/u;
const ALIAS = /^[a-z][a-z0-9_-]{0,63}$/u;
const REFERENCE = /^[A-Za-z_][A-Za-z0-9_.:-]*(?:\.[A-Za-z_][A-Za-z0-9_.:-]*)*$/u;
// Strictly segmented: `.` separates segments and can never appear inside one. An inner
// character class that also accepts `.` lets a rejecting input partition its dots between
// two quantified parts, which is exponential backtracking on a synchronous parse thread.
const REFERENCE = /^[A-Za-z_][A-Za-z0-9_:-]*(?:\.[A-Za-z_][A-Za-z0-9_:-]*)*$/u;

type OpeningTag = {
readonly name: string;
Expand Down
32 changes: 32 additions & 0 deletions packages/markup/test/syntax.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import test from "node:test";

import { MarkupFrontendError } from "../src/error.js";
import { parseOpeningTag } from "../src/syntax.js";

const source = (text: string) => ({ name: "test.markup", text });

test("reference attributes accept dotted segment paths", () => {
const parsed = parseOpeningTag(source(`<Item value={timeline.start} other={ns:record-1.x}/>`), 0);
assert.deepEqual(parsed.attributes["value"], { kind: "reference", path: "timeline.start" });
assert.deepEqual(parsed.attributes["other"], { kind: "reference", path: "ns:record-1.x" });
});

test("reference attributes reject empty or trailing segments", () => {
for (const path of ["a.", "a..b", ".a", "a.b."]) {
assert.throws(
() => parseOpeningTag(source(`<Item value={${path}}/>`), 0),
(error: unknown) => error instanceof MarkupFrontendError && error.code === "MARKUP_REFERENCE",
);
}
});

test("a long malformed reference rejects without exponential backtracking", () => {
const path = `a${".b".repeat(40)}!`;
const startedAt = Date.now();
assert.throws(
() => parseOpeningTag(source(`<Item value={${path}}/>`), 0),
(error: unknown) => error instanceof MarkupFrontendError && error.code === "MARKUP_REFERENCE",
);
assert(Date.now() - startedAt < 1000, "malformed reference validation took too long");
});