oh-my-opencode 5.0.0-beta.89 → 5.0.0-beta.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh-cn.md +1 -1
- package/dist/agents/sisyphus/grok-4.d.ts +3 -2
- package/dist/cli/config-manager/refresh-opencode-plugin-sandbox.d.ts +31 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +11 -0
- package/dist/cli/index.js +1661 -859
- package/dist/cli/install-native/index.d.ts +9 -1
- package/dist/cli/install-native/legacy-omo-bin.d.ts +27 -0
- package/dist/cli/install-native/offer-native-setup.d.ts +31 -0
- package/dist/cli/install-native/omo-bin-test-fixtures.d.ts +35 -0
- package/dist/cli/install-native/plan.d.ts +8 -0
- package/dist/cli/install-native/repair-legacy-omo-bin.d.ts +26 -0
- package/dist/cli/install-native/run-native-install.d.ts +11 -1
- package/dist/cli/install-native/verify-omo-command.d.ts +22 -0
- package/dist/cli/native-edition-hint.d.ts +2 -2
- package/dist/cli-node/index.js +1661 -859
- package/dist/hooks/auto-update-checker/checker/sandbox-refresh.d.ts +51 -0
- package/dist/hooks/auto-update-checker/checker/semver-compare.d.ts +17 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +3 -0
- package/dist/hooks/auto-update-checker/hook/background-update-check.d.ts +8 -0
- package/dist/hooks/auto-update-checker/hook.d.ts +2 -0
- package/dist/hooks/native-edition-nudge/hook.d.ts +1 -1
- package/dist/index.js +1433 -1055
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/opencode-plugin-sandbox.d.ts +54 -0
- package/dist/skills/browser/SKILL.md +8 -1
- package/dist/skills/browser/references/install.md +34 -5
- package/dist/skills/browser/runtime/omowright/index.js +2 -2
- package/dist/skills/browser/runtime/omowright/manifest.json +3 -3
- package/dist/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/dist/skills/browser/scripts/browser-install.mjs +13 -1
- package/dist/skills/ulw-research/AGENTS.md +63 -0
- package/dist/skills/ulw-research/SKILL.md +38 -26
- package/dist/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/dist/skills/ulw-research/references/report-gates.md +66 -0
- package/dist/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/dist/skills/ulw-research/scripts/cli-support.test.ts +88 -0
- package/dist/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/dist/skills/ulw-research/scripts/contracts.test.ts +177 -0
- package/dist/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/dist/skills/ulw-research/scripts/css-lite.test.ts +159 -0
- package/dist/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/dist/skills/ulw-research/scripts/design-spec.test.ts +162 -0
- package/dist/skills/ulw-research/scripts/entities.mjs +39 -0
- package/dist/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/dist/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/dist/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/dist/skills/ulw-research/scripts/format-extract.test.ts +243 -0
- package/dist/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/dist/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/dist/skills/ulw-research/scripts/gates-layout.test.ts +236 -0
- package/dist/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/dist/skills/ulw-research/scripts/gates-static.test.ts +265 -0
- package/dist/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/dist/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/dist/skills/ulw-research/scripts/glossary-vocabulary.test.ts +43 -0
- package/dist/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/dist/skills/ulw-research/scripts/html-lite.test.ts +191 -0
- package/dist/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/dist/skills/ulw-research/scripts/layout-probe.test.ts +89 -0
- package/dist/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/dist/skills/ulw-research/scripts/outcome.test.ts +281 -0
- package/dist/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/dist/skills/ulw-research/scripts/repair-tracker.test.ts +246 -0
- package/dist/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/dist/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/dist/skills/ulw-research/scripts/report-tools.test.ts +213 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/assets/figure-1.svg +1 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/bad-report.html +44 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/boxes.json +90 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/design-spec.md +29 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/good-report.html +57 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/reference-report.html +86 -0
- package/dist/skills/ulw-research/scripts/tests/fixtures/reference.md +23 -0
- package/dist/skills/visual-qa/references/browser-setup.md +3 -1
- package/dist/tui.js +534 -66
- package/package.json +15 -15
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +5 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +7 -3
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +7 -3
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +8 -1
- package/packages/omo-codex/plugin/skills/browser/references/install.md +34 -5
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +2 -2
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +13 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +7 -3
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +63 -0
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -25
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +66 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/entities.mjs +39 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +3 -1
- package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +11 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -2
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +20 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/prompts-core/prompts/ultrawork/codex.md +7 -3
- package/packages/shared-skills/skills/browser/SKILL.md +8 -1
- package/packages/shared-skills/skills/browser/references/install.md +34 -5
- package/packages/shared-skills/skills/browser/runtime/omowright/index.js +2 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +13 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +63 -0
- package/packages/shared-skills/skills/ulw-research/SKILL.md +38 -26
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +66 -0
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +88 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +177 -0
- package/packages/shared-skills/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/packages/shared-skills/skills/ulw-research/scripts/css-lite.test.ts +159 -0
- package/packages/shared-skills/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/packages/shared-skills/skills/ulw-research/scripts/design-spec.test.ts +162 -0
- package/packages/shared-skills/skills/ulw-research/scripts/entities.mjs +39 -0
- package/packages/shared-skills/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract.test.ts +243 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.test.ts +236 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.test.ts +265 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/packages/shared-skills/skills/ulw-research/scripts/glossary-vocabulary.test.ts +43 -0
- package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/packages/shared-skills/skills/ulw-research/scripts/html-lite.test.ts +191 -0
- package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.test.ts +89 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +281 -0
- package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.test.ts +246 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +213 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/assets/figure-1.svg +1 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/bad-report.html +44 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/boxes.json +90 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/design-spec.md +29 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/good-report.html +57 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference-report.html +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference.md +23 -0
- package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +3 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// gates-text.mjs - prose gates: G1 keep-all, G3 emoji, G4 dashes, G5/G6 headings, G7 unsourced numbers.
|
|
2
|
+
//
|
|
3
|
+
// Each gate is { codes, run(ctx) -> Defect[] } over the context built by gates-static.mjs.
|
|
4
|
+
// Plain Node ESM, sibling imports only.
|
|
5
|
+
|
|
6
|
+
import { proseText } from "./html-lite.mjs"
|
|
7
|
+
|
|
8
|
+
const HANGUL = /[\uAC00-\uD7A3]/g
|
|
9
|
+
const KEEP_ALL = /word-break\s*:\s*keep-all/i
|
|
10
|
+
const MIN_HANGUL = 20
|
|
11
|
+
/** Dingbats (2700-27BF) that render as emoji without FE0F (Emoji_Presentation=Yes). */
|
|
12
|
+
const DINGBAT_EMOJI = new Set([0x2705, 0x270a, 0x270b, 0x2728, 0x274c, 0x274e, 0x2753, 0x2754, 0x2755, 0x2757, 0x2795, 0x2796, 0x2797, 0x27b0, 0x27bf])
|
|
13
|
+
const EM_DASH = "\u2014"
|
|
14
|
+
const EN_DASH = "\u2013"
|
|
15
|
+
const HEADING_NUMBER = /^\s*(?:\d+(?:[.-]\d+)*[.):]?|[IVXLC]+[.)]|[A-Z][.)])\s+/
|
|
16
|
+
const GENERIC = new Set(["overview", "introduction", "background", "summary", "conclusion", "conclusions", "results", "discussion", "methodology"])
|
|
17
|
+
const MAX_HEADING_HANGUL = 26
|
|
18
|
+
const MAX_HEADING_WORDS = 12
|
|
19
|
+
const LINEAGE_TOKEN = /\b(?:MEASURED|ASSUMED|DERIVED)\b/
|
|
20
|
+
const NUMBER_CONTAINERS = new Set(["figure", "table", "nav", "header", "footer"])
|
|
21
|
+
const MAX_UNSOURCED = 50
|
|
22
|
+
const MONTH = "(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\\.?"
|
|
23
|
+
/** Stripped before claim detection: dates, versions, figure refs, times, bare years. */
|
|
24
|
+
const NOT_CLAIMS = [
|
|
25
|
+
/\b\d{4}[-./]\d{1,2}(?:[-./]\d{1,2})?\b/g,
|
|
26
|
+
/\b\d{2}\.\d{2}\.\d{2}\b/g,
|
|
27
|
+
/\d{2,4}\s*\uB144\s*\d{1,2}\s*\uC6D4(?:\s*\d{1,2}\s*\uC77C)?/g,
|
|
28
|
+
/\d{1,2}\s*\uC6D4(?:\s*\d{1,2}\s*\uC77C)?/g,
|
|
29
|
+
/(?:19|20)\d{2}\s*\uB144/g,
|
|
30
|
+
new RegExp(`\\b${MONTH}\\s+\\d{1,2}(?:st|nd|rd|th)?(?:,?\\s+\\d{4})?`, "gi"),
|
|
31
|
+
new RegExp(`\\b\\d{1,2}(?:st|nd|rd|th)?\\s+${MONTH}(?:\\s+\\d{4})?`, "gi"),
|
|
32
|
+
/\bv\d+(?:\.\d+)*\b|\b\d+(?:\.\d+){2,}\b/gi,
|
|
33
|
+
/\b(?:version|ver\.?|release)\s*\d+(?:\.\d+)*/gi,
|
|
34
|
+
/\b(?:fig(?:ure)?\.?|table|chart|section|step|phase|wave|part)\s*\d+(?:\.\d+)*/gi,
|
|
35
|
+
/(?:\uADF8\uB9BC|\uB3C4\uD45C|\uD45C|\uBC84\uC804)\s*\d+(?:\.\d+)*/g,
|
|
36
|
+
/\b\d{1,2}:\d{2}\b/g,
|
|
37
|
+
/\b(?:19|20)\d{2}s?\b/g,
|
|
38
|
+
]
|
|
39
|
+
const NUM = "(?<![\\p{L}\\d.,])\\d+(?:[.,]\\d+)*"
|
|
40
|
+
const UNIT =
|
|
41
|
+
"(?:%|\\u2030|\\u00D7|\\uD37C\\uC13C\\uD2B8|\\uB2EC\\uB7EC|\\uC720\\uB85C|\\uC5D4|\\uC6D0|\\uB9CC|\\uC5B5|\\uC870|\\uCC9C|\\uCD08|\\uBD84|\\uC2DC\\uAC04|\\uC77C|\\uC8FC|\\uAC1C\\uC6D4|\\uB144|\\uBA85|\\uAC74|\\uAC1C|\\uD68C|\\uBC88|\\uBC30" +
|
|
42
|
+
"|(?:percent|pp|bps?|x|k|m|b|bn|million|billion|thousand|trillion|usd|krw|eur|ms|s|secs?|seconds?|mins?|minutes?|h|hrs?|hours?|days?|weeks?|months?|years?|kb|mb|gb|tb|users?|people|employees|engineers|tokens?|requests?|stars?|downloads?|customers?)(?![\\p{L}\\d]))"
|
|
43
|
+
const CLAIMS = [
|
|
44
|
+
new RegExp(`[$\\u20AC\\u00A3\\u00A5\\u20A9]\\s?${NUM}`, "u"),
|
|
45
|
+
new RegExp(`${NUM}\\s?${UNIT}`, "iu"),
|
|
46
|
+
/(?<![\d.,])\d{1,3}(?:,\d{3})+(?![\d,])/,
|
|
47
|
+
/(?<![\d.])\d+\.\d+(?![\d.])/,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
function snippet(text, index, span = 24) {
|
|
51
|
+
return text.slice(Math.max(0, index - span), index + span).trim()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Concatenated prose per owning element, in document order; `keep` filters text entries. */
|
|
55
|
+
function ownerTexts(ctx, keep) {
|
|
56
|
+
const byOwner = new Map()
|
|
57
|
+
for (const entry of ctx.texts) {
|
|
58
|
+
if (!keep(entry)) continue
|
|
59
|
+
byOwner.set(entry.owner, (byOwner.get(entry.owner) ?? "") + entry.node.text)
|
|
60
|
+
}
|
|
61
|
+
return [...byOwner].map(([owner, text]) => ({ owner, text: text.replace(/\s+/g, " ") }))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function firstEmoji(text) {
|
|
65
|
+
const chars = [...text]
|
|
66
|
+
for (let i = 0; i < chars.length; i++) {
|
|
67
|
+
const cp = chars[i].codePointAt(0)
|
|
68
|
+
if (chars[i + 1] === "\uFE0F" && cp !== 0xfe0f) return chars[i] + chars[i + 1]
|
|
69
|
+
if ((cp >= 0x1f000 && cp <= 0x1faff) || (cp >= 0x2600 && cp <= 0x26ff) || DINGBAT_EMOJI.has(cp)) return chars[i]
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isDigit = (ch) => ch >= "0" && ch <= "9"
|
|
75
|
+
|
|
76
|
+
/** En dash allowed only when both neighbors (one optional space) are digits. */
|
|
77
|
+
function badEnDash(text) {
|
|
78
|
+
for (let at = text.indexOf(EN_DASH); at !== -1; at = text.indexOf(EN_DASH, at + 1)) {
|
|
79
|
+
const before = text[at - 1] === " " ? text[at - 2] : text[at - 1]
|
|
80
|
+
const after = text[at + 1] === " " ? text[at + 2] : text[at + 1]
|
|
81
|
+
if (!isDigit(before ?? "") || !isDigit(after ?? "")) return at
|
|
82
|
+
}
|
|
83
|
+
return -1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stripNumbering = (text) => text.replace(HEADING_NUMBER, "").trim()
|
|
87
|
+
|
|
88
|
+
function headings(ctx, max) {
|
|
89
|
+
return ctx.elements.filter(({ node }) => /^h[1-6]$/.test(node.tag) && Number(node.tag[1]) <= max).map(({ node }) => node)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Citation markers need no stripping: an element carrying one is skipped before this runs. */
|
|
93
|
+
function claimIn(text) {
|
|
94
|
+
let rest = text
|
|
95
|
+
for (const re of NOT_CLAIMS) rest = rest.replace(re, " ")
|
|
96
|
+
for (const re of CLAIMS) {
|
|
97
|
+
const match = re.exec(rest)
|
|
98
|
+
if (match) return match[0].trim()
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function numberCandidates(ctx) {
|
|
104
|
+
const { closest, classTokens, descendants } = ctx.util
|
|
105
|
+
return ctx.elements.filter(({ node, scope }) => {
|
|
106
|
+
if (node.tag !== "p" && node.tag !== "li") return false
|
|
107
|
+
if (node.tag === "li" && descendants(node).some((child) => child.tag === "p" || child.tag === "li")) return false
|
|
108
|
+
const kind = ctx.kindOf(scope)
|
|
109
|
+
if (kind === "closing" || kind === "sources") return false
|
|
110
|
+
return !closest(node, (cur) => NUMBER_CONTAINERS.has(cur.tag) || classTokens(cur).includes("fig"))
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const textGates = [
|
|
115
|
+
{
|
|
116
|
+
codes: ["korean_no_keep_all"],
|
|
117
|
+
run(ctx) {
|
|
118
|
+
const syllables = ctx.texts.reduce((sum, entry) => sum + (entry.node.text.match(HANGUL)?.length ?? 0), 0)
|
|
119
|
+
if (syllables < MIN_HANGUL) return []
|
|
120
|
+
if ([...ctx.styleBlocks, ...ctx.styleAttrs].some((entry) => KEEP_ALL.test(entry.css))) return []
|
|
121
|
+
return [ctx.defect("korean_no_keep_all", ctx.body, `${syllables} Hangul syllables in prose and no "word-break: keep-all" in any style block or style attribute`)]
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
codes: ["emoji_in_prose"],
|
|
126
|
+
run(ctx) {
|
|
127
|
+
const defects = []
|
|
128
|
+
for (const { owner, text } of ownerTexts(ctx, () => true)) {
|
|
129
|
+
const emoji = firstEmoji(text)
|
|
130
|
+
if (emoji) defects.push(ctx.defect("emoji_in_prose", owner, `emoji "${emoji}" in <${owner.tag}>: "${snippet(text, text.indexOf(emoji))}"`))
|
|
131
|
+
}
|
|
132
|
+
return defects
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
codes: ["em_dash_in_prose", "en_dash_in_prose"],
|
|
137
|
+
run(ctx) {
|
|
138
|
+
const defects = []
|
|
139
|
+
const inScope = (entry) => !entry.cell && ctx.kindOf(entry.scope) !== "sources"
|
|
140
|
+
for (const { owner, text } of ownerTexts(ctx, inScope)) {
|
|
141
|
+
const em = text.indexOf(EM_DASH)
|
|
142
|
+
if (em !== -1) defects.push(ctx.defect("em_dash_in_prose", owner, `em dash "${EM_DASH}" in <${owner.tag}>: "${snippet(text, em)}"`))
|
|
143
|
+
const en = badEnDash(text)
|
|
144
|
+
if (en !== -1) defects.push(ctx.defect("en_dash_in_prose", owner, `en dash "${EN_DASH}" outside a numeric range in <${owner.tag}>: "${snippet(text, en)}"`))
|
|
145
|
+
}
|
|
146
|
+
return defects
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
codes: ["heading_too_long"],
|
|
151
|
+
run(ctx) {
|
|
152
|
+
const defects = []
|
|
153
|
+
for (const node of headings(ctx, 4)) {
|
|
154
|
+
const text = stripNumbering(proseText(node))
|
|
155
|
+
const hangul = /[\uAC00-\uD7A3]/.test(text)
|
|
156
|
+
const size = hangul ? [...text].length : text.split(/\s+/).filter(Boolean).length
|
|
157
|
+
if (size > (hangul ? MAX_HEADING_HANGUL : MAX_HEADING_WORDS)) {
|
|
158
|
+
defects.push(ctx.defect("heading_too_long", node, `<${node.tag}> "${text}" is ${size} ${hangul ? "characters (> 26)" : "words (> 12)"}`))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return defects
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
codes: ["heading_generic"],
|
|
166
|
+
run(ctx) {
|
|
167
|
+
const defects = []
|
|
168
|
+
for (const node of headings(ctx, 3)) {
|
|
169
|
+
const text = stripNumbering(proseText(node))
|
|
170
|
+
if (GENERIC.has(text.toLowerCase().replace(/[.:]+$/, ""))) defects.push(ctx.defect("heading_generic", node, `generic <${node.tag}> "${text}"`))
|
|
171
|
+
}
|
|
172
|
+
return defects
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
codes: ["unsourced_number"],
|
|
177
|
+
run(ctx) {
|
|
178
|
+
const defects = []
|
|
179
|
+
for (const { node } of numberCandidates(ctx)) {
|
|
180
|
+
if (defects.length >= MAX_UNSOURCED) break
|
|
181
|
+
const text = proseText(node)
|
|
182
|
+
if (LINEAGE_TOKEN.test(text) || ctx.util.hasCitation(node)) continue
|
|
183
|
+
if (node.attrs["data-lineage"] !== undefined || ctx.util.descendants(node).some((child) => child.attrs["data-lineage"] !== undefined)) continue
|
|
184
|
+
const claim = claimIn(text)
|
|
185
|
+
if (claim) defects.push(ctx.defect("unsourced_number", node, `number "${claim}" without a lineage tag or citation: "${snippet(text, text.indexOf(claim))}"`))
|
|
186
|
+
}
|
|
187
|
+
return defects
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// html-lite.mjs - a dependency-free HTML tokenizer for report checks.
|
|
2
|
+
//
|
|
3
|
+
// Single forward scan with indexOf (no whole-document regex, no backtracking). Produces a
|
|
4
|
+
// LiteNode tree: { tag, attrs, children, parent, line, text, raw }. Text nodes carry
|
|
5
|
+
// tag "#text"; <style>/<script>/<svg> keep their inner source in `raw` and have no parsed
|
|
6
|
+
// children. The "#document" root also carries `ids` (id -> occurrence count) so
|
|
7
|
+
// selectorPath can tell whether an id is unique. Plain Node ESM, no dependencies.
|
|
8
|
+
|
|
9
|
+
import { decodeEntities } from "./entities.mjs"
|
|
10
|
+
|
|
11
|
+
export { decodeEntities }
|
|
12
|
+
|
|
13
|
+
const VOID_TAGS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"])
|
|
14
|
+
const RAW_TAGS = new Set(["style", "script", "svg"])
|
|
15
|
+
const HEADINGS = ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
16
|
+
const P_CLOSERS = new Set([
|
|
17
|
+
"address", "article", "blockquote", "details", "div", "dl", "fieldset", "figcaption", "figure", "footer",
|
|
18
|
+
"form", "header", "hr", "main", "menu", "nav", "ol", "p", "pre", "section", "table", "ul", ...HEADINGS,
|
|
19
|
+
])
|
|
20
|
+
const P_SCOPE = new Set(["td", "th", "caption", "button", "body", "html"])
|
|
21
|
+
const LI_SCOPE = new Set(["ul", "ol", "menu", "body", "html"])
|
|
22
|
+
const PROSE_SKIP = new Set(["code", "pre", "kbd", "samp", "script", "style", "svg"])
|
|
23
|
+
const SAFE_ID = /^[A-Za-z_][\w-]*$/
|
|
24
|
+
|
|
25
|
+
export const PROSE_TAGS = new Set(["p", "li", ...HEADINGS, "td", "th", "figcaption", "blockquote", "dd", "dt"])
|
|
26
|
+
const PROSE_BREAKS = new Set([...P_CLOSERS, ...PROSE_TAGS, "br", "tr"])
|
|
27
|
+
|
|
28
|
+
function makeNode(tag, attrs, parent, line) {
|
|
29
|
+
return { tag, attrs, children: [], parent, line, text: "", raw: null }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function lineStarts(source) {
|
|
33
|
+
const starts = [0]
|
|
34
|
+
for (let at = source.indexOf("\n"); at !== -1; at = source.indexOf("\n", at + 1)) starts.push(at + 1)
|
|
35
|
+
return starts
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function lineAt(starts, offset) {
|
|
39
|
+
let lo = 0
|
|
40
|
+
let hi = starts.length - 1
|
|
41
|
+
while (lo < hi) {
|
|
42
|
+
const mid = (lo + hi + 1) >> 1
|
|
43
|
+
if (starts[mid] <= offset) lo = mid
|
|
44
|
+
else hi = mid - 1
|
|
45
|
+
}
|
|
46
|
+
return lo + 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isSpace = (ch) => ch === " " || ch === "\n" || ch === "\t" || ch === "\r" || ch === "\f"
|
|
50
|
+
const isLetter = (ch) => (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z")
|
|
51
|
+
|
|
52
|
+
function skipSpaces(source, at) {
|
|
53
|
+
while (at < source.length && isSpace(source[at])) at++
|
|
54
|
+
return at
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read `<name attrs...>` starting just after `<`. */
|
|
58
|
+
function readTag(source, start) {
|
|
59
|
+
const end = source.length
|
|
60
|
+
let at = start
|
|
61
|
+
while (at < end && !isSpace(source[at]) && source[at] !== ">" && source[at] !== "/") at++
|
|
62
|
+
const name = source.slice(start, at).toLowerCase()
|
|
63
|
+
const attrs = {}
|
|
64
|
+
let selfClosing = false
|
|
65
|
+
while (at < end) {
|
|
66
|
+
const ch = source[at]
|
|
67
|
+
if (isSpace(ch)) { at++; continue }
|
|
68
|
+
if (ch === ">") return { name, attrs, end: at + 1, selfClosing }
|
|
69
|
+
selfClosing = ch === "/" && source[at + 1] === ">"
|
|
70
|
+
if (ch === "/") { at++; continue }
|
|
71
|
+
let stop = at + 1
|
|
72
|
+
while (stop < end && !isSpace(source[stop]) && source[stop] !== "/" && source[stop] !== ">" && source[stop] !== "=") stop++
|
|
73
|
+
const key = source.slice(at, stop).toLowerCase()
|
|
74
|
+
at = skipSpaces(source, stop)
|
|
75
|
+
let value = ""
|
|
76
|
+
if (source[at] === "=") {
|
|
77
|
+
at = skipSpaces(source, at + 1)
|
|
78
|
+
const quote = source[at]
|
|
79
|
+
if (quote === '"' || quote === "'") {
|
|
80
|
+
const close = source.indexOf(quote, at + 1)
|
|
81
|
+
const valueEnd = close === -1 ? end : close
|
|
82
|
+
value = source.slice(at + 1, valueEnd)
|
|
83
|
+
at = valueEnd + 1
|
|
84
|
+
} else {
|
|
85
|
+
let valueEnd = at
|
|
86
|
+
while (valueEnd < end && !isSpace(source[valueEnd]) && source[valueEnd] !== ">") valueEnd++
|
|
87
|
+
value = source.slice(at, valueEnd)
|
|
88
|
+
at = valueEnd
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (key !== "__proto__" && !Object.hasOwn(attrs, key)) attrs[key] = decodeEntities(value)
|
|
92
|
+
}
|
|
93
|
+
return { name, attrs, end, selfClosing }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function tagNameAt(source, at, tag) {
|
|
97
|
+
if (source.slice(at, at + tag.length).toLowerCase() !== tag) return false
|
|
98
|
+
const next = source[at + tag.length] ?? ">"
|
|
99
|
+
return isSpace(next) || next === ">" || next === "/"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Find the close of a raw element; nested <svg> is depth-counted. Returns [innerEnd, resumeAt]. */
|
|
103
|
+
function findRawClose(source, from, tag) {
|
|
104
|
+
let depth = 0
|
|
105
|
+
for (let lt = source.indexOf("<", from); lt !== -1; lt = source.indexOf("<", lt + 1)) {
|
|
106
|
+
if (source[lt + 1] === "/" && tagNameAt(source, lt + 2, tag)) {
|
|
107
|
+
if (depth === 0) {
|
|
108
|
+
const gt = source.indexOf(">", lt)
|
|
109
|
+
return [lt, gt === -1 ? source.length : gt + 1]
|
|
110
|
+
}
|
|
111
|
+
depth--
|
|
112
|
+
} else if (tag === "svg" && tagNameAt(source, lt + 1, tag)) {
|
|
113
|
+
depth++
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [source.length, source.length]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Parse an HTML string into a LiteNode tree rooted at "#document"; never throws on malformed input. */
|
|
120
|
+
export function parseHtml(source) {
|
|
121
|
+
const src = String(source)
|
|
122
|
+
const starts = lineStarts(src)
|
|
123
|
+
const root = makeNode("#document", {}, null, 1)
|
|
124
|
+
root.ids = new Map()
|
|
125
|
+
const stack = [root]
|
|
126
|
+
let sawBody = false
|
|
127
|
+
let textStart = 0
|
|
128
|
+
let at = 0
|
|
129
|
+
const top = () => stack[stack.length - 1]
|
|
130
|
+
const flushText = (to) => {
|
|
131
|
+
if (to > textStart) {
|
|
132
|
+
const node = makeNode("#text", {}, top(), lineAt(starts, textStart))
|
|
133
|
+
node.text = decodeEntities(src.slice(textStart, to))
|
|
134
|
+
top().children.push(node)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const closeWithin = (tag, scope) => {
|
|
138
|
+
for (let k = stack.length - 1; k > 0; k--) {
|
|
139
|
+
if (stack[k].tag === tag) { stack.length = k; return }
|
|
140
|
+
if (scope.has(stack[k].tag)) return
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
while (at < src.length) {
|
|
144
|
+
const lt = src.indexOf("<", at)
|
|
145
|
+
if (lt === -1) break
|
|
146
|
+
const next = src[lt + 1] ?? ""
|
|
147
|
+
if (src.startsWith("<!--", lt)) {
|
|
148
|
+
flushText(lt)
|
|
149
|
+
const close = src.indexOf("-->", lt + 4)
|
|
150
|
+
at = textStart = close === -1 ? src.length : close + 3
|
|
151
|
+
} else if (next === "!" || next === "?") {
|
|
152
|
+
flushText(lt)
|
|
153
|
+
const close = src.indexOf(">", lt)
|
|
154
|
+
at = textStart = close === -1 ? src.length : close + 1
|
|
155
|
+
} else if (next === "/" && isLetter(src[lt + 2] ?? "")) {
|
|
156
|
+
flushText(lt)
|
|
157
|
+
const tag = readTag(src, lt + 2)
|
|
158
|
+
if (tag.name !== "body" && tag.name !== "html") {
|
|
159
|
+
for (let k = stack.length - 1; k > 0; k--) {
|
|
160
|
+
if (stack[k].tag === tag.name) { stack.length = k; break }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
at = textStart = tag.end
|
|
164
|
+
} else if (isLetter(next)) {
|
|
165
|
+
flushText(lt)
|
|
166
|
+
const tag = readTag(src, lt + 1)
|
|
167
|
+
at = textStart = tag.end
|
|
168
|
+
if ((tag.name === "html" || tag.name === "body" || tag.name === "head") && stack.some((n) => n.tag === tag.name)) continue
|
|
169
|
+
if (P_CLOSERS.has(tag.name)) closeWithin("p", P_SCOPE)
|
|
170
|
+
if (tag.name === "li") closeWithin("li", LI_SCOPE)
|
|
171
|
+
const node = makeNode(tag.name, tag.attrs, top(), lineAt(starts, lt))
|
|
172
|
+
top().children.push(node)
|
|
173
|
+
if (tag.name === "body") sawBody = true
|
|
174
|
+
if (tag.attrs.id) root.ids.set(tag.attrs.id, (root.ids.get(tag.attrs.id) ?? 0) + 1)
|
|
175
|
+
if (RAW_TAGS.has(tag.name)) {
|
|
176
|
+
node.raw = ""
|
|
177
|
+
if (!tag.selfClosing) {
|
|
178
|
+
const [innerEnd, resume] = findRawClose(src, tag.end, tag.name)
|
|
179
|
+
node.raw = src.slice(tag.end, innerEnd)
|
|
180
|
+
at = textStart = resume
|
|
181
|
+
}
|
|
182
|
+
} else if (!VOID_TAGS.has(tag.name)) {
|
|
183
|
+
stack.push(node)
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
at = lt + 1
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
flushText(src.length)
|
|
190
|
+
if (!sawBody) wrapInBody(root)
|
|
191
|
+
return root
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Fragments without <body>: move everything except <head> under a synthesized body. */
|
|
195
|
+
function wrapInBody(root) {
|
|
196
|
+
const host = root.children.find((child) => child.tag === "html") ?? root
|
|
197
|
+
const kept = host.children.filter((child) => child.tag === "head")
|
|
198
|
+
const moved = host.children.filter((child) => child.tag !== "head")
|
|
199
|
+
const body = makeNode("body", {}, host, moved[0]?.line ?? 1)
|
|
200
|
+
for (const child of moved) child.parent = body
|
|
201
|
+
body.children = moved
|
|
202
|
+
host.children = [...kept, body]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function segment(node) {
|
|
206
|
+
const same = node.parent.children.filter((child) => child.tag === node.tag)
|
|
207
|
+
return same.length > 1 ? `${node.tag}:nth-of-type(${same.indexOf(node) + 1})` : node.tag
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** CSS selector for a node: nearest unique-id anchor, else `body > tag:nth-of-type(k) > ...`. */
|
|
211
|
+
export function selectorPath(node) {
|
|
212
|
+
let root = node
|
|
213
|
+
while (root.parent) root = root.parent
|
|
214
|
+
const ids = root.ids ?? new Map()
|
|
215
|
+
const segments = []
|
|
216
|
+
for (let cur = node.tag === "#text" ? node.parent : node; cur?.parent; cur = cur.parent) {
|
|
217
|
+
const id = cur.attrs.id
|
|
218
|
+
if (id && SAFE_ID.test(id) && ids.get(id) === 1) return [`#${id}`, ...segments].join(" > ")
|
|
219
|
+
if (cur.tag === "body") return ["body", ...segments].join(" > ")
|
|
220
|
+
if (cur.tag === "html") break
|
|
221
|
+
segments.unshift(segment(cur))
|
|
222
|
+
}
|
|
223
|
+
return segments.join(" > ")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Descendant text minus code/pre/kbd/samp/script/style/svg, whitespace collapsed. */
|
|
227
|
+
export function proseText(node) {
|
|
228
|
+
const parts = []
|
|
229
|
+
const walk = (cur) => {
|
|
230
|
+
if (cur.tag === "#text") { parts.push(cur.text); return }
|
|
231
|
+
if (PROSE_SKIP.has(cur.tag)) return
|
|
232
|
+
const breaks = PROSE_BREAKS.has(cur.tag)
|
|
233
|
+
if (breaks) parts.push(" ")
|
|
234
|
+
for (const child of cur.children) walk(child)
|
|
235
|
+
if (breaks) parts.push(" ")
|
|
236
|
+
}
|
|
237
|
+
for (const child of node.children) walk(child)
|
|
238
|
+
return parts.join("").replace(/\s+/g, " ").trim()
|
|
239
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// layout-probe.mjs - the in-page probe the layout gates consume.
|
|
2
|
+
//
|
|
3
|
+
// buildProbeSource() returns ONE async IIFE expression (the engine's evaluate awaits it): it first
|
|
4
|
+
// forces lazy images to load eagerly and awaits their decode (bounded), so natural sizes are real. The orchestrator evaluates it in the rendered
|
|
5
|
+
// report through the browser skill's owned headless engine and writes the result to boxes.json;
|
|
6
|
+
// gates-layout.mjs reads that file. This module never launches a browser.
|
|
7
|
+
//
|
|
8
|
+
// Selector paths use the SAME rule as html-lite.selectorPath (a unique safe #id anchors the path,
|
|
9
|
+
// otherwise tag segments with :nth-of-type(k) only when the parent has more than one child of that
|
|
10
|
+
// tag, prefixed "body"), so a layout defect can be mapped back to the static tree. The rule lives in
|
|
11
|
+
// PATH_FUNCTION_SOURCE, embedded verbatim in the probe and exercised by the parity test.
|
|
12
|
+
|
|
13
|
+
export const PROBE_VERSION = 1
|
|
14
|
+
export const SAMPLED_SELECTOR = "figure, .fig, img, svg, table, h1, h2, h3, p, pre"
|
|
15
|
+
export const EXCLUDED_ANCESTORS = Object.freeze(["svg", "nav", "header", "footer"])
|
|
16
|
+
|
|
17
|
+
/** In-page path rule. idCount(id) returns how many elements carry that id. */
|
|
18
|
+
export const PATH_FUNCTION_SOURCE = `function ulwSelectorPath(el, idCount) {
|
|
19
|
+
const SAFE_ID = /^[A-Za-z_][\\w-]*$/;
|
|
20
|
+
const segments = [];
|
|
21
|
+
for (let cur = el; cur && cur.parentElement; cur = cur.parentElement) {
|
|
22
|
+
const tag = cur.tagName.toLowerCase();
|
|
23
|
+
const id = cur.id;
|
|
24
|
+
if (id && SAFE_ID.test(id) && idCount(id) === 1) return ["#" + id].concat(segments).join(" > ");
|
|
25
|
+
if (tag === "body") return ["body"].concat(segments).join(" > ");
|
|
26
|
+
if (tag === "html") break;
|
|
27
|
+
const same = Array.from(cur.parentElement.children).filter((c) => c.tagName === cur.tagName);
|
|
28
|
+
segments.unshift(same.length > 1 ? tag + ":nth-of-type(" + (same.indexOf(cur) + 1) + ")" : tag);
|
|
29
|
+
}
|
|
30
|
+
return segments.join(" > ");
|
|
31
|
+
}`
|
|
32
|
+
|
|
33
|
+
export class ProbeError extends Error {
|
|
34
|
+
constructor(code, message) {
|
|
35
|
+
super(message)
|
|
36
|
+
this.name = "ProbeError"
|
|
37
|
+
this.code = code
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {{ cap?: number, root?: string }} [options] */
|
|
42
|
+
export function buildProbeSource({ cap = 400, root = "main, article" } = {}) {
|
|
43
|
+
if (!Number.isInteger(cap) || cap <= 0) throw new ProbeError("invalid_cap", `--cap requires a positive integer, got ${cap}`)
|
|
44
|
+
if (typeof root !== "string" || root.trim().length === 0) throw new ProbeError("invalid_root", "--root requires a non-empty CSS selector")
|
|
45
|
+
return `(async () => {
|
|
46
|
+
const CAP = ${cap};
|
|
47
|
+
const IMAGE_WAIT_MS = 5000;
|
|
48
|
+
const ROOT = document.querySelector(${JSON.stringify(root)}) || document.body;
|
|
49
|
+
const EXCLUDED = ${JSON.stringify(EXCLUDED_ANCESTORS)};
|
|
50
|
+
const images = Array.from(document.images);
|
|
51
|
+
for (const img of images) if (img.loading === "lazy") img.loading = "eager";
|
|
52
|
+
const settle = Promise.all(images.map((img) => (img.complete && img.naturalWidth > 0 ? null : img.decode().catch(() => null))));
|
|
53
|
+
await Promise.race([settle, new Promise((resolve) => setTimeout(resolve, IMAGE_WAIT_MS))]);
|
|
54
|
+
${PATH_FUNCTION_SOURCE}
|
|
55
|
+
const idCount = (id) => document.querySelectorAll("#" + CSS.escape(id)).length;
|
|
56
|
+
const path = (el) => ulwSelectorPath(el, idCount);
|
|
57
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
58
|
+
const rect = (el) => { const b = el.getBoundingClientRect(); return { x: r1(b.left + scrollX), y: r1(b.top + scrollY), w: r1(b.width), h: r1(b.height) }; };
|
|
59
|
+
const excluded = (el) => { for (let p = el.parentElement; p; p = p.parentElement) if (EXCLUDED.includes(p.tagName.toLowerCase())) return true; return false; };
|
|
60
|
+
const all = Array.from(ROOT.querySelectorAll(${JSON.stringify(SAMPLED_SELECTOR)})).filter((el) => {
|
|
61
|
+
if (excluded(el)) return false;
|
|
62
|
+
const b = el.getBoundingClientRect();
|
|
63
|
+
return b.width > 0 || b.height > 0;
|
|
64
|
+
});
|
|
65
|
+
const boxes = all.slice(0, CAP).map((el) => {
|
|
66
|
+
const cs = getComputedStyle(el);
|
|
67
|
+
const parent = el.parentElement;
|
|
68
|
+
const box = {
|
|
69
|
+
selector: path(el), tag: el.tagName.toLowerCase(), rect: rect(el),
|
|
70
|
+
parentSelector: parent ? path(parent) : null, parentRect: parent ? rect(parent) : null,
|
|
71
|
+
parentOverflow: parent ? { x: getComputedStyle(parent).overflowX, y: getComputedStyle(parent).overflowY } : null,
|
|
72
|
+
scroll: { sw: el.scrollWidth, cw: el.clientWidth, sh: el.scrollHeight, ch: el.clientHeight },
|
|
73
|
+
overflow: { x: cs.overflowX, y: cs.overflowY }, position: cs.position,
|
|
74
|
+
textLen: (el.textContent || "").trim().length,
|
|
75
|
+
};
|
|
76
|
+
if (el.tagName === "IMG") { box.objectFit = cs.objectFit; box.natural = { w: el.naturalWidth, h: el.naturalHeight }; }
|
|
77
|
+
return box;
|
|
78
|
+
});
|
|
79
|
+
return { probe_version: ${PROBE_VERSION}, viewport: { w: innerWidth, h: innerHeight },
|
|
80
|
+
document: { w: document.documentElement.scrollWidth, h: document.documentElement.scrollHeight },
|
|
81
|
+
count: all.length, truncated: all.length > CAP, boxes };
|
|
82
|
+
})()`
|
|
83
|
+
}
|