mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): Studio's own text reads without em or en dashes (#5414)
* fix(studio): text in Studio reads without em or en dashes * fix(studio): the dash check also catches HTML entities, and peak reads Unavailable * test(studio): the dash check test covers every entity form * fix(studio): messages Studio shows from lint, sdk, core and the renderer drop dashes too * fix(producer): the gate check stops early on the engine's new fallback wording
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// Text a person reads in Studio carries no em or en dash: use a colon, comma, period or parentheses.
|
||||
// Covers Studio and every package whose messages it shows; the CLI's terminal-only output is out.
|
||||
// Reads the TypeScript AST, so strings, template text and JSX text count and comments never do.
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { extname, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const ROOT = join(import.meta.dirname, "..");
|
||||
const PACKAGES = [
|
||||
"core",
|
||||
"engine",
|
||||
"lint",
|
||||
"parsers",
|
||||
"player",
|
||||
"producer",
|
||||
"sdk",
|
||||
"shader-transitions",
|
||||
"studio",
|
||||
"studio-server",
|
||||
];
|
||||
// JSX keeps HTML entities raw in the AST, and React renders them as dashes.
|
||||
const DASH = /[\u2013\u2014]|&[mn]dash;|̵[12];|ȁ[34];/i;
|
||||
// Dashes no person sees in Studio, each matched as narrowly as the text allows.
|
||||
const EXEMPT = [
|
||||
// A comment in injected runtime JS, copied byte for byte into producer goldens and catalog payloads.
|
||||
{
|
||||
file: "packages/core/src/compiler/compositionScoping.ts",
|
||||
contains: "Swallow \u2014 the scoped root",
|
||||
},
|
||||
// The empty cell of .media/index.md, an agent file kept identical to the media-use copies.
|
||||
{ file: "packages/core/src/figma/mediaIndex.ts", equals: "\u2014" },
|
||||
];
|
||||
const isExempt = (filename, text) =>
|
||||
EXEMPT.some(
|
||||
(rule) =>
|
||||
rule.file === filename &&
|
||||
(rule.equals === undefined ? text.includes(rule.contains) : text === rule.equals),
|
||||
);
|
||||
const TEXT_KINDS = new Set([
|
||||
ts.SyntaxKind.StringLiteral,
|
||||
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
||||
ts.SyntaxKind.TemplateHead,
|
||||
ts.SyntaxKind.TemplateMiddle,
|
||||
ts.SyntaxKind.TemplateTail,
|
||||
ts.SyntaxKind.JsxText,
|
||||
]);
|
||||
|
||||
export function listDashedText(source, filename = "source.tsx") {
|
||||
const kind = extname(filename) === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
||||
const sourceFile = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, kind);
|
||||
const issues = [];
|
||||
function visit(node) {
|
||||
if (TEXT_KINDS.has(node.kind) && DASH.test(node.text) && !isExempt(filename, node.text)) {
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
||||
issues.push(`${filename}:${line + 1} ${node.text.trim().slice(0, 100)}`);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(sourceFile);
|
||||
return issues;
|
||||
}
|
||||
|
||||
export const isShippedSource = (path) =>
|
||||
/\.tsx?$/.test(path) && !/\.d\.ts$|\.(test|spec)\.tsx?$/.test(path);
|
||||
|
||||
export function checkStudioCopyDashes(root = ROOT) {
|
||||
return PACKAGES.flatMap((name) => {
|
||||
const sourceRoot = join(root, "packages", name, "src");
|
||||
return readdirSync(sourceRoot, { recursive: true })
|
||||
.filter(isShippedSource)
|
||||
.sort()
|
||||
.flatMap((path) =>
|
||||
listDashedText(
|
||||
readFileSync(join(sourceRoot, path), "utf8"),
|
||||
relative(root, join(sourceRoot, path)),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
const issues = checkStudioCopyDashes();
|
||||
if (issues.length > 0) {
|
||||
console.error("Studio text with an em or en dash (use a colon, comma, period or parentheses):");
|
||||
issues.forEach((issue) => console.error(`- ${issue}`));
|
||||
console.error(`${issues.length} hit(s).`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
console.log("Studio copy verified: no em or en dash in shipped text.");
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
|
||||
@@ -0,0 +1,105 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
checkStudioCopyDashes,
|
||||
isShippedSource,
|
||||
listDashedText,
|
||||
} from "./check-studio-copy-dashes.mjs";
|
||||
|
||||
const lines = (source) => listDashedText(source, "x.tsx").map((issue) => issue.split(" ")[0]);
|
||||
|
||||
describe("Studio copy dash checker", () => {
|
||||
it("reports dashes in every string, template and JSX text form, literal or escaped", () => {
|
||||
const source = [
|
||||
'const a = "One \\u2014 two";',
|
||||
"const b = `plain \\u2013 template`;",
|
||||
"const c = `\\u2014 ${a}`;",
|
||||
"const d = `${a} \\u2014 ${b} x`;",
|
||||
"const e = `${a} x \\u2014`;",
|
||||
'const f = "Literal \u2014 too";',
|
||||
"const g = () => <p>Range 1\u20132</p>;",
|
||||
"const h = () => <p>One — two</p>;",
|
||||
'const i = () => <p title="One – two" />;',
|
||||
"const j = () => <p>One – two</p>;",
|
||||
"const l = () => <p>One — two</p>;",
|
||||
'const k = () => <p title="One — two" />;',
|
||||
].join("\n");
|
||||
assert.deepEqual(lines(source), [
|
||||
"x.tsx:1",
|
||||
"x.tsx:2",
|
||||
"x.tsx:3",
|
||||
"x.tsx:4",
|
||||
"x.tsx:5",
|
||||
"x.tsx:6",
|
||||
"x.tsx:7",
|
||||
"x.tsx:8",
|
||||
"x.tsx:9",
|
||||
"x.tsx:10",
|
||||
"x.tsx:11",
|
||||
"x.tsx:12",
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores comments, regexes, hyphens and shortcut glyphs", () => {
|
||||
const source = [
|
||||
"// A comment \u2014 never copy",
|
||||
"/* block \u2013 comment */",
|
||||
"const d = /[\u2013\u2014]/;",
|
||||
'const f = "A plain hyphen - is fine, \u2325-click too";',
|
||||
].join("\n");
|
||||
assert.deepEqual(lines(source), []);
|
||||
});
|
||||
|
||||
it("exempts only the named strings in the named files", () => {
|
||||
const media = 'const a = "\u2014";\nconst b = "a \u2014 b";';
|
||||
assert.deepEqual(
|
||||
listDashedText(media, "packages/core/src/figma/mediaIndex.ts").map((i) => i.split(" ")[0]),
|
||||
["packages/core/src/figma/mediaIndex.ts:2"],
|
||||
);
|
||||
assert.equal(listDashedText(media, "packages/core/src/other.ts").length, 2);
|
||||
const scoping = 'const s = `/* Swallow \u2014 the scoped root */`;\nconst t = "a \u2014 b";';
|
||||
assert.deepEqual(
|
||||
listDashedText(scoping, "packages/core/src/compiler/compositionScoping.ts").map(
|
||||
(i) => i.split(" ")[0],
|
||||
),
|
||||
["packages/core/src/compiler/compositionScoping.ts:2"],
|
||||
);
|
||||
});
|
||||
|
||||
it("checks shipped .ts and .tsx files, not tests or declarations", () => {
|
||||
assert.deepEqual(
|
||||
["a.ts", "b.tsx", "c.test.ts", "d.spec.tsx", "e.d.ts", "f.css", "g.js"].filter(
|
||||
isShippedSource,
|
||||
),
|
||||
["a.ts", "b.tsx"],
|
||||
);
|
||||
});
|
||||
|
||||
it("scans Studio and every package whose text Studio shows, but not the CLI", () => {
|
||||
const shown = [
|
||||
"core",
|
||||
"engine",
|
||||
"lint",
|
||||
"parsers",
|
||||
"player",
|
||||
"producer",
|
||||
"sdk",
|
||||
"shader-transitions",
|
||||
"studio",
|
||||
"studio-server",
|
||||
];
|
||||
const root = mkdtempSync(join(tmpdir(), "studio-dashes-"));
|
||||
for (const name of [...shown, "cli"]) {
|
||||
const src = join(root, "packages", name, "src");
|
||||
mkdirSync(src, { recursive: true });
|
||||
writeFileSync(join(src, "a.ts"), 'export const a = "x \u2014 y";');
|
||||
writeFileSync(join(src, "a.test.ts"), 'const b = "x \u2014 y";');
|
||||
}
|
||||
const scanned = checkStudioCopyDashes(root).map((issue) => issue.split("/")[1]);
|
||||
rmSync(root, { recursive: true });
|
||||
assert.deepEqual(scanned.sort(), shown);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user