@warpgogol/werkstatt-shared 0.14.0 → 0.15.0
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.md +31 -13
- package/README.uk.md +25 -15
- package/package.json +519 -762
- package/src/{share/agent → agent}/ard-catalog.ts +6 -1
- package/src/{share/agent → agent}/dns-aid.test.ts +2 -2
- package/src/{share/agent → agent}/manifest.ts +15 -1
- package/src/{share/agent → agent}/search.ts +6 -1
- package/src/{share → agent}/tests/api-catalog.test.ts +2 -2
- package/src/{share → agent}/tests/ard-catalog.test.ts +2 -2
- package/src/{share → agent}/tests/capability.test.ts +1 -1
- package/src/{share → agent}/tests/fleet-catalog.test.ts +1 -1
- package/src/{share → agent}/tests/knowledge.test.ts +2 -2
- package/src/{share → agent}/tests/manifest.test.ts +1 -1
- package/src/{share → agent}/tests/mcp-card.test.ts +2 -2
- package/src/agent/tests/omit-empty.test.ts +33 -0
- package/src/{share → agent}/tests/openapi.test.ts +2 -2
- package/src/{share → agent}/tests/proof.test.ts +1 -1
- package/src/{share → attribution}/material-credits.ts +4 -4
- package/src/{share → checks}/check-hints.ts +6 -1
- package/src/checks/index.ts +4 -1
- package/src/checks/lib/astro-site-url.ts +6 -1
- package/src/checks/lib/i18n.ts +14 -0
- package/src/checks/result-helpers.ts +22 -5
- package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
- package/src/{share → client-scripts}/counter-utils.ts +6 -2
- package/src/{share/scripts → client-scripts}/external-link-qr.ts +11 -16
- package/src/{share/scripts → client-scripts}/external-links.ts +12 -1
- package/src/{share/scripts → client-scripts}/gsap-counter.ts +8 -3
- package/src/client-scripts/index.ts +35 -0
- package/src/{share/scripts → client-scripts}/inline-number-animation.ts +1 -1
- package/src/{share/scripts → client-scripts}/lenis.ts +6 -1
- package/src/{share/scripts → client-scripts}/lordicon.ts +6 -1
- package/src/{share/scripts → client-scripts}/orchestrator.ts +10 -1
- package/src/{share/scripts → client-scripts}/scheduler.ts +6 -1
- package/src/client-scripts/scroll-spy.ts +106 -0
- package/src/client-scripts/tests/external-link-qr.test.ts +8 -0
- package/src/client-scripts/tests/external-links.test.ts +25 -0
- package/src/client-scripts/tests/gsap-counter.test.ts +8 -0
- package/src/client-scripts/tests/gsap-mountain-journey.test.ts +8 -0
- package/src/client-scripts/tests/gsap-parallax.test.ts +8 -0
- package/src/client-scripts/tests/gsap-reveal.test.ts +8 -0
- package/src/client-scripts/tests/gsap-stagger.test.ts +8 -0
- package/src/client-scripts/tests/inline-number-animation.test.ts +8 -0
- package/src/client-scripts/tests/lenis.test.ts +8 -0
- package/src/client-scripts/tests/live-photos.test.ts +8 -0
- package/src/client-scripts/tests/lordicon.test.ts +8 -0
- package/src/client-scripts/tests/orchestrator.test.ts +47 -0
- package/src/client-scripts/tests/scheduler.test.ts +23 -0
- package/src/client-scripts/tests/scroll-spy.test.ts +204 -0
- package/src/{share/scripts → client-scripts/tests}/video-player.test.ts +51 -31
- package/src/component/contracts.ts +206 -0
- package/src/component/index.ts +11 -0
- package/src/{share → content}/content-reference.ts +3 -3
- package/src/{share/content → content}/entity-id.ts +6 -3
- package/src/content/index.ts +3 -0
- package/src/{share/content → content}/merge.ts +6 -1
- package/src/content/system-manifest.test.ts +107 -0
- package/src/content/system-manifest.ts +12 -94
- package/src/{share → content}/tests/resolve-field-path.test.ts +1 -1
- package/src/{share → content}/tests/substitute-deep.test.ts +1 -1
- package/src/content-discipline/index.ts +19 -0
- package/src/{share/content-discipline → content-discipline}/parsers.ts +2 -2
- package/src/{share/env.d.ts → env.d.ts} +6 -3
- package/src/fingerprint/canonical-json.ts +731 -0
- package/src/fingerprint/index.ts +12 -0
- package/src/fingerprint/primitives.ts +72 -0
- package/src/{share/i18n → i18n}/localization.ts +6 -2
- package/src/integration/adapters.ts +10 -71
- package/src/integration/dispatch.ts +1 -1
- package/src/integration/funnel.ts +6 -1
- package/src/integration/index.ts +101 -4
- package/src/integration/lagebild-destination-adapter.ts +108 -0
- package/src/integration/lagebild-ingress.ts +7 -12
- package/src/integration/orchestration.ts +11 -10
- package/src/integration/tests/deliver-event.test.ts +1 -1
- package/src/integration/tests/dispatch.test.ts +4 -4
- package/src/integration/tests/hub-runtime.test.ts +22 -14
- package/src/integration/vitest.config.ts +8 -0
- package/src/kernel/desired-state.ts +227 -0
- package/src/kernel/diagnostic.ts +289 -0
- package/src/kernel/fs-atomic.ts +131 -0
- package/src/kernel/index.ts +15 -0
- package/src/kernel/tests/fs-atomic.test.ts +150 -0
- package/src/kernel/tests/workspace-io.test.ts +187 -0
- package/src/kernel/types.ts +459 -0
- package/src/kernel/workspace-io.ts +248 -0
- package/src/{share → knowledge}/tests/claim.test.ts +1 -1
- package/src/{share → knowledge}/tests/derivation.test.ts +1 -1
- package/src/{share → knowledge}/tests/freshness.test.ts +1 -1
- package/src/{share → knowledge}/tests/ledger.test.ts +1 -1
- package/src/{share → knowledge}/tests/plan.test.ts +1 -1
- package/src/{share → knowledge}/tests/source.test.ts +1 -1
- package/src/legal/index.ts +15 -0
- package/src/{share/middleware → middleware}/access-protection.ts +8 -8
- package/src/{share → node}/dev-props-validator.ts +6 -1
- package/src/{share → node}/import-scan.ts +23 -3
- package/src/{share → node}/semantic/derived-prices-loader.ts +4 -2
- package/src/{share → node}/semantic/markdown-twin-provenance.ts +1 -1
- package/src/node/tests/derived-prices-loader.test.ts +59 -0
- package/src/node/tests/markdown-twin-provenance.test.ts +152 -0
- package/src/observability/env-resolver.ts +6 -1
- package/src/observability/index.ts +6 -1
- package/src/observability/otlp-converter.ts +6 -1
- package/src/observability/otlp-transport.ts +6 -1
- package/src/observability/pusher.ts +6 -1
- package/src/observability/redact.ts +6 -1
- package/src/observability/typed-refs.ts +6 -2
- package/src/observability/vitest.config.ts +1 -0
- package/src/{share → offers}/formula-eval.ts +3 -3
- package/src/{share → offers}/tests/formula-eval.test.ts +2 -2
- package/src/onboarding/index.ts +1 -0
- package/src/{share → onboarding}/tests/onboarding-yaml.test.ts +1 -1
- package/src/ontology/archetype-registry.ts +6 -1
- package/src/ontology/archetypes/index.json +33 -1
- package/src/ontology/archetypes/index.yaml +27 -1
- package/src/ontology/archetypes/sections/entity-grid.yaml +46 -0
- package/src/ontology/archetypes/sections/exit-diagram.yaml +43 -0
- package/src/ontology/biome-token-projection.ts +6 -1
- package/src/ontology/cosmic/index.ts +6 -1
- package/src/ontology/cosmic/moon-catalog.ts +6 -1
- package/src/ontology/cosmic/planet-catalog.ts +6 -1
- package/src/ontology/cosmic/star-catalog.ts +6 -1
- package/src/ontology/enums.ts +6 -1
- package/src/ontology/external-surfaces/index.ts +1 -1
- package/src/ontology/index.ts +6 -2
- package/src/ontology/manifest.ts +26 -4
- package/src/ontology/operations/artifact-store.ts +45 -0
- package/src/ontology/operations/dht.ts +98 -0
- package/src/ontology/operations/handoff.ts +92 -0
- package/src/ontology/operations/index.ts +187 -1
- package/src/ontology/operations/leitstand.ts +99 -0
- package/src/ontology/operations/materialization.ts +90 -0
- package/src/ontology/operations/mission.ts +111 -0
- package/src/ontology/operations/naming-policy.ts +132 -0
- package/src/ontology/operations/notausgang.ts +64 -0
- package/src/ontology/operations/release.ts +121 -0
- package/src/ontology/operations/sternsystem.ts +192 -0
- package/src/ontology/operations/werkstatt.ts +48 -0
- package/src/ontology/schemas/biome.ts +7 -2
- package/src/ontology/schemas/capability.ts +1 -1
- package/src/ontology/schemas/constellation.ts +6 -1
- package/src/ontology/schemas/index.ts +6 -2
- package/src/ontology/schemas/manifest-resolver.ts +6 -1
- package/src/ontology/schemas/page-entry.ts +9 -5
- package/src/ontology/schemas/system/growth.ts +6 -1
- package/src/ontology/schemas/system/manifest.ts +40 -1
- package/src/ontology/schemas/system/page-output.ts +7 -6
- package/src/ontology/schemas/system/tests/growth.test.ts +22 -0
- package/src/ontology/schemas/system/tests/integrations.test.ts +12 -0
- package/src/ontology/schemas/system/tests/manifest.test.ts +22 -0
- package/src/ontology/schemas/system/tests/page-output.test.ts +13 -0
- package/src/ontology/schemas/system/tests/release.test.ts +22 -0
- package/src/ontology/schemas/system/tests/text.test.ts +18 -0
- package/src/ontology/schemas/system/tests/verification.test.ts +12 -0
- package/src/ontology/schemas/system/text.ts +15 -0
- package/src/ontology/schemas/system.ts +6 -2
- package/src/ontology/schemas/tests/biome.test.ts +19 -0
- package/src/ontology/schemas/tests/capability.test.ts +22 -0
- package/src/ontology/schemas/tests/constellation.test.ts +22 -0
- package/src/ontology/schemas/tests/dns-records.test.ts +13 -0
- package/src/ontology/schemas/tests/manifest-resolver.test.ts +13 -0
- package/src/ontology/schemas/tests/page-entry.test.ts +22 -0
- package/src/ontology/schemas/tests/section-archetype.test.ts +12 -0
- package/src/ontology/schemas/tests/site-family.test.ts +18 -0
- package/src/ontology/schemas/tests/system.test.ts +20 -0
- package/src/ontology/shared-section-props/body-fragments.ts +2 -0
- package/src/ontology/tests/sternsystem-owner.test.ts +1 -1
- package/src/ontology/vitest.config.ts +1 -0
- package/src/{share → page}/page.ts +13 -9
- package/src/{share → page}/tests/dev-props-validator.test.ts +1 -1
- package/src/{share → page}/tests/hero-section-numbering-parity.test.ts +1 -1
- package/src/passport/index.ts +6 -1
- package/src/passport/key-rotate.ts +10 -2
- package/src/passport/schema.ts +6 -1
- package/src/passport/sign.ts +8 -3
- package/src/passport/vitest.config.ts +1 -0
- package/src/pbt/index.test.ts +95 -0
- package/src/pbt/index.ts +251 -0
- package/src/plugin/plugin-contract.ts +1 -1
- package/src/{share → policy}/entitlement.ts +24 -4
- package/src/{share → policy}/feature-policy.ts +8 -3
- package/src/{share → policy}/visibility.ts +7 -2
- package/src/redirects/redirects.ts +117 -0
- package/src/{share → redirects/tests}/redirects.test.ts +1 -1
- package/src/remediation/remediation-catalog.ts +316 -0
- package/src/{share/routes → routes}/template-filter.ts +1 -1
- package/src/{share → runtime}/runtime-context.ts +7 -2
- package/src/{share → runtime}/shared-context.ts +1 -1
- package/src/{share/schemas → schemas}/claims.ts +2 -2
- package/src/{share/schemas → schemas}/effects.ts +6 -1
- package/src/{share/schemas → schemas}/features.ts +1 -1
- package/src/{share/schemas → schemas}/index.ts +3 -3
- package/src/{share/schemas → schemas}/navigation.ts +6 -1
- package/src/{share/schemas → schemas}/page-base.ts +6 -2
- package/src/{share/schemas → schemas}/person.ts +1 -1
- package/src/{share/schemas → schemas}/standard-list-item.ts +1 -1
- package/src/schemas/tests/claim-records.test.ts +52 -0
- package/src/schemas/tests/claims.test.ts +23 -0
- package/src/schemas/tests/effects.test.ts +116 -0
- package/src/schemas/tests/features.test.ts +61 -0
- package/src/schemas/tests/horizontal-align.test.ts +16 -0
- package/src/schemas/tests/icon-color.test.ts +16 -0
- package/src/schemas/tests/live-photo.test.ts +40 -0
- package/src/schemas/tests/material-credit.test.ts +51 -0
- package/src/schemas/tests/media.test.ts +26 -0
- package/src/schemas/tests/navigation.test.ts +58 -0
- package/src/schemas/tests/page-base.test.ts +19 -0
- package/src/schemas/tests/participant.test.ts +26 -0
- package/src/schemas/tests/person.test.ts +12 -0
- package/src/schemas/tests/print.test.ts +30 -0
- package/src/schemas/tests/section-background.test.ts +31 -0
- package/src/schemas/tests/section-body.test.ts +16 -0
- package/src/schemas/tests/section-cards.test.ts +17 -0
- package/src/schemas/tests/section-cta.test.ts +41 -0
- package/src/schemas/tests/section-header.test.ts +31 -0
- package/src/schemas/tests/section-image.test.ts +24 -0
- package/src/schemas/tests/section-motion.test.ts +29 -0
- package/src/schemas/tests/section-shell.test.ts +42 -0
- package/src/schemas/tests/section-stats.test.ts +17 -0
- package/src/schemas/tests/site-background.test.ts +33 -0
- package/src/schemas/tests/standard-list-item.test.ts +16 -0
- package/src/{share → sections}/rfc0042-utils.ts +1 -1
- package/src/{share/semantic → semantic}/block-extractors/index.ts +20 -1
- package/src/{share/semantic → semantic}/business-projection.ts +95 -1
- package/src/semantic/canonical-uri.ts +54 -0
- package/src/{share/semantic → semantic}/extract.ts +100 -1
- package/src/semantic/fact-extraction.test.ts +47 -0
- package/src/semantic/fact-extraction.ts +46 -0
- package/src/semantic/freshness.ts +67 -0
- package/src/{share/semantic → semantic}/ids.ts +6 -1
- package/src/{share/semantic → semantic}/index.ts +9 -2
- package/src/{share/semantic → semantic}/jsonld/breadcrumb.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/context.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/faq.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/initiative.ts +6 -1
- package/src/{share/semantic → semantic}/jsonld/organization.ts +6 -1
- package/src/{share/semantic → semantic}/jsonld/person.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/service.ts +6 -2
- package/src/semantic/jsonld/shared.ts +36 -0
- package/src/semantic/jsonld/tests/article.test.ts +89 -0
- package/src/semantic/jsonld/tests/breadcrumb.test.ts +62 -0
- package/src/semantic/jsonld/tests/collection-list.test.ts +39 -0
- package/src/semantic/jsonld/tests/faq.test.ts +60 -0
- package/src/semantic/jsonld/tests/initiative.test.ts +56 -0
- package/src/semantic/jsonld/tests/organization.test.ts +123 -0
- package/src/semantic/jsonld/tests/person.test.ts +89 -0
- package/src/semantic/jsonld/tests/service.test.ts +65 -0
- package/src/semantic/jsonld/tests/shared.test.ts +41 -0
- package/src/semantic/jsonld/tests/video.test.ts +84 -0
- package/src/semantic/jsonld/tests/webpage.test.ts +93 -0
- package/src/semantic/jsonld/tests/website.test.ts +27 -0
- package/src/semantic/jsonld/types.ts +24 -0
- package/src/{share/semantic → semantic}/jsonld/webpage.ts +6 -1
- package/src/semantic/jsonld/website.ts +32 -0
- package/src/{share/semantic → semantic}/jsonld.ts +6 -1
- package/src/{share/semantic → semantic}/llms.ts +41 -3
- package/src/{share/semantic → semantic}/models.ts +27 -38
- package/src/{share/semantic → semantic}/organization-profile.ts +3 -0
- package/src/{share/semantic → semantic}/page-builders/markdown-page.ts +6 -3
- package/src/{share/semantic → semantic}/page-utils.ts +6 -3
- package/src/semantic/tests/ai.test.ts +55 -0
- package/src/semantic/tests/block-extraction.test.ts +34 -0
- package/src/{share/tests/breadcrumbs.test.ts → semantic/tests/breadcrumbs-0229.test.ts} +1 -1
- package/src/semantic/tests/breadcrumbs.test.ts +127 -0
- package/src/{share → semantic}/tests/build-page-price-markers.test.ts +2 -2
- package/src/semantic/tests/build-page.test.ts +122 -0
- package/src/semantic/tests/business-projection.test.ts +367 -0
- package/src/semantic/tests/canonical-uri.test.ts +56 -0
- package/src/semantic/tests/extract.test.ts +209 -0
- package/src/semantic/tests/feed.test.ts +113 -0
- package/src/semantic/tests/freshness.pbt.test.ts +108 -0
- package/src/semantic/tests/helpers.ts +32 -0
- package/src/semantic/tests/ids.test.ts +118 -0
- package/src/{share/tests/image-sitemap.test.ts → semantic/tests/image-sitemap-0150.test.ts} +1 -1
- package/src/semantic/tests/image-sitemap.test.ts +109 -0
- package/src/{share → semantic}/tests/jsonld-service.test.ts +2 -2
- package/src/{share → semantic}/tests/jsonld-video.test.ts +3 -3
- package/src/{share → semantic}/tests/jsonld-webpage.test.ts +3 -3
- package/src/semantic/tests/jsonld.test.ts +61 -0
- package/src/{share → semantic}/tests/llms-0789.test.ts +2 -2
- package/src/semantic/tests/llms-policy.test.ts +60 -0
- package/src/semantic/tests/llms.test.ts +164 -0
- package/src/semantic/tests/markdown-hygiene.test.ts +72 -0
- package/src/semantic/tests/markdown-page.test.ts +97 -0
- package/src/{share → semantic}/tests/organization-jsonld.test.ts +3 -3
- package/src/semantic/tests/organization-profile.test.ts +107 -0
- package/src/{share/tests/output-projection.test.ts → semantic/tests/output-projection-0143.test.ts} +1 -1
- package/src/semantic/tests/output-projection.test.ts +77 -0
- package/src/semantic/tests/page-markdown.test.ts +98 -0
- package/src/semantic/tests/page-utils.test.ts +88 -0
- package/src/{share/tests/price-marker-resolver.test.ts → semantic/tests/price-marker-resolver-0767.test.ts} +2 -2
- package/src/semantic/tests/price-marker-resolver.test.ts +90 -0
- package/src/semantic/tests/project-claims.pbt.test.ts +60 -0
- package/src/semantic/tests/robots.test.ts +70 -0
- package/src/{share/semantic → semantic}/tests/split-sentences.test.ts +1 -1
- package/src/{share → semantic}/tests/twin-path.test.ts +1 -1
- package/src/semantic/tests/update-stamp.test.ts +102 -0
- package/src/signing/index.ts +13 -0
- package/src/signing/key.ts +139 -0
- package/src/signing/sign.ts +66 -0
- package/src/signing/types.ts +29 -0
- package/src/{share/slug → slug}/slug-url.ts +4 -1
- package/src/stack/run-tool.ts +108 -0
- package/src/stack/stack-checks.ts +124 -0
- package/src/stack/tests/run-tool.test.ts +138 -0
- package/src/stack/tests/stack-checks.test.ts +141 -0
- package/src/stack/tests/walk-files.test.ts +131 -0
- package/src/stack/walk-files.ts +114 -0
- package/src/surface/blueprint.ts +342 -66
- package/src/surface/decision-composer.ts +6 -1
- package/src/surface/geo.ts +6 -1
- package/src/surface/index.ts +11 -7
- package/src/surface/types.ts +2 -2
- package/src/surface/vitest.config.ts +1 -0
- package/src/tests/signing-core.test.ts +129 -0
- package/src/{share → text}/css-value-normalize.ts +6 -1
- package/src/text/string-utils.ts +24 -0
- package/src/{share → text}/tests/text-normalize.test.ts +5 -0
- package/src/{share → text}/text-normalize.ts +4 -3
- package/src/{share → text}/wrap-inline-numbers.ts +1 -1
- package/src/types/axiom-study.d.ts +36 -0
- package/src/types/hls-js-light.d.ts +21 -0
- package/src/types/qrcode-browser.d.ts +23 -0
- package/src/typography/fix.pbt.test.ts +208 -0
- package/src/typography/fix.ts +148 -0
- package/src/typography/index.ts +54 -0
- package/src/typography/locale-defaults.ts +97 -0
- package/src/typography/rules-tier1.test.ts +459 -0
- package/src/typography/rules-tier1.ts +925 -0
- package/src/typography/rules-tier2-locale.pbt.test.ts +190 -0
- package/src/typography/rules-tier2-locale.test.ts +286 -0
- package/src/typography/rules-tier2-locale.ts +399 -0
- package/src/typography/rules-tier2-structure.test.ts +330 -0
- package/src/typography/rules-tier2-structure.ts +378 -0
- package/src/typography/rules-tier3-advisory.test.ts +290 -0
- package/src/typography/rules-tier3-advisory.ts +239 -0
- package/src/typography/text-surface.test.ts +244 -0
- package/src/typography/text-surface.ts +448 -0
- package/src/index.ts +0 -14
- package/src/integration/port-barrel.ts +0 -105
- package/src/share/content/index.ts +0 -16
- package/src/share/content-discipline/index.ts +0 -14
- package/src/share/index.ts +0 -86
- package/src/share/legal/index.ts +0 -1
- package/src/share/redirects.ts +0 -54
- package/src/share/scripts/index.ts +0 -26
- package/src/share/semantic/jsonld/shared.ts +0 -31
- package/src/share/semantic/jsonld/types.ts +0 -18
- package/src/share/semantic/jsonld/website.ts +0 -27
- package/src/share/string-utils.ts +0 -19
- package/src/share/types/axiom-study.d.ts +0 -22
- package/src/share/types/hls-js-light.d.ts +0 -16
- package/src/share/types/qrcode-browser.d.ts +0 -18
- package/src/share/utility-registry.yaml +0 -36
- package/src/share/vitest.config.ts +0 -8
- package/src/surface/blueprint-schema.ts +0 -314
- package/src/surface/blueprint-types.ts +0 -256
- /package/src/{share/agent → agent}/api-catalog.ts +0 -0
- /package/src/{share/agent → agent}/capability.ts +0 -0
- /package/src/{share/agent → agent}/dns-aid.ts +0 -0
- /package/src/{share/agent → agent}/fleet-catalog.ts +0 -0
- /package/src/{share/agent → agent}/index.ts +0 -0
- /package/src/{share/agent → agent}/knowledge.ts +0 -0
- /package/src/{share/agent → agent}/mcp-card.ts +0 -0
- /package/src/{share/agent → agent}/omit-empty.ts +0 -0
- /package/src/{share/agent → agent}/openapi.ts +0 -0
- /package/src/{share/agent → agent}/proof.ts +0 -0
- /package/src/{share/agent → agent}/search.test.ts +0 -0
- /package/src/{share → attribution}/attribution-display.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-mountain-journey.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-parallax.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-reveal.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-stagger.ts +0 -0
- /package/src/{share/scripts → client-scripts}/live-photos.ts +0 -0
- /package/src/{share/scripts → client-scripts}/video-player.ts +0 -0
- /package/src/{share/content → content}/resolve-field-path.ts +0 -0
- /package/src/{share/content → content}/substitute-deep.ts +0 -0
- /package/src/{share → content}/tests/content-ref-index.test.ts +0 -0
- /package/src/{share/content-discipline → content-discipline}/types.ts +0 -0
- /package/src/{share → image-provider}/image-provider.ts +0 -0
- /package/src/{share/knowledge → knowledge}/claim.ts +0 -0
- /package/src/{share/knowledge → knowledge}/derivation.ts +0 -0
- /package/src/{share/knowledge → knowledge}/freshness.ts +0 -0
- /package/src/{share/knowledge → knowledge}/index.ts +0 -0
- /package/src/{share/knowledge → knowledge}/ledger.ts +0 -0
- /package/src/{share/knowledge → knowledge}/plan.ts +0 -0
- /package/src/{share/knowledge → knowledge}/source.ts +0 -0
- /package/src/{share/legal → legal}/translation-policy.ts +0 -0
- /package/src/{share/middleware → middleware}/language-redirect.ts +0 -0
- /package/src/{share/middleware → middleware}/tests/access-protection.test.ts +0 -0
- /package/src/{share → node}/fs/index.ts +0 -0
- /package/src/{share/tests/markdown-twin-provenance.test.ts → node/tests/markdown-twin-provenance-0613.test.ts} +0 -0
- /package/src/{share → offers}/offer-capacity.ts +0 -0
- /package/src/{share/onboarding-yaml/index.ts → onboarding/yaml.ts} +0 -0
- /package/src/{share → ontology}/tests/ontology-role-by-cosmic-name.test.ts +0 -0
- /package/src/{share/routes → routes}/tests/template-filter.test.ts +0 -0
- /package/src/{share/schemas → schemas}/claim-records.ts +0 -0
- /package/src/{share/schemas → schemas}/horizontal-align.ts +0 -0
- /package/src/{share/schemas → schemas}/icon-color.ts +0 -0
- /package/src/{share/schemas → schemas}/live-photo.ts +0 -0
- /package/src/{share/schemas → schemas}/material-credit.ts +0 -0
- /package/src/{share/schemas → schemas}/media.ts +0 -0
- /package/src/{share/schemas → schemas}/participant.ts +0 -0
- /package/src/{share/schemas → schemas}/print.ts +0 -0
- /package/src/{share/schemas → schemas}/section-background.ts +0 -0
- /package/src/{share/schemas → schemas}/section-body.ts +0 -0
- /package/src/{share/schemas → schemas}/section-cards.ts +0 -0
- /package/src/{share/schemas → schemas}/section-cta.ts +0 -0
- /package/src/{share/schemas → schemas}/section-header.ts +0 -0
- /package/src/{share/schemas → schemas}/section-image.ts +0 -0
- /package/src/{share/schemas → schemas}/section-motion.ts +0 -0
- /package/src/{share/schemas → schemas}/section-shell.ts +0 -0
- /package/src/{share/schemas → schemas}/section-stats.ts +0 -0
- /package/src/{share/schemas → schemas}/site-background.ts +0 -0
- /package/src/{share/semantic → semantic}/ai.ts +0 -0
- /package/src/{share/semantic → semantic}/block-extraction.ts +0 -0
- /package/src/{share/semantic → semantic}/breadcrumbs.ts +0 -0
- /package/src/{share/semantic → semantic}/build-page.ts +0 -0
- /package/src/{share/semantic → semantic}/feed.ts +0 -0
- /package/src/{share/semantic → semantic}/image-sitemap.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/article.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/collection-list.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/video.ts +0 -0
- /package/src/{share/semantic → semantic}/llms-policy.ts +0 -0
- /package/src/{share/semantic → semantic}/markdown-hygiene.ts +0 -0
- /package/src/{share/semantic → semantic}/output-projection.ts +0 -0
- /package/src/{share/semantic → semantic}/page-markdown.ts +0 -0
- /package/src/{share/semantic → semantic}/price-marker-resolver.ts +0 -0
- /package/src/{share/semantic → semantic}/robots.ts +0 -0
- /package/src/{share/semantic → semantic}/update-stamp.ts +0 -0
- /package/src/{share/slug → slug}/heading-slugger.ts +0 -0
- /package/src/{share/slug → slug}/index.ts +0 -0
- /package/src/{share/slug → slug}/slug-id.ts +0 -0
- /package/src/{share/slug → slug}/strategies.ts +0 -0
- /package/src/{share/slug → slug}/tests/slug.test.ts +0 -0
- /package/src/{share → text}/tests/text-normalize.pbt.test.ts +0 -0
- /package/src/{share → text}/text-position.ts +0 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Tier 1 typography rule engine (RFC-1068). Implements 14 rules across
|
|
4
|
+
5 families: PUNCT (4), SPACE (4), CASE (2), LOCALE (3), YAML (2). All regexes
|
|
5
|
+
use the `u` flag and Unicode property escapes. Rules operate on TextSegment
|
|
6
|
+
objects produced by the text-surface extractor, never on raw file bytes.
|
|
7
|
+
TypographyRuleId and TypographyRuleFamily include Tier 2 families (HEAD, PAIR,
|
|
8
|
+
MD per RFC-1069; NUM, ABBR, APOS per RFC-1070) and Tier 3 families (UNICODE, LINK,
|
|
9
|
+
SENT per RFC-1071) so that Tier 2 and Tier 3 rule arrays can be typed as
|
|
10
|
+
TypographyRule[].</purpose>
|
|
11
|
+
<non-goals>
|
|
12
|
+
<item>Do not extract text — that is text-surface.ts.</item>
|
|
13
|
+
<item>Do not handle file I/O or command registration — that is the command adapter.</item>
|
|
14
|
+
<item>Do not auto-fix — autofix is RFC-1072.</item>
|
|
15
|
+
</non-goals>
|
|
16
|
+
</MODULE_CONTRACT>
|
|
17
|
+
<CHANGE_SUMMARY>
|
|
18
|
+
<item>RFC-1068: initial creation with 14 Tier 1 rules.</item>
|
|
19
|
+
<item>RFC-1069: extended TypographyRuleId and TypographyRuleFamily with HEAD, PAIR, MD families.</item>
|
|
20
|
+
<item>RFC-1070: extended TypographyRuleId and TypographyRuleFamily with NUM, ABBR, APOS families; added localeDefaults to TypographyContext.</item>
|
|
21
|
+
<item>RFC-1071: extended TypographyRuleId and TypographyRuleFamily with UNICODE, LINK, SENT families.</item>
|
|
22
|
+
<item>RFC-1072: changed TypographyFinding.fix from null to FixAction | null; extended finding() helper with optional FixAction param; populated fix on PUNCT-01, SPACE-01, SPACE-02.</item>
|
|
23
|
+
</CHANGE_SUMMARY>
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { TextSegment } from "./text-surface.ts";
|
|
27
|
+
import type { FixAction } from "./fix.ts";
|
|
28
|
+
import {
|
|
29
|
+
LOCALE_DEFAULTS,
|
|
30
|
+
getLocaleDefaults,
|
|
31
|
+
type LocaleTypographyDefaults,
|
|
32
|
+
} from "./locale-defaults.ts";
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Types (from RFC-1068)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export type TypographyRuleId = `TYPO-${
|
|
39
|
+
| "PUNCT"
|
|
40
|
+
| "SPACE"
|
|
41
|
+
| "CASE"
|
|
42
|
+
| "LOCALE"
|
|
43
|
+
| "YAML"
|
|
44
|
+
| "HEAD"
|
|
45
|
+
| "PAIR"
|
|
46
|
+
| "MD"
|
|
47
|
+
| "NUM"
|
|
48
|
+
| "ABBR"
|
|
49
|
+
| "APOS"
|
|
50
|
+
| "UNICODE"
|
|
51
|
+
| "LINK"
|
|
52
|
+
| "SENT"}-${string}`;
|
|
53
|
+
|
|
54
|
+
export type TypographyRuleFamily =
|
|
55
|
+
| "PUNCT"
|
|
56
|
+
| "SPACE"
|
|
57
|
+
| "CASE"
|
|
58
|
+
| "LOCALE"
|
|
59
|
+
| "YAML"
|
|
60
|
+
| "HEAD"
|
|
61
|
+
| "PAIR"
|
|
62
|
+
| "MD"
|
|
63
|
+
| "NUM"
|
|
64
|
+
| "ABBR"
|
|
65
|
+
| "APOS"
|
|
66
|
+
| "UNICODE"
|
|
67
|
+
| "LINK"
|
|
68
|
+
| "SENT";
|
|
69
|
+
|
|
70
|
+
export interface TypographyFinding {
|
|
71
|
+
ruleId: TypographyRuleId;
|
|
72
|
+
segment: TextSegment;
|
|
73
|
+
/** 0-based column inside segment.text. */
|
|
74
|
+
column: number;
|
|
75
|
+
/** Offending substring (for messages and fix). */
|
|
76
|
+
match: string;
|
|
77
|
+
message: string;
|
|
78
|
+
fixHint: string;
|
|
79
|
+
/** Set by rules whitelisted in RFC-1072; null for non-fixable rules. */
|
|
80
|
+
fix: FixAction | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TypographyRule {
|
|
84
|
+
id: TypographyRuleId;
|
|
85
|
+
family: TypographyRuleFamily;
|
|
86
|
+
tier: 1 | 2 | 3;
|
|
87
|
+
severity: "error" | "warning";
|
|
88
|
+
check(segment: TextSegment, ctx: TypographyContext): TypographyFinding[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TypographyContext {
|
|
92
|
+
locale: string;
|
|
93
|
+
allowedTokens: ReadonlySet<string>;
|
|
94
|
+
abbreviations: ReadonlySet<string>;
|
|
95
|
+
/** Locale-specific typography defaults (number format, apostrophe policy). Added by RFC-1070. */
|
|
96
|
+
localeDefaults: LocaleTypographyDefaults;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Default allowed tokens seed (TYPO-CASE-01 and TYPO-LOCALE-03 exclusions)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
export const DEFAULT_ALLOWED_TOKENS = new Set([
|
|
104
|
+
"iPhone",
|
|
105
|
+
"iPad",
|
|
106
|
+
"eBay",
|
|
107
|
+
"GitHub",
|
|
108
|
+
"YouTube",
|
|
109
|
+
"LinkedIn",
|
|
110
|
+
"WordPress",
|
|
111
|
+
"JavaScript",
|
|
112
|
+
"TypeScript",
|
|
113
|
+
"PayPal",
|
|
114
|
+
"macOS",
|
|
115
|
+
"iOS",
|
|
116
|
+
"OpenAI",
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Helper: create a finding
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export function finding(
|
|
124
|
+
ruleId: TypographyRuleId,
|
|
125
|
+
segment: TextSegment,
|
|
126
|
+
match: string,
|
|
127
|
+
column: number,
|
|
128
|
+
message: string,
|
|
129
|
+
fixHint: string,
|
|
130
|
+
fix: FixAction | null = null,
|
|
131
|
+
): TypographyFinding {
|
|
132
|
+
return { ruleId, segment, match, column, message, fixHint, fix };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Helper: check if a token is in allowedTokens
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
function isAllowedToken(token: string, ctx: TypographyContext): boolean {
|
|
140
|
+
if (ctx.allowedTokens.has(token)) return true;
|
|
141
|
+
// Also check with leading/trailing punctuation stripped
|
|
142
|
+
const stripped = token.replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, "");
|
|
143
|
+
if (stripped !== token && ctx.allowedTokens.has(stripped)) return true;
|
|
144
|
+
// Also check hyphenated parts (e.g. LinkedIn-Profil → LinkedIn)
|
|
145
|
+
for (const part of stripped.split("-")) {
|
|
146
|
+
if (part.length > 0 && ctx.allowedTokens.has(part)) return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Helper: extract tokens (maximal non-whitespace sequences) from text
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
function tokenize(text: string): string[] {
|
|
156
|
+
return text.split(/\s+/).filter(Boolean);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Placeholder characters — treated as letters for spacing, skipped for casing/locale
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
const FORMULA_PLACEHOLDER = "\uE000";
|
|
164
|
+
const URL_PLACEHOLDER = "\uE001";
|
|
165
|
+
const CMS_PLACEHOLDER = "\uE002";
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// TYPO-PUNCT-01: Doubled or clashing punctuation
|
|
169
|
+
// Pattern: [,;:!?]\. · \.[,;] · :: · ;; · ,, · [!?]{3,}
|
|
170
|
+
// Exclusions: ?! and !? are allowed
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const PUNCT01_PATTERNS: RegExp[] = [/[,;:!?]\./u, /\.[,;]/u, /::/u, /;;/u, /,,/u, /[!?]{3,}/u];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Deduplicate clashing punctuation for PUNCT-01 fix.
|
|
177
|
+
* [,;:!?]. → . (keep the period)
|
|
178
|
+
* .[,;] → . (keep the period)
|
|
179
|
+
* :: → :
|
|
180
|
+
* ;; → ;
|
|
181
|
+
* ,, → ,
|
|
182
|
+
* [!?]{3,} → !! (keep first two)
|
|
183
|
+
*/
|
|
184
|
+
function dedupPunct(matched: string): string {
|
|
185
|
+
// [,;:!?]. — closing mark followed by period → keep period
|
|
186
|
+
if (/^[,;:!?]\.$/.test(matched)) return ".";
|
|
187
|
+
// .[,;] — period followed by comma/semicolon → keep period
|
|
188
|
+
if (/^\.[,;]$/.test(matched)) return ".";
|
|
189
|
+
// :: → :
|
|
190
|
+
if (matched === "::") return ":";
|
|
191
|
+
// ;; → ;
|
|
192
|
+
if (matched === ";;") return ";";
|
|
193
|
+
// ,, → ,
|
|
194
|
+
if (matched === ",,") return ",";
|
|
195
|
+
// [!?]{3,} → keep first two
|
|
196
|
+
if (/^[!?]{3,}$/.test(matched)) return matched.slice(0, 2);
|
|
197
|
+
return matched;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const PUNCT01_MESSAGES: Record<string, { message: string; fixHint: string }> = {
|
|
201
|
+
"[,;:!?]\\.": {
|
|
202
|
+
message: "Clashing punctuation — a closing mark followed by a period.",
|
|
203
|
+
fixHint: "Keep exactly one closing mark.",
|
|
204
|
+
},
|
|
205
|
+
"\\.[,;]": {
|
|
206
|
+
message: "Clashing punctuation — a period followed by a comma or semicolon.",
|
|
207
|
+
fixHint: "Keep exactly one closing mark.",
|
|
208
|
+
},
|
|
209
|
+
"::": {
|
|
210
|
+
message: "Doubled colon.",
|
|
211
|
+
fixHint: "Use a single colon.",
|
|
212
|
+
},
|
|
213
|
+
";;": {
|
|
214
|
+
message: "Doubled semicolon.",
|
|
215
|
+
fixHint: "Use a single semicolon.",
|
|
216
|
+
},
|
|
217
|
+
",,": {
|
|
218
|
+
message: "Doubled comma.",
|
|
219
|
+
fixHint: "Use a single comma.",
|
|
220
|
+
},
|
|
221
|
+
"[!?]{3,}": {
|
|
222
|
+
message: "Three or more exclamation/question marks in a row.",
|
|
223
|
+
fixHint: "Use at most two exclamation or question marks.",
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const punct01: TypographyRule = {
|
|
228
|
+
id: "TYPO-PUNCT-01",
|
|
229
|
+
family: "PUNCT",
|
|
230
|
+
tier: 1,
|
|
231
|
+
severity: "error",
|
|
232
|
+
check(segment, ctx) {
|
|
233
|
+
const findings: TypographyFinding[] = [];
|
|
234
|
+
const text = segment.text;
|
|
235
|
+
for (const pattern of PUNCT01_PATTERNS) {
|
|
236
|
+
const match = pattern.exec(text);
|
|
237
|
+
if (match) {
|
|
238
|
+
// Check if it's ?! or !? — those are allowed
|
|
239
|
+
const matched = match[0];
|
|
240
|
+
if (matched === "?!" || matched === "!?") continue;
|
|
241
|
+
|
|
242
|
+
// Exclusion: period followed by comma/semicolon where the period
|
|
243
|
+
// is the last char of a known abbreviation (e.g. "e.V.,", "Inc.,")
|
|
244
|
+
if (pattern.source === "\\.[,;]") {
|
|
245
|
+
const textUpToDot = text.slice(0, match.index + 1);
|
|
246
|
+
let isAbbrev = false;
|
|
247
|
+
for (const abbr of ctx.abbreviations) {
|
|
248
|
+
if (textUpToDot.endsWith(abbr)) {
|
|
249
|
+
isAbbrev = true;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (isAbbrev) continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const key = pattern.source;
|
|
257
|
+
const msg = PUNCT01_MESSAGES[key] ?? {
|
|
258
|
+
message: `Doubled or clashing punctuation: "${matched}".`,
|
|
259
|
+
fixHint: "Keep exactly one closing mark.",
|
|
260
|
+
};
|
|
261
|
+
findings.push(
|
|
262
|
+
finding(this.id, segment, matched, match.index, msg.message, msg.fixHint, {
|
|
263
|
+
type: "replace",
|
|
264
|
+
old: matched,
|
|
265
|
+
new: dedupPunct(matched),
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return findings;
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// TYPO-PUNCT-02: Two dots or four-plus dots (not three-dot ellipsis)
|
|
276
|
+
// Pattern: (?<!\.)\.\.(?!\.) · \.{4,}
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
const PUNCT02_DOUBLE_DOT = /(?<!\.)\.\.(?!\.)/u;
|
|
280
|
+
const PUNCT02_MANY_DOTS = /\.{4,}/u;
|
|
281
|
+
|
|
282
|
+
const punct02: TypographyRule = {
|
|
283
|
+
id: "TYPO-PUNCT-02",
|
|
284
|
+
family: "PUNCT",
|
|
285
|
+
tier: 1,
|
|
286
|
+
severity: "error",
|
|
287
|
+
check(segment, ctx) {
|
|
288
|
+
const findings: TypographyFinding[] = [];
|
|
289
|
+
const text = segment.text;
|
|
290
|
+
let m = PUNCT02_DOUBLE_DOT.exec(text);
|
|
291
|
+
if (m) {
|
|
292
|
+
// Exclusion: first dot is the last char of a known abbreviation
|
|
293
|
+
const textUpToFirstDot = text.slice(0, m.index + 1);
|
|
294
|
+
let isAbbrev = false;
|
|
295
|
+
for (const abbr of ctx.abbreviations) {
|
|
296
|
+
if (textUpToFirstDot.endsWith(abbr)) {
|
|
297
|
+
isAbbrev = true;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!isAbbrev) {
|
|
302
|
+
findings.push(
|
|
303
|
+
finding(
|
|
304
|
+
this.id,
|
|
305
|
+
segment,
|
|
306
|
+
m[0],
|
|
307
|
+
m.index,
|
|
308
|
+
"Two consecutive dots (not a three-dot ellipsis).",
|
|
309
|
+
"Use a single period or a three-dot ellipsis (...).",
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
m = PUNCT02_MANY_DOTS.exec(text);
|
|
315
|
+
if (m) {
|
|
316
|
+
findings.push(
|
|
317
|
+
finding(
|
|
318
|
+
this.id,
|
|
319
|
+
segment,
|
|
320
|
+
m[0],
|
|
321
|
+
m.index,
|
|
322
|
+
"Four or more consecutive dots.",
|
|
323
|
+
"Use a three-dot ellipsis (...) or a single period.",
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return findings;
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// TYPO-PUNCT-03: Whitespace before closing punctuation
|
|
333
|
+
// Pattern: \s[,;:!?] · \s\.(?!\.\.)
|
|
334
|
+
// Exclusions: \s\.\.\. (spaced ellipsis) is allowed
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
const PUNCT03_WS_BEFORE_PUNCT = /\s[,;:!?]/u;
|
|
338
|
+
const PUNCT03_WS_BEFORE_DOT = /\s\.(?!\.)/u;
|
|
339
|
+
|
|
340
|
+
const punct03: TypographyRule = {
|
|
341
|
+
id: "TYPO-PUNCT-03",
|
|
342
|
+
family: "PUNCT",
|
|
343
|
+
tier: 1,
|
|
344
|
+
severity: "error",
|
|
345
|
+
check(segment, _ctx) {
|
|
346
|
+
const findings: TypographyFinding[] = [];
|
|
347
|
+
const text = segment.text;
|
|
348
|
+
let m = PUNCT03_WS_BEFORE_PUNCT.exec(text);
|
|
349
|
+
if (m) {
|
|
350
|
+
findings.push(
|
|
351
|
+
finding(
|
|
352
|
+
this.id,
|
|
353
|
+
segment,
|
|
354
|
+
m[0],
|
|
355
|
+
m.index,
|
|
356
|
+
"Whitespace before closing punctuation.",
|
|
357
|
+
"Remove the space before the punctuation mark.",
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
m = PUNCT03_WS_BEFORE_DOT.exec(text);
|
|
362
|
+
if (m) {
|
|
363
|
+
findings.push(
|
|
364
|
+
finding(
|
|
365
|
+
this.id,
|
|
366
|
+
segment,
|
|
367
|
+
m[0],
|
|
368
|
+
m.index,
|
|
369
|
+
"Whitespace before a period.",
|
|
370
|
+
"Remove the space before the period.",
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return findings;
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// TYPO-PUNCT-04: Missing space after closing punctuation before a letter
|
|
380
|
+
// Pattern: [,;!?]\p{L} · (?<!\b\p{L})\.\p{L}
|
|
381
|
+
// Exclusions: single-letter abbreviation stems (z.B., u.a.) — deferred to TYPO-ABBR (RFC-1070);
|
|
382
|
+
// URL/e-mail/domain tokens; decimal separators followed by digits
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
const PUNCT04_COMMA_SEMICOLON_EXCL = /[,;!?]\p{L}/u;
|
|
386
|
+
const PUNCT04_DOT_LETTER = /(?<!\b\p{L})\.\p{L}/u;
|
|
387
|
+
|
|
388
|
+
const punct04: TypographyRule = {
|
|
389
|
+
id: "TYPO-PUNCT-04",
|
|
390
|
+
family: "PUNCT",
|
|
391
|
+
tier: 1,
|
|
392
|
+
severity: "error",
|
|
393
|
+
check(segment, _ctx) {
|
|
394
|
+
const findings: TypographyFinding[] = [];
|
|
395
|
+
const text = segment.text;
|
|
396
|
+
|
|
397
|
+
let m = PUNCT04_COMMA_SEMICOLON_EXCL.exec(text);
|
|
398
|
+
if (m) {
|
|
399
|
+
// Skip decimal separators (digit,digit)
|
|
400
|
+
const before = text[m.index - 1];
|
|
401
|
+
const after = text[m.index + 2];
|
|
402
|
+
if (before && /\p{Nd}/u.test(before) && after && /\p{Nd}/u.test(after)) {
|
|
403
|
+
// decimal separator — skip
|
|
404
|
+
} else {
|
|
405
|
+
findings.push(
|
|
406
|
+
finding(
|
|
407
|
+
this.id,
|
|
408
|
+
segment,
|
|
409
|
+
m[0],
|
|
410
|
+
m.index,
|
|
411
|
+
"Missing space after closing punctuation before a letter.",
|
|
412
|
+
"Add a space after the punctuation mark.",
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
m = PUNCT04_DOT_LETTER.exec(text);
|
|
419
|
+
if (m) {
|
|
420
|
+
// Version pattern exclusion: \d+\.x\b (e.g. "4.x", "2.0.x") — RFC-1083.
|
|
421
|
+
// match[0] is ".x" (dot + letter), NOT "4.x". Check the character before
|
|
422
|
+
// the dot (must be a digit) and the letter after the dot (must be "x"
|
|
423
|
+
// followed by a non-word character or end of string).
|
|
424
|
+
const beforeChar = text[m.index - 1];
|
|
425
|
+
const afterLetter = text[m.index + 2];
|
|
426
|
+
const isVersion =
|
|
427
|
+
beforeChar !== undefined &&
|
|
428
|
+
/\p{Nd}/u.test(beforeChar) &&
|
|
429
|
+
m[0][1] === "x" &&
|
|
430
|
+
(afterLetter === undefined || /\W/u.test(afterLetter));
|
|
431
|
+
if (isVersion) {
|
|
432
|
+
// version designator like "4.x" — skip
|
|
433
|
+
} else {
|
|
434
|
+
// Skip if preceded by a single letter (abbreviation like z.B.)
|
|
435
|
+
const isSingleLetterAbbrev =
|
|
436
|
+
beforeChar !== undefined &&
|
|
437
|
+
/\p{L}/u.test(beforeChar) &&
|
|
438
|
+
(text[m.index - 2] === undefined || /\s/.test(text[m.index - 2]));
|
|
439
|
+
if (!isSingleLetterAbbrev) {
|
|
440
|
+
findings.push(
|
|
441
|
+
finding(
|
|
442
|
+
this.id,
|
|
443
|
+
segment,
|
|
444
|
+
m[0],
|
|
445
|
+
m.index,
|
|
446
|
+
"Missing space after a period before a letter.",
|
|
447
|
+
"Add a space after the period.",
|
|
448
|
+
),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return findings;
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// TYPO-SPACE-01: Two or more consecutive spaces inside text
|
|
460
|
+
// Pattern: \S {2,}\S
|
|
461
|
+
// Exclusions: markdown table rows
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
const SPACE01 = /\S {2,}\S/u;
|
|
465
|
+
|
|
466
|
+
const space01: TypographyRule = {
|
|
467
|
+
id: "TYPO-SPACE-01",
|
|
468
|
+
family: "SPACE",
|
|
469
|
+
tier: 1,
|
|
470
|
+
severity: "error",
|
|
471
|
+
check(segment, _ctx) {
|
|
472
|
+
if (segment.isTableRow) return [];
|
|
473
|
+
const m = SPACE01.exec(segment.text);
|
|
474
|
+
if (m) {
|
|
475
|
+
return [
|
|
476
|
+
finding(
|
|
477
|
+
this.id,
|
|
478
|
+
segment,
|
|
479
|
+
m[0],
|
|
480
|
+
m.index,
|
|
481
|
+
"Two or more consecutive spaces inside text.",
|
|
482
|
+
"Use a single space.",
|
|
483
|
+
{ type: "replace", old: m[0], new: m[0].replace(/ {2,}/g, " ") },
|
|
484
|
+
),
|
|
485
|
+
];
|
|
486
|
+
}
|
|
487
|
+
return [];
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// TYPO-SPACE-02: Trailing whitespace at end of a frontmatter string or body line
|
|
493
|
+
// Pattern: [ \t]+$
|
|
494
|
+
// Exclusions: none — markdown two-space hard breaks are forbidden
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
const SPACE02 = /[ \t]+$/u;
|
|
498
|
+
|
|
499
|
+
const space02: TypographyRule = {
|
|
500
|
+
id: "TYPO-SPACE-02",
|
|
501
|
+
family: "SPACE",
|
|
502
|
+
tier: 1,
|
|
503
|
+
severity: "error",
|
|
504
|
+
check(segment, _ctx) {
|
|
505
|
+
const m = SPACE02.exec(segment.text);
|
|
506
|
+
if (m) {
|
|
507
|
+
return [
|
|
508
|
+
finding(
|
|
509
|
+
this.id,
|
|
510
|
+
segment,
|
|
511
|
+
m[0],
|
|
512
|
+
m.index,
|
|
513
|
+
"Trailing whitespace at end of line.",
|
|
514
|
+
"Remove trailing spaces.",
|
|
515
|
+
{ type: "delete", at: m.index, length: m[0].length },
|
|
516
|
+
),
|
|
517
|
+
];
|
|
518
|
+
}
|
|
519
|
+
return [];
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// TYPO-SPACE-03: Tab character inside text
|
|
525
|
+
// Pattern: \t
|
|
526
|
+
// Exclusions: fenced code blocks (already stripped by extractor)
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
const SPACE03 = /\t/u;
|
|
530
|
+
|
|
531
|
+
const space03: TypographyRule = {
|
|
532
|
+
id: "TYPO-SPACE-03",
|
|
533
|
+
family: "SPACE",
|
|
534
|
+
tier: 1,
|
|
535
|
+
severity: "error",
|
|
536
|
+
check(segment, _ctx) {
|
|
537
|
+
const m = SPACE03.exec(segment.text);
|
|
538
|
+
if (m) {
|
|
539
|
+
return [
|
|
540
|
+
finding(
|
|
541
|
+
this.id,
|
|
542
|
+
segment,
|
|
543
|
+
m[0],
|
|
544
|
+
m.index,
|
|
545
|
+
"Tab character inside text.",
|
|
546
|
+
"Use spaces instead of tabs.",
|
|
547
|
+
),
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
return [];
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
// TYPO-SPACE-04: Leading whitespace in a frontmatter string value
|
|
556
|
+
// Pattern: ^[ \t]+
|
|
557
|
+
// Exclusions: none
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
const SPACE04 = /^[ \t]+/u;
|
|
561
|
+
|
|
562
|
+
const space04: TypographyRule = {
|
|
563
|
+
id: "TYPO-SPACE-04",
|
|
564
|
+
family: "SPACE",
|
|
565
|
+
tier: 1,
|
|
566
|
+
severity: "error",
|
|
567
|
+
check(segment, _ctx) {
|
|
568
|
+
if (segment.source !== "frontmatter") return [];
|
|
569
|
+
const m = SPACE04.exec(segment.text);
|
|
570
|
+
if (m) {
|
|
571
|
+
return [
|
|
572
|
+
finding(
|
|
573
|
+
this.id,
|
|
574
|
+
segment,
|
|
575
|
+
m[0],
|
|
576
|
+
m.index,
|
|
577
|
+
"Leading whitespace in a frontmatter string value.",
|
|
578
|
+
"Remove leading spaces from the value.",
|
|
579
|
+
),
|
|
580
|
+
];
|
|
581
|
+
}
|
|
582
|
+
return [];
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
// TYPO-CASE-01: Uppercase letter between two lowercase letters inside a word
|
|
588
|
+
// Pattern: \p{Ll}\p{Lu}\p{Ll}
|
|
589
|
+
// Exclusions: tokens in typography.allowedTokens; code spans; formulas; URLs
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
|
|
592
|
+
const CASE01 = /\p{Ll}\p{Lu}\p{Ll}/u;
|
|
593
|
+
|
|
594
|
+
const case01: TypographyRule = {
|
|
595
|
+
id: "TYPO-CASE-01",
|
|
596
|
+
family: "CASE",
|
|
597
|
+
tier: 1,
|
|
598
|
+
severity: "error",
|
|
599
|
+
check(segment, ctx) {
|
|
600
|
+
const text = segment.text;
|
|
601
|
+
const m = CASE01.exec(text);
|
|
602
|
+
if (!m) return [];
|
|
603
|
+
|
|
604
|
+
// Check if the containing token is in allowedTokens
|
|
605
|
+
// Find the token (maximal non-whitespace sequence) that contains this match
|
|
606
|
+
const matchStart = m.index;
|
|
607
|
+
const matchEnd = m.index + m[0].length;
|
|
608
|
+
|
|
609
|
+
// Find token boundaries
|
|
610
|
+
let tokenStart = matchStart;
|
|
611
|
+
while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1])) tokenStart--;
|
|
612
|
+
let tokenEnd = matchEnd;
|
|
613
|
+
while (tokenEnd < text.length && !/\s/.test(text[tokenEnd])) tokenEnd++;
|
|
614
|
+
|
|
615
|
+
const token = text.slice(tokenStart, tokenEnd);
|
|
616
|
+
|
|
617
|
+
// Skip if token contains placeholders (formula/URL)
|
|
618
|
+
if (
|
|
619
|
+
token.includes(FORMULA_PLACEHOLDER) ||
|
|
620
|
+
token.includes(URL_PLACEHOLDER) ||
|
|
621
|
+
token.includes(CMS_PLACEHOLDER)
|
|
622
|
+
)
|
|
623
|
+
return [];
|
|
624
|
+
|
|
625
|
+
if (isAllowedToken(token, ctx)) return [];
|
|
626
|
+
|
|
627
|
+
return [
|
|
628
|
+
finding(
|
|
629
|
+
this.id,
|
|
630
|
+
segment,
|
|
631
|
+
m[0],
|
|
632
|
+
m.index,
|
|
633
|
+
`Uppercase letter between two lowercase letters inside a word: "${m[0]}" in "${token}".`,
|
|
634
|
+
"Fix the casing — this is likely a corruption from a regex edit or translation tool.",
|
|
635
|
+
),
|
|
636
|
+
];
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// TYPO-CASE-02: Lowercase sentence start after sentence-ending punctuation inside a paragraph
|
|
642
|
+
// Pattern: [.!?]\s+\p{Ll}
|
|
643
|
+
// Exclusions: preceding token is a known abbreviation for the file locale;
|
|
644
|
+
// preceding token is ...; preceding token is a digit sequence (\d+)
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
646
|
+
|
|
647
|
+
const CASE02 = /[.!?]\s+\p{Ll}/gu;
|
|
648
|
+
|
|
649
|
+
const case02: TypographyRule = {
|
|
650
|
+
id: "TYPO-CASE-02",
|
|
651
|
+
family: "CASE",
|
|
652
|
+
tier: 1,
|
|
653
|
+
severity: "error",
|
|
654
|
+
check(segment, ctx) {
|
|
655
|
+
const text = segment.text;
|
|
656
|
+
for (const m of text.matchAll(CASE02)) {
|
|
657
|
+
const punctIndex = m.index;
|
|
658
|
+
|
|
659
|
+
// Find the token before the punctuation
|
|
660
|
+
// Scan backwards to find the token (maximal non-whitespace before punct)
|
|
661
|
+
let tokenEnd = punctIndex;
|
|
662
|
+
while (tokenEnd > 0 && /\s/.test(text[tokenEnd - 1])) tokenEnd--;
|
|
663
|
+
let tokenStart = tokenEnd;
|
|
664
|
+
while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1])) tokenStart--;
|
|
665
|
+
|
|
666
|
+
const token = text.slice(tokenStart, tokenEnd);
|
|
667
|
+
|
|
668
|
+
// Exclusion: preceding token is a known abbreviation
|
|
669
|
+
if (ctx.abbreviations.has(token)) continue;
|
|
670
|
+
|
|
671
|
+
// Exclusion: text ending at the punctuation matches a multi-word abbreviation
|
|
672
|
+
// (e.g. "z. B." — the period at punctIndex is part of the abbreviation)
|
|
673
|
+
// Also check text including the matched characters, for abbreviations like
|
|
674
|
+
// "u. a." where the first period triggers the match but the abbreviation
|
|
675
|
+
// continues past the matched lowercase letter
|
|
676
|
+
const textUpToPunct = text.slice(0, punctIndex + 1);
|
|
677
|
+
const textUpToMatch = text.slice(0, m.index + m[0].length + 1);
|
|
678
|
+
let isAbbrev = false;
|
|
679
|
+
for (const abbr of ctx.abbreviations) {
|
|
680
|
+
if (textUpToPunct.endsWith(abbr) || textUpToMatch.endsWith(abbr)) {
|
|
681
|
+
isAbbrev = true;
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (isAbbrev) continue;
|
|
686
|
+
|
|
687
|
+
// Exclusion: preceding token is "..."
|
|
688
|
+
if (token === "...") continue;
|
|
689
|
+
|
|
690
|
+
// Exclusion: preceding token is a digit sequence
|
|
691
|
+
if (/^\d+$/.test(token)) continue;
|
|
692
|
+
|
|
693
|
+
// Exclusion: preceding token contains a placeholder (formula/URL/CMS)
|
|
694
|
+
if (
|
|
695
|
+
token.includes(FORMULA_PLACEHOLDER) ||
|
|
696
|
+
token.includes(URL_PLACEHOLDER) ||
|
|
697
|
+
token.includes(CMS_PLACEHOLDER)
|
|
698
|
+
)
|
|
699
|
+
continue;
|
|
700
|
+
|
|
701
|
+
return [
|
|
702
|
+
finding(
|
|
703
|
+
this.id,
|
|
704
|
+
segment,
|
|
705
|
+
m[0],
|
|
706
|
+
m.index,
|
|
707
|
+
"Lowercase sentence start after sentence-ending punctuation.",
|
|
708
|
+
"Capitalize the first letter of the sentence.",
|
|
709
|
+
),
|
|
710
|
+
];
|
|
711
|
+
}
|
|
712
|
+
return [];
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// ---------------------------------------------------------------------------
|
|
717
|
+
// TYPO-LOCALE-01: Mixed-script word (Latin and Cyrillic letters in one token)
|
|
718
|
+
// Pattern: token matches both \p{Script=Latin} and \p{Script=Cyrillic}
|
|
719
|
+
// Exclusions: Latin parts of hyphenated tokens in typography.allowedTokens
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
const LATIN_SCRIPT = /\p{Script=Latin}/u;
|
|
723
|
+
const CYRILLIC_SCRIPT = /\p{Script=Cyrillic}/u;
|
|
724
|
+
|
|
725
|
+
const LATIN_ONLY = /^[\p{Script=Latin}0-9.]+$/u;
|
|
726
|
+
const HAS_LATIN_LETTER = /\p{Script=Latin}/u;
|
|
727
|
+
|
|
728
|
+
function extractLatinParts(token: string): string[] {
|
|
729
|
+
const parts = token.split("-");
|
|
730
|
+
return parts
|
|
731
|
+
.map((p) => p.replace(/[^\p{Script=Latin}0-9.]/gu, ""))
|
|
732
|
+
.filter((p) => p.length > 0 && LATIN_ONLY.test(p) && HAS_LATIN_LETTER.test(p));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const locale01: TypographyRule = {
|
|
736
|
+
id: "TYPO-LOCALE-01",
|
|
737
|
+
family: "LOCALE",
|
|
738
|
+
tier: 1,
|
|
739
|
+
severity: "error",
|
|
740
|
+
check(segment, ctx) {
|
|
741
|
+
const tokens = tokenize(segment.text);
|
|
742
|
+
for (const token of tokens) {
|
|
743
|
+
// Skip tokens with placeholders
|
|
744
|
+
if (
|
|
745
|
+
token.includes(FORMULA_PLACEHOLDER) ||
|
|
746
|
+
token.includes(URL_PLACEHOLDER) ||
|
|
747
|
+
token.includes(CMS_PLACEHOLDER)
|
|
748
|
+
)
|
|
749
|
+
continue;
|
|
750
|
+
if (LATIN_SCRIPT.test(token) && CYRILLIC_SCRIPT.test(token)) {
|
|
751
|
+
// Check if all Latin parts are in allowedTokens
|
|
752
|
+
const latinParts = extractLatinParts(token);
|
|
753
|
+
if (latinParts.length > 0 && latinParts.every((p) => ctx.allowedTokens.has(p))) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
return [
|
|
757
|
+
finding(
|
|
758
|
+
this.id,
|
|
759
|
+
segment,
|
|
760
|
+
token,
|
|
761
|
+
segment.text.indexOf(token),
|
|
762
|
+
`Mixed-script word (Latin and Cyrillic letters in one token): "${token}".`,
|
|
763
|
+
"Separate scripts — this is likely a homoglyph or translation artifact.",
|
|
764
|
+
),
|
|
765
|
+
];
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return [];
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
// TYPO-LOCALE-02: Foreign-alphabet letters for a Cyrillic locale
|
|
774
|
+
// Pattern: file locale `uk`: [ыэъёЫЭЪЁ]
|
|
775
|
+
// Exclusions: tokens in typography.allowedTokens
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
|
|
778
|
+
const UK_FOREIGN_LETTERS = /[ыэъёЫЭЪЁ]/u;
|
|
779
|
+
|
|
780
|
+
const locale02: TypographyRule = {
|
|
781
|
+
id: "TYPO-LOCALE-02",
|
|
782
|
+
family: "LOCALE",
|
|
783
|
+
tier: 1,
|
|
784
|
+
severity: "error",
|
|
785
|
+
check(segment, ctx) {
|
|
786
|
+
if (segment.locale !== "uk") return [];
|
|
787
|
+
const text = segment.text;
|
|
788
|
+
const m = UK_FOREIGN_LETTERS.exec(text);
|
|
789
|
+
if (!m) return [];
|
|
790
|
+
|
|
791
|
+
// Check if the containing token is in allowedTokens
|
|
792
|
+
const matchIndex = m.index;
|
|
793
|
+
let tokenStart = matchIndex;
|
|
794
|
+
while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1])) tokenStart--;
|
|
795
|
+
let tokenEnd = matchIndex + 1;
|
|
796
|
+
while (tokenEnd < text.length && !/\s/.test(text[tokenEnd])) tokenEnd++;
|
|
797
|
+
const token = text.slice(tokenStart, tokenEnd);
|
|
798
|
+
|
|
799
|
+
if (isAllowedToken(token, ctx)) return [];
|
|
800
|
+
|
|
801
|
+
return [
|
|
802
|
+
finding(
|
|
803
|
+
this.id,
|
|
804
|
+
segment,
|
|
805
|
+
m[0],
|
|
806
|
+
m.index,
|
|
807
|
+
`Foreign-alphabet letter "${m[0]}" in a Ukrainian locale (these letters are Russian, not Ukrainian).`,
|
|
808
|
+
"Replace with the Ukrainian equivalent.",
|
|
809
|
+
),
|
|
810
|
+
];
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// ---------------------------------------------------------------------------
|
|
815
|
+
// TYPO-LOCALE-03: Cyrillic letters in a non-Cyrillic locale
|
|
816
|
+
// Pattern: file locale `de`/`en`: \p{Script=Cyrillic}
|
|
817
|
+
// Exclusions: tokens in typography.allowedTokens; markdown blockquote lines (lines starting with >)
|
|
818
|
+
// ---------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
const locale03: TypographyRule = {
|
|
821
|
+
id: "TYPO-LOCALE-03",
|
|
822
|
+
family: "LOCALE",
|
|
823
|
+
tier: 1,
|
|
824
|
+
severity: "error",
|
|
825
|
+
check(segment, ctx) {
|
|
826
|
+
if (segment.locale === "uk") return [];
|
|
827
|
+
const text = segment.text;
|
|
828
|
+
const m = CYRILLIC_SCRIPT.exec(text);
|
|
829
|
+
if (!m) return [];
|
|
830
|
+
|
|
831
|
+
// Check if the containing token is in allowedTokens
|
|
832
|
+
const matchIndex = m.index;
|
|
833
|
+
let tokenStart = matchIndex;
|
|
834
|
+
while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1])) tokenStart--;
|
|
835
|
+
let tokenEnd = matchIndex + 1;
|
|
836
|
+
while (tokenEnd < text.length && !/\s/.test(text[tokenEnd])) tokenEnd++;
|
|
837
|
+
const token = text.slice(tokenStart, tokenEnd);
|
|
838
|
+
|
|
839
|
+
if (isAllowedToken(token, ctx)) return [];
|
|
840
|
+
|
|
841
|
+
// Check if this is a blockquote line (raw starts with >)
|
|
842
|
+
if (segment.source === "body" && /^\s*>/.test(segment.raw)) return [];
|
|
843
|
+
|
|
844
|
+
return [
|
|
845
|
+
finding(
|
|
846
|
+
this.id,
|
|
847
|
+
segment,
|
|
848
|
+
m[0],
|
|
849
|
+
m.index,
|
|
850
|
+
`Cyrillic letter "${m[0]}" in a non-Cyrillic locale (${segment.locale}).`,
|
|
851
|
+
"Replace with the Latin equivalent or wrap in a blockquote/fenced block.",
|
|
852
|
+
),
|
|
853
|
+
];
|
|
854
|
+
},
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
// TYPO-YAML-02: Odd number of straight double quotes in a text value
|
|
859
|
+
// Pattern: count of " in value is odd
|
|
860
|
+
// Exclusions: none
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
|
|
863
|
+
const yaml02: TypographyRule = {
|
|
864
|
+
id: "TYPO-YAML-02",
|
|
865
|
+
family: "YAML",
|
|
866
|
+
tier: 1,
|
|
867
|
+
severity: "error",
|
|
868
|
+
check(segment, _ctx) {
|
|
869
|
+
if (segment.source !== "frontmatter") return [];
|
|
870
|
+
const quoteCount = (segment.raw.match(/"/g) ?? []).length;
|
|
871
|
+
if (quoteCount % 2 !== 0) {
|
|
872
|
+
return [
|
|
873
|
+
finding(
|
|
874
|
+
this.id,
|
|
875
|
+
segment,
|
|
876
|
+
'"',
|
|
877
|
+
0,
|
|
878
|
+
`Odd number of straight double quotes (${quoteCount}) in a text value.`,
|
|
879
|
+
"Balance the quotes — add or remove a double quote.",
|
|
880
|
+
),
|
|
881
|
+
];
|
|
882
|
+
}
|
|
883
|
+
return [];
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
// ---------------------------------------------------------------------------
|
|
888
|
+
// Export all Tier 1 rules
|
|
889
|
+
// ---------------------------------------------------------------------------
|
|
890
|
+
|
|
891
|
+
export const TIER1_RULES: readonly TypographyRule[] = [
|
|
892
|
+
punct01,
|
|
893
|
+
punct02,
|
|
894
|
+
punct03,
|
|
895
|
+
punct04,
|
|
896
|
+
space01,
|
|
897
|
+
space02,
|
|
898
|
+
space03,
|
|
899
|
+
space04,
|
|
900
|
+
case01,
|
|
901
|
+
case02,
|
|
902
|
+
locale01,
|
|
903
|
+
locale02,
|
|
904
|
+
locale03,
|
|
905
|
+
yaml02,
|
|
906
|
+
];
|
|
907
|
+
|
|
908
|
+
// Note: TYPO-YAML-01 is not a rule function — it is emitted by the command
|
|
909
|
+
// adapter when the text-surface extractor returns a non-null yamlError.
|
|
910
|
+
|
|
911
|
+
export function createTypographyContext(
|
|
912
|
+
locale: string,
|
|
913
|
+
allowedTokens: ReadonlySet<string>,
|
|
914
|
+
abbreviationsOverride?: ReadonlySet<string>,
|
|
915
|
+
): TypographyContext {
|
|
916
|
+
const defaults = getLocaleDefaults(locale);
|
|
917
|
+
return {
|
|
918
|
+
locale,
|
|
919
|
+
allowedTokens: new Set([...DEFAULT_ALLOWED_TOKENS, ...allowedTokens]),
|
|
920
|
+
abbreviations: abbreviationsOverride ?? defaults.abbreviations,
|
|
921
|
+
localeDefaults: defaults,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export { LOCALE_DEFAULTS, getLocaleDefaults };
|