218 lines
7.2 KiB
JavaScript
218 lines
7.2 KiB
JavaScript
import { readFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, resolve } from "node:path";
|
|
|
|
import { britishSpellings, imperativeVerbs, nonSteTerms, technicalTerms } from "./custom-widgets-ste-terms.mjs";
|
|
|
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
const docsDirectory = resolve(scriptDirectory, "../docs");
|
|
|
|
const files = [
|
|
{ path: "management/custom-widgets/agent-authoring.mdx" },
|
|
{ path: "management/custom-widgets/authoring.mdx" },
|
|
{ path: "management/custom-widgets/custom-jsx.mdx" },
|
|
{ path: "management/custom-widgets/index.mdx" },
|
|
{ path: "management/custom-widgets/requests-and-security.mdx" },
|
|
{ path: "management/custom-widgets/troubleshooting.mdx" },
|
|
{ path: "management/workshop/index.mdx" },
|
|
{ path: "widgets/custom-api/index.mdx" },
|
|
{ path: "management/api/index.mdx", section: "Get Custom Widget authoring resources" },
|
|
{ path: "management/mcp.mdx", section: "Create a Custom Widget with an AI agent", includeMatching: /Custom Widget/ },
|
|
{ path: "advanced/environment-variables/index.mdx", includeMatching: /WORKSHOP_API_URL/ },
|
|
{ path: "community/faq.mdx", section: "Can I add my own widgets? Do I need programming skills?" },
|
|
{ path: "management/backup.mdx", includeMatching: /Custom Widget/ },
|
|
];
|
|
|
|
const imperativeSet = new Set(imperativeVerbs);
|
|
const issues = [];
|
|
|
|
const overview = await readFile(resolve(docsDirectory, "management/custom-widgets/index.mdx"), "utf8");
|
|
if (!overview.includes("## Audience and prerequisites")) {
|
|
addIssue("management/custom-widgets/index.mdx", 1, "Add the required audience and prerequisites section.");
|
|
}
|
|
|
|
for (const file of files) {
|
|
const absolutePath = resolve(docsDirectory, file.path);
|
|
const source = await readFile(absolutePath, "utf8");
|
|
const selectedLines = selectLines(source, file);
|
|
const paragraphs = getProseParagraphs(selectedLines);
|
|
|
|
for (const paragraph of paragraphs) {
|
|
const sentences = splitSentences(paragraph.text);
|
|
if (sentences.length > 6) {
|
|
addIssue(file.path, paragraph.line, `Paragraph has ${sentences.length} sentences. The limit is 6.`);
|
|
}
|
|
|
|
for (const sentence of sentences) {
|
|
const wordCount = countWords(sentence);
|
|
const isProcedure = paragraph.isList || startsWithImperative(sentence);
|
|
const limit = isProcedure ? 20 : 25;
|
|
if (wordCount > limit) {
|
|
const type = isProcedure ? "Procedural" : "Descriptive";
|
|
addIssue(file.path, paragraph.line, `${type} sentence has ${wordCount} words. The limit is ${limit}.`);
|
|
}
|
|
}
|
|
|
|
checkTerms(file.path, paragraph.line, paragraph.text);
|
|
}
|
|
}
|
|
|
|
if (issues.length > 0) {
|
|
console.error("Custom Widget STE check failed:\n");
|
|
for (const issue of issues) console.error(`- ${issue}`);
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log(`Custom Widget STE check passed. ${technicalTerms.length} technical terms are registered.`);
|
|
}
|
|
|
|
function selectLines(source, file) {
|
|
const lines = source.split("\n").map((text, index) => ({ text, line: index + 1 }));
|
|
if (!file.section && !file.includeMatching) return lines;
|
|
|
|
const selected = [];
|
|
if (file.section) {
|
|
const start = lines.findIndex(({ text }) => text.trim().replace(/^#{2,6}\s+/, "") === file.section);
|
|
if (start === -1) throw new Error(`Section '${file.section}' was not found in ${file.path}`);
|
|
const headingLevel = lines[start].text.trim().match(/^#+/)?.[0].length ?? 2;
|
|
|
|
for (let index = start; index < lines.length; index += 1) {
|
|
const nextHeadingLevel = lines[index].text.trim().match(/^#+(?=\s)/)?.[0].length;
|
|
if (index > start && nextHeadingLevel && nextHeadingLevel <= headingLevel) break;
|
|
selected.push(lines[index]);
|
|
}
|
|
}
|
|
|
|
if (file.includeMatching) {
|
|
for (const line of lines) {
|
|
if (file.includeMatching.test(line.text) && !selected.some((item) => item.line === line.line))
|
|
selected.push(line);
|
|
}
|
|
}
|
|
|
|
return selected.toSorted((left, right) => left.line - right.line);
|
|
}
|
|
|
|
function getProseParagraphs(lines) {
|
|
const paragraphs = [];
|
|
let paragraph = null;
|
|
let inFence = false;
|
|
let inFrontmatter = false;
|
|
let inTemplateExport = false;
|
|
let inJsxTag = false;
|
|
|
|
const flush = () => {
|
|
if (paragraph?.text.trim()) paragraphs.push({ ...paragraph, text: paragraph.text.trim() });
|
|
paragraph = null;
|
|
};
|
|
|
|
for (const { text: originalText, line } of lines) {
|
|
const trimmed = originalText.trim();
|
|
|
|
if (line === 1 && trimmed === "---") {
|
|
inFrontmatter = true;
|
|
continue;
|
|
}
|
|
if (inFrontmatter && trimmed === "---") {
|
|
inFrontmatter = false;
|
|
continue;
|
|
}
|
|
if (trimmed.startsWith("```")) {
|
|
flush();
|
|
inFence = !inFence;
|
|
continue;
|
|
}
|
|
if (inFence) continue;
|
|
|
|
if (/^export const .*?=\s*`/.test(trimmed)) inTemplateExport = true;
|
|
if (inTemplateExport) {
|
|
if (/`;\s*$/.test(trimmed)) inTemplateExport = false;
|
|
continue;
|
|
}
|
|
|
|
if (/^(import|export)\s/.test(trimmed)) continue;
|
|
if (trimmed.startsWith("<")) inJsxTag = true;
|
|
if (inJsxTag) {
|
|
if (trimmed.includes(">")) inJsxTag = false;
|
|
continue;
|
|
}
|
|
|
|
let text = trimmed;
|
|
if (inFrontmatter) {
|
|
const match = text.match(/^(title|description|sidebar_label):\s*["']?(.*?)["']?$/);
|
|
if (!match) continue;
|
|
text = match[2];
|
|
}
|
|
|
|
if (!text || /^\|?\s*:?-+:?/.test(text)) {
|
|
flush();
|
|
continue;
|
|
}
|
|
|
|
const isList = /^\s*(?:[-*]|\d+\.)\s+/.test(text);
|
|
text = cleanMarkdown(text);
|
|
if (!text) {
|
|
flush();
|
|
continue;
|
|
}
|
|
|
|
if (isList || text.startsWith("#") || text.startsWith("|")) flush();
|
|
text = text
|
|
.replace(/^#{1,6}\s+/, "")
|
|
.replace(/^\s*(?:[-*]|\d+\.)\s+/, "")
|
|
.replace(/^\|\s*/, "")
|
|
.replace(/\s*\|\s*$/, "");
|
|
|
|
if (!paragraph) paragraph = { text, line, isList };
|
|
else paragraph.text += ` ${text}`;
|
|
|
|
if (isList || trimmed.startsWith("#") || trimmed.startsWith("|")) flush();
|
|
}
|
|
|
|
flush();
|
|
return paragraphs;
|
|
}
|
|
|
|
function cleanMarkdown(text) {
|
|
return text
|
|
.replace(/\[([^\]]+)]\([^\s)]+(?:\s+"[^"]*")?\)/g, "$1")
|
|
.replace(/`[^`]*`/g, " ")
|
|
.replace(/<[^>]+>/g, " ")
|
|
.replace(/[*_~]/g, "")
|
|
.replace(/https?:\/\/\S+/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
function splitSentences(text) {
|
|
const normalized = text.replace(/\b(?:for example|such as)\b/gi, "example");
|
|
const matches = normalized.match(/[^.!?]+[.!?]+|[^.!?]+$/g) ?? [];
|
|
return matches.map((sentence) => sentence.trim()).filter(Boolean);
|
|
}
|
|
|
|
function countWords(sentence) {
|
|
return sentence.match(/[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*/g)?.length ?? 0;
|
|
}
|
|
|
|
function startsWithImperative(sentence) {
|
|
const firstWord = sentence.toLowerCase().match(/[a-z]+/)?.[0];
|
|
return firstWord ? imperativeSet.has(firstWord) : false;
|
|
}
|
|
|
|
function checkTerms(path, line, text) {
|
|
const lowerText = text.toLowerCase();
|
|
for (const spelling of britishSpellings) {
|
|
if (new RegExp(`\\b${spelling}\\b`, "i").test(text)) {
|
|
addIssue(path, line, `Use American English instead of '${spelling}'.`);
|
|
}
|
|
}
|
|
for (const term of nonSteTerms) {
|
|
if (new RegExp(`\\b${term}\\b`, "i").test(lowerText)) {
|
|
addIssue(path, line, `Replace the non-STE term '${term}'.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function addIssue(path, line, message) {
|
|
issues.push(`${path}:${line} ${message}`);
|
|
}
|