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
34 changes: 27 additions & 7 deletions packages/recipe/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,39 @@ function parseAttributes(
offset: number,
): Readonly<Record<string, string>> {
const attributes: Record<string, string> = {};
const attribute = /([a-z][a-z0-9-]*)\s*=\s*(["'])(.*?)\2/gu;
// Scanned by hand: the regex this replaced backtracked through one character class per
// position and went quadratic on a long run of name characters with no "=".
let cursor = 0;
for (const match of raw.matchAll(attribute)) {
const start = match.index ?? 0;
if (raw.slice(cursor, start).trim().length > 0) {
while (cursor < raw.length) {
while (cursor < raw.length && /\s/u.test(raw[cursor]!)) cursor++;
if (cursor >= raw.length) break;
const start = cursor;
if (!(raw[cursor]! >= "a" && raw[cursor]! <= "z")) {
fail(sourceName, "SVS_SHEET_ATTRIBUTE", "Malformed <sheet> attribute.", offset + cursor);
}
while (cursor < raw.length && /[a-z0-9-]/u.test(raw[cursor]!)) cursor++;
const name = raw.slice(start, cursor);
while (cursor < raw.length && /\s/u.test(raw[cursor]!)) cursor++;
if (raw[cursor] !== "=") {
fail(sourceName, "SVS_SHEET_ATTRIBUTE", "Malformed <sheet> attribute.", offset + cursor);
}
cursor++;
while (cursor < raw.length && /\s/u.test(raw[cursor]!)) cursor++;
const quote = raw[cursor];
if (quote !== '"' && quote !== "'") {
fail(sourceName, "SVS_SHEET_ATTRIBUTE", "Malformed <sheet> attribute.", offset + cursor);
}
const close = raw.indexOf(quote, cursor + 1);
// Line terminators never matched the old (.*?)\2 body, so a quoted value spanning lines
// was malformed rather than a short value.
if (close === -1 || /[\n\r\u2028\u2029]/u.test(raw.slice(cursor + 1, close))) {
fail(sourceName, "SVS_SHEET_ATTRIBUTE", "Malformed <sheet> attribute.", offset + cursor);
}
const name = match[1] as string;
if (Object.hasOwn(attributes, name)) {
fail(sourceName, "SVS_SHEET_ATTRIBUTE_DUPLICATE", `Duplicate sheet attribute ${name}.`, offset + start);
}
attributes[name] = match[3] as string;
cursor = start + match[0].length;
attributes[name] = raw.slice(cursor + 1, close);
cursor = close + 1;
}
if (raw.slice(cursor).trim().length > 0) {
fail(sourceName, "SVS_SHEET_ATTRIBUTE", "Malformed <sheet> attribute.", offset + cursor);
Expand Down
22 changes: 18 additions & 4 deletions packages/studio/src/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,24 @@ function attributeGroup(file: StudioSourceFile, target: AuthorElement, draft: St
if (target.references[name] !== undefined) { writable = false; value[name] = target.references[name]!; continue; }
value[name] = target.attributes[name] ?? target.references[name] ?? "";
const before = file.text.slice(target.range.start, range.start);
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const prefix = new RegExp(`\\s+${escaped}\\s*=\\s*["']$`, "u").exec(before);
if (!prefix) throw new Error(`Cannot locate scalar attribute ${name}.`);
ranges[name] = { start: prefix.index, end: range.end - target.range.start + 1 };
// The tail must end `<ws>+ name <ws>* = <ws>* quote`. Scanned backward from the end: a
// regex anchored at the end still retried from every position and went quadratic on long
// whitespace runs.
const located = ((): number | undefined => {
let i = before.length - 1;
if (i < 0 || (before[i] !== '"' && before[i] !== "'")) return undefined;
while (i > 0 && /\s/u.test(before[i - 1]!)) i--;
if (i === 0 || before[i - 1] !== "=") return undefined;
i--;
while (i > 0 && /\s/u.test(before[i - 1]!)) i--;
if (i < name.length || before.slice(i - name.length, i) !== name) return undefined;
i -= name.length;
if (i === 0 || !/\s/u.test(before[i - 1]!)) return undefined;
while (i > 0 && /\s/u.test(before[i - 1]!)) i--;
return i;
})();
if (located === undefined) throw new Error(`Cannot locate scalar attribute ${name}.`);
ranges[name] = { start: located, end: range.end - target.range.start + 1 };
}
const schema = parameterRecordSchema(declaration.schema, value);
for (const [name, held] of Object.entries(value)) {
Expand Down
8 changes: 7 additions & 1 deletion packages/text-fine/src/surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,13 @@ export const decodeTypographyPathMotionSurface: StructuredSurfaceHandler = ({ el
};

function dedent(value: string): string {
const lines = value.replace(/^\n/u, "").replace(/\n\s*$/u, "").split("\n");
let text = value.startsWith("\n") ? value.slice(1) : value;
// The trailing run is dropped only when it begins with a newline — what /\n\s*$/ did, but
// scanned backward: the regex retried the tail from every position and went quadratic.
let end = text.length;
while (end > 0 && /\s/u.test(text[end - 1]!)) end--;
if (end < text.length && text[end] === "\n") text = text.slice(0, end);
const lines = text.split("\n");
const indentation = lines.filter((line) => line.trim()).reduce((min, line) => Math.min(min, /^\s*/u.exec(line)?.[0].length ?? 0), Number.POSITIVE_INFINITY);
return lines.map((line) => line.slice(Number.isFinite(indentation) ? indentation : 0)).join("\n");
}
Expand Down