@softure-ai/blog 0.0.0-stage → 0.1.6
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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +604 -2
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/command.d.ts +9 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +40 -0
- package/dist/cli/command.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +62 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +555 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/skill.d.ts +47 -0
- package/dist/cli/skill.d.ts.map +1 -0
- package/dist/cli/skill.js +221 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/content/article-file.d.ts +27 -0
- package/dist/content/article-file.d.ts.map +1 -0
- package/dist/content/article-file.js +179 -0
- package/dist/content/article-file.js.map +1 -0
- package/dist/contract.d.ts +77 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +5 -0
- package/dist/contract.js.map +1 -0
- package/dist/db/articles.d.ts +30 -0
- package/dist/db/articles.d.ts.map +1 -0
- package/dist/db/articles.js +138 -0
- package/dist/db/articles.js.map +1 -0
- package/dist/db/publish-run.d.ts +50 -0
- package/dist/db/publish-run.d.ts.map +1 -0
- package/dist/db/publish-run.js +168 -0
- package/dist/db/publish-run.js.map +1 -0
- package/dist/db/schema.d.ts +403 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +33 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/discovery/dates.d.ts +9 -0
- package/dist/discovery/dates.d.ts.map +1 -0
- package/dist/discovery/dates.js +19 -0
- package/dist/discovery/dates.js.map +1 -0
- package/dist/discovery/index.d.ts +8 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +11 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/indexnow.d.ts +11 -0
- package/dist/discovery/indexnow.d.ts.map +1 -0
- package/dist/discovery/indexnow.js +23 -0
- package/dist/discovery/indexnow.js.map +1 -0
- package/dist/discovery/refresh.d.ts +49 -0
- package/dist/discovery/refresh.d.ts.map +1 -0
- package/dist/discovery/refresh.js +55 -0
- package/dist/discovery/refresh.js.map +1 -0
- package/dist/discovery/related.d.ts +17 -0
- package/dist/discovery/related.d.ts.map +1 -0
- package/dist/discovery/related.js +28 -0
- package/dist/discovery/related.js.map +1 -0
- package/dist/discovery/rss.d.ts +30 -0
- package/dist/discovery/rss.d.ts.map +1 -0
- package/dist/discovery/rss.js +48 -0
- package/dist/discovery/rss.js.map +1 -0
- package/dist/discovery/sitemap.d.ts +25 -0
- package/dist/discovery/sitemap.d.ts.map +1 -0
- package/dist/discovery/sitemap.js +31 -0
- package/dist/discovery/sitemap.js.map +1 -0
- package/dist/discovery/submit.d.ts +44 -0
- package/dist/discovery/submit.d.ts.map +1 -0
- package/dist/discovery/submit.js +46 -0
- package/dist/discovery/submit.js.map +1 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/en.d.ts +83 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +83 -0
- package/dist/messages/en.js.map +1 -0
- package/dist/messages/index.d.ts +158 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +5 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/pl.d.ts +78 -0
- package/dist/messages/pl.d.ts.map +1 -0
- package/dist/messages/pl.js +78 -0
- package/dist/messages/pl.js.map +1 -0
- package/dist/next/context.d.ts +6 -0
- package/dist/next/context.d.ts.map +1 -0
- package/dist/next/context.js +27 -0
- package/dist/next/context.js.map +1 -0
- package/dist/next/data.d.ts +11 -0
- package/dist/next/data.d.ts.map +1 -0
- package/dist/next/data.js +40 -0
- package/dist/next/data.js.map +1 -0
- package/dist/next/discovery.d.ts +6 -0
- package/dist/next/discovery.d.ts.map +1 -0
- package/dist/next/discovery.js +49 -0
- package/dist/next/discovery.js.map +1 -0
- package/dist/next/index.d.ts +9 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +10 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/og-fonts.d.ts +6 -0
- package/dist/next/og-fonts.d.ts.map +1 -0
- package/dist/next/og-fonts.js +8 -0
- package/dist/next/og-fonts.js.map +1 -0
- package/dist/next/og-image.d.ts +36 -0
- package/dist/next/og-image.d.ts.map +1 -0
- package/dist/next/og-image.js +77 -0
- package/dist/next/og-image.js.map +1 -0
- package/dist/next/pages.d.ts +45 -0
- package/dist/next/pages.d.ts.map +1 -0
- package/dist/next/pages.js +189 -0
- package/dist/next/pages.js.map +1 -0
- package/dist/next/refresh.d.ts +9 -0
- package/dist/next/refresh.d.ts.map +1 -0
- package/dist/next/refresh.js +81 -0
- package/dist/next/refresh.js.map +1 -0
- package/dist/options.d.ts +320 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +184 -0
- package/dist/options.js.map +1 -0
- package/dist/pages/body.d.ts +18 -0
- package/dist/pages/body.d.ts.map +1 -0
- package/dist/pages/body.js +21 -0
- package/dist/pages/body.js.map +1 -0
- package/dist/pages/dates.d.ts +21 -0
- package/dist/pages/dates.d.ts.map +1 -0
- package/dist/pages/dates.js +24 -0
- package/dist/pages/dates.js.map +1 -0
- package/dist/pages/index.d.ts +7 -0
- package/dist/pages/index.d.ts.map +1 -0
- package/dist/pages/index.js +9 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/json-ld.d.ts +33 -0
- package/dist/pages/json-ld.d.ts.map +1 -0
- package/dist/pages/json-ld.js +95 -0
- package/dist/pages/json-ld.js.map +1 -0
- package/dist/pages/listing.d.ts +46 -0
- package/dist/pages/listing.d.ts.map +1 -0
- package/dist/pages/listing.js +57 -0
- package/dist/pages/listing.js.map +1 -0
- package/dist/pages/paths.d.ts +37 -0
- package/dist/pages/paths.d.ts.map +1 -0
- package/dist/pages/paths.js +61 -0
- package/dist/pages/paths.js.map +1 -0
- package/dist/pages/redirects.d.ts +57 -0
- package/dist/pages/redirects.d.ts.map +1 -0
- package/dist/pages/redirects.js +78 -0
- package/dist/pages/redirects.js.map +1 -0
- package/dist/proxy/index.d.ts +15 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +60 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/quality/blocks.d.ts +21 -0
- package/dist/quality/blocks.d.ts.map +1 -0
- package/dist/quality/blocks.js +113 -0
- package/dist/quality/blocks.js.map +1 -0
- package/dist/quality/catalog.d.ts +10 -0
- package/dist/quality/catalog.d.ts.map +1 -0
- package/dist/quality/catalog.js +71 -0
- package/dist/quality/catalog.js.map +1 -0
- package/dist/quality/check-article.d.ts +34 -0
- package/dist/quality/check-article.d.ts.map +1 -0
- package/dist/quality/check-article.js +74 -0
- package/dist/quality/check-article.js.map +1 -0
- package/dist/quality/check-files.d.ts +23 -0
- package/dist/quality/check-files.d.ts.map +1 -0
- package/dist/quality/check-files.js +44 -0
- package/dist/quality/check-files.js.map +1 -0
- package/dist/quality/external-links.d.ts +19 -0
- package/dist/quality/external-links.d.ts.map +1 -0
- package/dist/quality/external-links.js +30 -0
- package/dist/quality/external-links.js.map +1 -0
- package/dist/quality/finding.d.ts +17 -0
- package/dist/quality/finding.d.ts.map +1 -0
- package/dist/quality/finding.js +15 -0
- package/dist/quality/finding.js.map +1 -0
- package/dist/quality/gate.d.ts +5 -0
- package/dist/quality/gate.d.ts.map +1 -0
- package/dist/quality/gate.js +10 -0
- package/dist/quality/gate.js.map +1 -0
- package/dist/quality/index.d.ts +15 -0
- package/dist/quality/index.d.ts.map +1 -0
- package/dist/quality/index.js +16 -0
- package/dist/quality/index.js.map +1 -0
- package/dist/quality/link-targets.d.ts +44 -0
- package/dist/quality/link-targets.d.ts.map +1 -0
- package/dist/quality/link-targets.js +91 -0
- package/dist/quality/link-targets.js.map +1 -0
- package/dist/quality/options.d.ts +113 -0
- package/dist/quality/options.d.ts.map +1 -0
- package/dist/quality/options.js +93 -0
- package/dist/quality/options.js.map +1 -0
- package/dist/quality/plugin.d.ts +34 -0
- package/dist/quality/plugin.d.ts.map +1 -0
- package/dist/quality/plugin.js +7 -0
- package/dist/quality/plugin.js.map +1 -0
- package/dist/quality/rules/blocks.d.ts +5 -0
- package/dist/quality/rules/blocks.d.ts.map +1 -0
- package/dist/quality/rules/blocks.js +35 -0
- package/dist/quality/rules/blocks.js.map +1 -0
- package/dist/quality/rules/images.d.ts +4 -0
- package/dist/quality/rules/images.d.ts.map +1 -0
- package/dist/quality/rules/images.js +33 -0
- package/dist/quality/rules/images.js.map +1 -0
- package/dist/quality/rules/input.d.ts +11 -0
- package/dist/quality/rules/input.d.ts.map +1 -0
- package/dist/quality/rules/input.js +2 -0
- package/dist/quality/rules/input.js.map +1 -0
- package/dist/quality/rules/links.d.ts +17 -0
- package/dist/quality/rules/links.d.ts.map +1 -0
- package/dist/quality/rules/links.js +46 -0
- package/dist/quality/rules/links.js.map +1 -0
- package/dist/quality/rules/structure.d.ts +13 -0
- package/dist/quality/rules/structure.d.ts.map +1 -0
- package/dist/quality/rules/structure.js +139 -0
- package/dist/quality/rules/structure.js.map +1 -0
- package/dist/quality/rules/style.d.ts +11 -0
- package/dist/quality/rules/style.d.ts.map +1 -0
- package/dist/quality/rules/style.js +129 -0
- package/dist/quality/rules/style.js.map +1 -0
- package/dist/quality/rules/ymyl.d.ts +5 -0
- package/dist/quality/rules/ymyl.d.ts.map +1 -0
- package/dist/quality/rules/ymyl.js +51 -0
- package/dist/quality/rules/ymyl.js.map +1 -0
- package/dist/quality/rulesets/en/ruleset.d.ts +3 -0
- package/dist/quality/rulesets/en/ruleset.d.ts.map +1 -0
- package/dist/quality/rulesets/en/ruleset.js +106 -0
- package/dist/quality/rulesets/en/ruleset.js.map +1 -0
- package/dist/quality/rulesets/index.d.ts +7 -0
- package/dist/quality/rulesets/index.d.ts.map +1 -0
- package/dist/quality/rulesets/index.js +7 -0
- package/dist/quality/rulesets/index.js.map +1 -0
- package/dist/quality/rulesets/pl/ruleset.d.ts +3 -0
- package/dist/quality/rulesets/pl/ruleset.d.ts.map +1 -0
- package/dist/quality/rulesets/pl/ruleset.js +114 -0
- package/dist/quality/rulesets/pl/ruleset.js.map +1 -0
- package/dist/quality/rulesets/types.d.ts +28 -0
- package/dist/quality/rulesets/types.d.ts.map +1 -0
- package/dist/quality/rulesets/types.js +2 -0
- package/dist/quality/rulesets/types.js.map +1 -0
- package/dist/quality/settings.d.ts +26 -0
- package/dist/quality/settings.d.ts.map +1 -0
- package/dist/quality/settings.js +32 -0
- package/dist/quality/settings.js.map +1 -0
- package/dist/quality/text.d.ts +43 -0
- package/dist/quality/text.d.ts.map +1 -0
- package/dist/quality/text.js +85 -0
- package/dist/quality/text.js.map +1 -0
- package/dist/render/glossary.d.ts +27 -0
- package/dist/render/glossary.d.ts.map +1 -0
- package/dist/render/glossary.js +71 -0
- package/dist/render/glossary.js.map +1 -0
- package/dist/render/images.d.ts +42 -0
- package/dist/render/images.d.ts.map +1 -0
- package/dist/render/images.js +84 -0
- package/dist/render/images.js.map +1 -0
- package/dist/render/index.d.ts +6 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +8 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/reading-time.d.ts +8 -0
- package/dist/render/reading-time.d.ts.map +1 -0
- package/dist/render/reading-time.js +17 -0
- package/dist/render/reading-time.js.map +1 -0
- package/dist/render/render-article.d.ts +97 -0
- package/dist/render/render-article.d.ts.map +1 -0
- package/dist/render/render-article.js +373 -0
- package/dist/render/render-article.js.map +1 -0
- package/dist/render/slugify-heading.d.ts +2 -0
- package/dist/render/slugify-heading.d.ts.map +1 -0
- package/dist/render/slugify-heading.js +21 -0
- package/dist/render/slugify-heading.js.map +1 -0
- package/dist/server/health.d.ts +3 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +11 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +10 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +13 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/og-fonts.d.ts +24 -0
- package/dist/server/og-fonts.d.ts.map +1 -0
- package/dist/server/og-fonts.js +85 -0
- package/dist/server/og-fonts.js.map +1 -0
- package/dist/server/options.d.ts +17 -0
- package/dist/server/options.d.ts.map +1 -0
- package/dist/server/options.js +56 -0
- package/dist/server/options.js.map +1 -0
- package/dist/sitemap.d.ts +11 -0
- package/dist/sitemap.d.ts.map +1 -0
- package/dist/sitemap.js +35 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/ui/blog-article.d.ts +49 -0
- package/dist/ui/blog-article.d.ts.map +1 -0
- package/dist/ui/blog-article.js +45 -0
- package/dist/ui/blog-article.js.map +1 -0
- package/dist/ui/blog-glossary.d.ts +27 -0
- package/dist/ui/blog-glossary.d.ts.map +1 -0
- package/dist/ui/blog-glossary.js +20 -0
- package/dist/ui/blog-glossary.js.map +1 -0
- package/dist/ui/blog-layout.d.ts +31 -0
- package/dist/ui/blog-layout.d.ts.map +1 -0
- package/dist/ui/blog-layout.js +25 -0
- package/dist/ui/blog-layout.js.map +1 -0
- package/dist/ui/blog-listing.d.ts +15 -0
- package/dist/ui/blog-listing.d.ts.map +1 -0
- package/dist/ui/blog-listing.js +29 -0
- package/dist/ui/blog-listing.js.map +1 -0
- package/dist/ui/blog-method.d.ts +5 -0
- package/dist/ui/blog-method.d.ts.map +1 -0
- package/dist/ui/blog-method.js +21 -0
- package/dist/ui/blog-method.js.map +1 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +8 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/page-context.d.ts +15 -0
- package/dist/ui/page-context.d.ts.map +1 -0
- package/dist/ui/page-context.js +2 -0
- package/dist/ui/page-context.js.map +1 -0
- package/migrations/0001_create_articles.sql +67 -0
- package/migrations/README.md +8 -0
- package/module.json +28 -0
- package/package.json +102 -4
- package/skill/SKILL.md +72 -0
- package/skill/references/reviewer.md +39 -0
- package/skill/references/rules.md +122 -0
- package/skill/references/structure.md +69 -0
- package/skill/references/template.md +86 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/command.ts +49 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/run.ts +591 -0
- package/src/cli/skill.ts +242 -0
- package/src/content/article-file.ts +184 -0
- package/src/contract.ts +88 -0
- package/src/db/articles.ts +160 -0
- package/src/db/publish-run.ts +224 -0
- package/src/db/schema.ts +36 -0
- package/src/discovery/dates.ts +27 -0
- package/src/discovery/index.ts +18 -0
- package/src/discovery/indexnow.ts +25 -0
- package/src/discovery/refresh.ts +79 -0
- package/src/discovery/related.ts +35 -0
- package/src/discovery/rss.ts +77 -0
- package/src/discovery/sitemap.ts +59 -0
- package/src/discovery/submit.ts +69 -0
- package/src/index.ts +99 -0
- package/src/messages/en.ts +82 -0
- package/src/messages/index.ts +7 -0
- package/src/messages/pl.ts +77 -0
- package/src/next/context.ts +30 -0
- package/src/next/data.ts +55 -0
- package/src/next/discovery.ts +49 -0
- package/src/next/index.ts +25 -0
- package/src/next/next-modules.d.ts +15 -0
- package/src/next/og-fonts.ts +14 -0
- package/src/next/og-image.tsx +108 -0
- package/src/next/pages.tsx +251 -0
- package/src/next/refresh.ts +85 -0
- package/src/options.ts +224 -0
- package/src/pages/body.ts +38 -0
- package/src/pages/dates.ts +39 -0
- package/src/pages/index.ts +37 -0
- package/src/pages/json-ld.ts +120 -0
- package/src/pages/listing.ts +95 -0
- package/src/pages/paths.ts +84 -0
- package/src/pages/redirects.ts +120 -0
- package/src/proxy/index.ts +66 -0
- package/src/quality/blocks.ts +130 -0
- package/src/quality/catalog.ts +86 -0
- package/src/quality/check-article.ts +112 -0
- package/src/quality/check-files.ts +63 -0
- package/src/quality/external-links.ts +37 -0
- package/src/quality/finding.ts +29 -0
- package/src/quality/gate.ts +15 -0
- package/src/quality/index.ts +28 -0
- package/src/quality/link-targets.ts +107 -0
- package/src/quality/options.ts +105 -0
- package/src/quality/plugin.ts +43 -0
- package/src/quality/rules/blocks.ts +43 -0
- package/src/quality/rules/images.ts +34 -0
- package/src/quality/rules/input.ts +12 -0
- package/src/quality/rules/links.ts +56 -0
- package/src/quality/rules/structure.ts +137 -0
- package/src/quality/rules/style.ts +137 -0
- package/src/quality/rules/ymyl.ts +57 -0
- package/src/quality/rulesets/en/ruleset.ts +117 -0
- package/src/quality/rulesets/index.ts +9 -0
- package/src/quality/rulesets/pl/ruleset.ts +126 -0
- package/src/quality/rulesets/types.ts +31 -0
- package/src/quality/settings.ts +60 -0
- package/src/quality/text.ts +113 -0
- package/src/render/glossary.ts +103 -0
- package/src/render/images.ts +110 -0
- package/src/render/index.ts +29 -0
- package/src/render/reading-time.ts +18 -0
- package/src/render/render-article.ts +487 -0
- package/src/render/slugify-heading.ts +22 -0
- package/src/server/health.ts +12 -0
- package/src/server/index.ts +28 -0
- package/src/server/og-fonts.ts +102 -0
- package/src/server/options.ts +62 -0
- package/src/sitemap.ts +36 -0
- package/src/ui/blog-article.tsx +186 -0
- package/src/ui/blog-glossary.tsx +104 -0
- package/src/ui/blog-layout.tsx +89 -0
- package/src/ui/blog-listing.tsx +95 -0
- package/src/ui/blog-method.tsx +34 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/page-context.ts +17 -0
- package/styles.css +498 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruleset.d.ts","sourceRoot":"","sources":["../../../../src/quality/rulesets/pl/ruleset.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAInD,eAAO,MAAM,SAAS,EAAE,eAoHvB,CAAC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The Polish ruleset (FIRE_TRACKER `src/lib/blog/quality/rules-style.ts`, `text.ts`). A translation and
|
|
2
|
+
// extension of `blader/humanizer` and `hardikpandya/stop-slop`. Errors are habits a careful author uses
|
|
3
|
+
// rarely and on purpose; warnings are words that say nothing alone but betray a template in a cluster.
|
|
4
|
+
// FIRE's voice (no first person singular) and finance phrases are app config, not Polish.
|
|
5
|
+
import { wordPattern } from "../../text.js";
|
|
6
|
+
const L = "\\p{L}*";
|
|
7
|
+
export const plRuleset = {
|
|
8
|
+
language: "pl",
|
|
9
|
+
patterns: [
|
|
10
|
+
{
|
|
11
|
+
id: "announcement",
|
|
12
|
+
severity: "error",
|
|
13
|
+
pattern: wordPattern("(?:warto|należy) (?:zauważyć|podkreślić|wspomnieć|nadmienić|dodać|zaznaczyć)"),
|
|
14
|
+
message: "an announcement instead of a fact; write the fact",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "these-days",
|
|
18
|
+
severity: "error",
|
|
19
|
+
pattern: wordPattern(`w dzisiejszych czasach|w dzisiejszym (?:świecie|dynamicznym${L})|w dobie|w obecnych czasach`),
|
|
20
|
+
message: "an empty opener about \"these times\"; start with a number or the answer",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "not-only-but-also",
|
|
24
|
+
severity: "error",
|
|
25
|
+
pattern: wordPattern("nie tylko[^.!?]{1,120}?,? (?:ale|lecz) (?:także|również|też|i)"),
|
|
26
|
+
message: "\"not only…, but also\"; name both things plainly",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "not-x-but-y",
|
|
30
|
+
severity: "error",
|
|
31
|
+
pattern: wordPattern("(?:to nie (?:jest )?[^.!?,]{1,60}, (?:to|tylko|lecz)|nie chodzi o [^.!?]{1,80}[.,] (?:chodzi o|tylko o|lecz o))"),
|
|
32
|
+
message: "the \"it is not X, it is Y\" contrast; say Y right away",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "meta-commentary",
|
|
36
|
+
severity: "error",
|
|
37
|
+
pattern: wordPattern("w (?:tym|niniejszym) (?:artykule|tekście|wpisie|poradniku)|przyjrzyjmy się|zanurzmy się|przejdźmy (?:teraz )?do|jak (?:już )?wspomniano|jak zaraz zobaczysz|jak zobaczymy|dowiesz się (?:z tego|w tym)"),
|
|
38
|
+
message: "the text talks about itself instead of the topic; remove the announcement",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "throat-clearing",
|
|
42
|
+
severity: "error",
|
|
43
|
+
pattern: wordPattern("prawda jest taka|oto (?:dlaczego|jak|co|czemu)|i tu pojawia się|co ciekawe|co ważne|co istotne|nie da się ukryć|nie ulega wątpliwości|bez wątpienia|niewątpliwie|bezsprzecznie"),
|
|
44
|
+
message: "a run-up before the point; cut it and start with the point",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "empty-conclusion",
|
|
48
|
+
severity: "error",
|
|
49
|
+
pattern: wordPattern("podsumowując|reasumując|w podsumowaniu|na zakończenie|konkludując|krótko podsumowując"),
|
|
50
|
+
message: "an empty conclusion; end with the last concrete fact",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "crucial",
|
|
54
|
+
severity: "error",
|
|
55
|
+
pattern: wordPattern(`kluczow${L}`),
|
|
56
|
+
message: "\"crucial\", the most common Polish tic of language models; name what depends on the thing",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "plays-a-role",
|
|
60
|
+
severity: "error",
|
|
61
|
+
pattern: wordPattern(`odgryw${L} (?:\\p{L}+ )?rolę`),
|
|
62
|
+
message: "\"plays a role\"; write what the thing does",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "puffery",
|
|
66
|
+
severity: "error",
|
|
67
|
+
pattern: wordPattern(`kamień milowy|przełomow${L}|rewolucyjn${L}|game[- ]changer${L}|klucz do sukcesu|nieocenion${L}|holistyczn${L}|kompleksow${L}|szerok${L} (?:wachlarz${L}|gam${L}|spektrum)|w gąszczu|dynamicznie zmieniając${L}|nie ma jednej (?:dobrej |właściwej |uniwersalnej |prostej )?odpowiedzi|zależy od wielu czynników`),
|
|
68
|
+
message: "an inflated or empty phrase; replace it with a fact",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "chatbot-phrases",
|
|
72
|
+
severity: "error",
|
|
73
|
+
pattern: wordPattern("mam nadzieję, że|daj (?:mi )?znać|chętnie pomogę|świetne pytanie|jako model językowy|według mojej wiedzy|na dzień mojej wiedzy|w razie pytań"),
|
|
74
|
+
message: "a leftover of a chat with an assistant; remove it",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "emoji",
|
|
78
|
+
severity: "error",
|
|
79
|
+
pattern: /\p{Extended_Pictographic}/gu,
|
|
80
|
+
message: "an emoji in the text; remove it",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "filler-words",
|
|
84
|
+
severity: "warning",
|
|
85
|
+
pattern: wordPattern(`ponadto|dodatkowo|co więcej|istotn${L}|fundamentaln${L}|innymi słowy|krótko mówiąc|w rezultacie|niezwykle|niezmiernie|naprawdę|absolutnie|zdecydowanie|dosłownie|po prostu|optymaln${L}|efektywn${L}|pamiętaj, że|warto`),
|
|
86
|
+
message: "a filler word; check whether the sentence says the same without it",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "exclamation",
|
|
90
|
+
severity: "warning",
|
|
91
|
+
pattern: /!/g,
|
|
92
|
+
message: "an exclamation mark; a guide speaks calmly",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "straight-quotes",
|
|
96
|
+
severity: "warning",
|
|
97
|
+
pattern: /[“"]/g,
|
|
98
|
+
message: "an English quotation mark; Polish uses „…”",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
firstPersonSingular: wordPattern("ja|mnie|mój|moja|moje|mojego|mojej|moim|moich|moją|mi|uważam|sądzę|myślę|moim zdaniem|osobiście"),
|
|
102
|
+
profitPromises: wordPattern(`gwarantowan${L} (?:zysk${L}|zwrot${L}|dochód${L}|zarobek${L})|pewn${L} zysk${L}|(?<!nie (?:jest|są|ma|będzie) )bez (?:żadnego )?ryzyka|zero ryzyka|nie (?:możesz|można) (?:nic )?stracić|na pewno zarobisz|zarobisz na pewno|kup(?:uj)? (?:teraz|już dziś)|musisz (?:kupić|zainwestować)|szybk${L} (?:zysk${L}|wzbogacen${L})|łatw${L} pieniądz${L}|bez wysiłku`),
|
|
103
|
+
notation: {
|
|
104
|
+
number: /\d{1,3}(?:[ \u00a0\u202f]\d{3})+(?:,\d+)?|\d+(?:,\d+)?/g,
|
|
105
|
+
unitAfter: /^\s?(?:%|proc|zł|złotych|zl\b|PLN|tys|mln|mld|groszy|gr\b)/iu,
|
|
106
|
+
unitBefore: null,
|
|
107
|
+
referenceBefore: /(?:art\.|ust\.|poz\.|pkt|§|nr|Dz\.\s?U\.?|M\.P\.)\s*$/iu,
|
|
108
|
+
thousandsSeparator: /[ \u00a0\u202f]/,
|
|
109
|
+
decimalSeparator: ",",
|
|
110
|
+
},
|
|
111
|
+
triad: /(?<![\p{L}])\p{L}+, \p{L}+,? (?:i|oraz) \p{L}+(?![\p{L}])/gu,
|
|
112
|
+
flagsTitleCaseHeadings: true,
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=ruleset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruleset.js","sourceRoot":"","sources":["../../../../src/quality/rulesets/pl/ruleset.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { QualitySeverity } from "../finding.js";
|
|
2
|
+
import type { NumberNotation } from "../text.js";
|
|
3
|
+
export declare const QUALITY_LANGUAGES: readonly ["en", "pl"];
|
|
4
|
+
export type QualityLanguage = (typeof QUALITY_LANGUAGES)[number];
|
|
5
|
+
export interface StylePattern {
|
|
6
|
+
/** The rule id; the same id in two rulesets names the same habit. */
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly severity: QualitySeverity;
|
|
9
|
+
/** Global, so every hit counts. */
|
|
10
|
+
readonly pattern: RegExp;
|
|
11
|
+
/** What is wrong and what to write instead. */
|
|
12
|
+
readonly message: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LanguageRuleset {
|
|
15
|
+
readonly language: QualityLanguage;
|
|
16
|
+
/** AI-writing and generic style patterns. Errors: one hit is enough. Warnings: reported once per rule with a count. */
|
|
17
|
+
readonly patterns: readonly StylePattern[];
|
|
18
|
+
/** First person singular, for apps whose texts are signed by an editorial team (`voice.forbidFirstPersonSingular`). */
|
|
19
|
+
readonly firstPersonSingular: RegExp;
|
|
20
|
+
/** Promises of profit and orders to buy (YMYL). */
|
|
21
|
+
readonly profitPromises: RegExp;
|
|
22
|
+
readonly notation: NumberNotation;
|
|
23
|
+
/** An enumeration of three ("a, b and c"); more than three in a text reads as a template. */
|
|
24
|
+
readonly triad: RegExp;
|
|
25
|
+
/** Whether Title Case Headings are a mistake in this language. */
|
|
26
|
+
readonly flagsTitleCaseHeadings: boolean;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/quality/rulesets/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,eAAO,MAAM,iBAAiB,uBAAwB,CAAC;AACvD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,mCAAmC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,uHAAuH;IACvH,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,uHAAuH;IACvH,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,mDAAmD;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,6FAA6F;IAC7F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/quality/rulesets/types.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,IAAI,CAAU,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import type { ArticleImagePolicy } from "../render/images.js";
|
|
3
|
+
import type { QualitySeverity } from "./finding.js";
|
|
4
|
+
import type { QualityOptions } from "./options.js";
|
|
5
|
+
import type { LanguageRuleset, StylePattern } from "./rulesets/types.js";
|
|
6
|
+
export interface QualitySettings {
|
|
7
|
+
readonly options: QualityOptions;
|
|
8
|
+
readonly ruleset: LanguageRuleset;
|
|
9
|
+
/** The app's voice phrases plus, when switched on, the ruleset's first person singular. */
|
|
10
|
+
readonly voicePatterns: readonly StylePattern[];
|
|
11
|
+
/** Origins whose absolute links are internal: the app's `appOrigin`, the site origin and `ownOrigins`. */
|
|
12
|
+
readonly ownOrigins: readonly string[];
|
|
13
|
+
/** IANA time zone of "today". */
|
|
14
|
+
readonly timeZone: string;
|
|
15
|
+
/** The app's image policy (`blog({ images })`); `null` when bodies may show no image. */
|
|
16
|
+
readonly images: ArticleImagePolicy | null;
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveQualitySettings(options: QualityOptions, config: Pick<SoftureConfig, "appOrigin" | "timezone"> & {
|
|
19
|
+
/** The canonical site origin (core's `getSiteUrls(config).origin`) when it is not `appOrigin`. */
|
|
20
|
+
readonly siteOrigin?: string;
|
|
21
|
+
}, images?: ArticleImagePolicy | null): QualitySettings;
|
|
22
|
+
/** `YYYY-MM-DD` of a moment in a time zone. */
|
|
23
|
+
export declare function getLocalDate(moment: Date, timeZone: string): string;
|
|
24
|
+
/** The severity a finding of `rule` gets: the override, else its own; `null` for "off". */
|
|
25
|
+
export declare function getEffectiveSeverity(settings: QualitySettings, rule: string, severity: QualitySeverity): QualitySeverity | null;
|
|
26
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/quality/settings.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEzE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,2FAA2F;IAC3F,QAAQ,CAAC,aAAa,EAAE,SAAS,YAAY,EAAE,CAAC;IAChD,0GAA0G;IAC1G,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,iCAAiC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAC5C;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG;IACtD,kGAAkG;IAClG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,EACD,MAAM,GAAE,kBAAkB,GAAG,IAAW,GACvC,eAAe,CAkBjB;AAED,+CAA+C;AAC/C,wBAAgB,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,2FAA2F;AAC3F,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,eAAe,GAAG,IAAI,CAI/H"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { QUALITY_RULESETS } from "./rulesets/index.js";
|
|
2
|
+
export function resolveQualitySettings(options, config, images = null) {
|
|
3
|
+
const ruleset = QUALITY_RULESETS[options.language];
|
|
4
|
+
const voicePatterns = options.voice.phrases.map((phrase) => ({
|
|
5
|
+
id: phrase.id,
|
|
6
|
+
severity: phrase.severity,
|
|
7
|
+
pattern: phrase.pattern,
|
|
8
|
+
message: phrase.message,
|
|
9
|
+
}));
|
|
10
|
+
if (options.voice.forbidFirstPersonSingular) {
|
|
11
|
+
voicePatterns.push({
|
|
12
|
+
id: "first-person-singular",
|
|
13
|
+
severity: "error",
|
|
14
|
+
pattern: ruleset.firstPersonSingular,
|
|
15
|
+
message: "first person singular; the texts are signed by the editors (\"we\", \"you\"), never \"I\"",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const origins = [config.appOrigin, ...(config.siteOrigin === undefined ? [] : [config.siteOrigin]), ...options.ownOrigins].map((origin) => new URL(origin).origin);
|
|
19
|
+
return { options, ruleset, voicePatterns, ownOrigins: [...new Set(origins)], timeZone: config.timezone, images };
|
|
20
|
+
}
|
|
21
|
+
/** `YYYY-MM-DD` of a moment in a time zone. */
|
|
22
|
+
export function getLocalDate(moment, timeZone) {
|
|
23
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(moment);
|
|
24
|
+
}
|
|
25
|
+
/** The severity a finding of `rule` gets: the override, else its own; `null` for "off". */
|
|
26
|
+
export function getEffectiveSeverity(settings, rule, severity) {
|
|
27
|
+
const override = settings.options.severity[rule];
|
|
28
|
+
if (override === undefined)
|
|
29
|
+
return severity;
|
|
30
|
+
return override === "off" ? null : override;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/quality/settings.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAgBvD,MAAM,UAAU,sBAAsB,CACpC,OAAuB,EACvB,MAGC,EACD,SAAoC,IAAI;IAExC,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnD,MAAM,aAAa,GAAmB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC3E,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,MAAM,CAAC,OAAO;KACxB,CAAC,CAAC,CAAC;IACJ,IAAI,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,CAAC;QAC5C,aAAa,CAAC,IAAI,CAAC;YACjB,EAAE,EAAE,uBAAuB;YAC3B,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,OAAO,CAAC,mBAAmB;YACpC,OAAO,EAAE,2FAA2F;SACrG,CAAC,CAAC;IACL,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;IACnK,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;AACnH,CAAC;AAED,+CAA+C;AAC/C,MAAM,UAAU,YAAY,CAAC,MAAY,EAAE,QAAgB;IACzD,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC1H,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,oBAAoB,CAAC,QAAyB,EAAE,IAAY,EAAE,QAAyB;IACrG,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAC5C,OAAO,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface MarkdownLink {
|
|
2
|
+
readonly text: string;
|
|
3
|
+
readonly url: string;
|
|
4
|
+
}
|
|
5
|
+
/** How a language writes numbers, and which numbers count as facts that need a source. */
|
|
6
|
+
export interface NumberNotation {
|
|
7
|
+
/** A number token; global. */
|
|
8
|
+
readonly number: RegExp;
|
|
9
|
+
/** A unit right after the number (`%`, a currency, "million"); tested on the text that follows. */
|
|
10
|
+
readonly unitAfter: RegExp;
|
|
11
|
+
/** A unit right before the number (`$`); tested on the text before. `null`: none. */
|
|
12
|
+
readonly unitBefore: RegExp | null;
|
|
13
|
+
/** A legal or list reference before the number ("art.", "§", "no."), which needs no source. */
|
|
14
|
+
readonly referenceBefore: RegExp;
|
|
15
|
+
/** The digits to remove before reading the number (thousands separators). */
|
|
16
|
+
readonly thousandsSeparator: RegExp;
|
|
17
|
+
readonly decimalSeparator: "," | ".";
|
|
18
|
+
}
|
|
19
|
+
export declare function findLinks(text: string): MarkdownLink[];
|
|
20
|
+
export declare function findFootnoteRefs(text: string): string[];
|
|
21
|
+
/** Bare URLs in a text (a footnote definition), without trailing punctuation. */
|
|
22
|
+
export declare function findBareUrls(text: string): string[];
|
|
23
|
+
/** The prose a reader sees: no code, images, link targets, footnote refs, URLs or emphasis markers. */
|
|
24
|
+
export declare function toProse(text: string): string;
|
|
25
|
+
export declare function countWords(prose: string): number;
|
|
26
|
+
/**
|
|
27
|
+
* Sentences of prose. A boundary is `.`, `!`, `?` or `…` before an upper-case letter, a quote or a
|
|
28
|
+
* bracket, never before a digit, so "art. 27" and "approx. 300" do not cut a sentence.
|
|
29
|
+
*/
|
|
30
|
+
export declare function splitSentences(prose: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* The numbers that need a source: amounts, percentages and numbers from 1000 up. Ages, years
|
|
33
|
+
* (1900–2100), small counts and legal references ("art. 27") need none, or every text about law
|
|
34
|
+
* would fail.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findSignificantNumbers(prose: string, notation: NumberNotation): string[];
|
|
37
|
+
/** "28 260,50" (pl) or "28,260.50" (en) → 28260.5. */
|
|
38
|
+
export declare function parseNumber(raw: string, notation: NumberNotation): number;
|
|
39
|
+
/** A number without thousands separators, to compare a text with a table. */
|
|
40
|
+
export declare function normalizeNumber(raw: string, notation: NumberNotation): string;
|
|
41
|
+
/** A case-insensitive pattern with word edges that know non-ASCII letters (`\b` knows only ASCII). */
|
|
42
|
+
export declare function wordPattern(source: string): RegExp;
|
|
43
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/quality/text.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,8BAA8B;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mGAAmG;IACnG,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,+FAA+F;IAC/F,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,GAAG,GAAG,GAAG,CAAC;CACtC;AAUD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAEtD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEnD;AAED,uGAAuG;AACvG,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAY5C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAKtD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,EAAE,CAgBxF;AAED,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,CAGzE;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,CAE7E;AAED,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElD"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Text helpers of the gate (FIRE_TRACKER `src/lib/blog/quality/text.ts`): prose without markup, words,
|
|
2
|
+
// sentences, links, footnote references and the numbers that need a source. Number notation comes
|
|
3
|
+
// from the language ruleset, so "28 260,50" and "28,260.50" both read as one number.
|
|
4
|
+
// No `[` inside link text, link target or footnote id: each match attempt ends at the next `[`, so a
|
|
5
|
+
// long run of unclosed brackets stays linear instead of rescanning the rest of the text per bracket.
|
|
6
|
+
// An image (``) is not a link: the lookbehind skips it, and `IMAGE` removes it from prose.
|
|
7
|
+
const LINK = /(?<!!)\[([^[\]]*)\]\(([^)\s[]+)(?:\s+"[^"]*")?\)/g;
|
|
8
|
+
const IMAGE = /!\[[^[\]]*\]\([^)\s[]+(?:\s+"[^"]*")?\)/g;
|
|
9
|
+
const FOOTNOTE_REF = /\[\^([^[\]]+)\]/g;
|
|
10
|
+
const URL_IN_TEXT = /https?:\/\/[^\s)>\]]+/g;
|
|
11
|
+
export function findLinks(text) {
|
|
12
|
+
return [...text.matchAll(LINK)].map((match) => ({ text: match[1] ?? "", url: match[2] ?? "" }));
|
|
13
|
+
}
|
|
14
|
+
export function findFootnoteRefs(text) {
|
|
15
|
+
return [...text.matchAll(FOOTNOTE_REF)].map((match) => match[1] ?? "");
|
|
16
|
+
}
|
|
17
|
+
/** Bare URLs in a text (a footnote definition), without trailing punctuation. */
|
|
18
|
+
export function findBareUrls(text) {
|
|
19
|
+
return [...text.matchAll(URL_IN_TEXT)].map((match) => match[0].replace(/[.,;]$/, ""));
|
|
20
|
+
}
|
|
21
|
+
/** The prose a reader sees: no code, images, link targets, footnote refs, URLs or emphasis markers. */
|
|
22
|
+
export function toProse(text) {
|
|
23
|
+
return text
|
|
24
|
+
.replace(/`[^`]*`/g, " ")
|
|
25
|
+
.replace(IMAGE, " ")
|
|
26
|
+
.replace(LINK, "$1")
|
|
27
|
+
.replace(FOOTNOTE_REF, "")
|
|
28
|
+
.replace(/<https?:\/\/[^>]+>/g, " ")
|
|
29
|
+
.replace(/https?:\/\/\S+/g, " ")
|
|
30
|
+
.replace(/\*\*|__/g, "")
|
|
31
|
+
.replace(/(?<![\p{L}\d])[*_](?=\S)|(?<=\S)[*_](?![\p{L}\d])/gu, "")
|
|
32
|
+
.replace(/\s+/g, " ")
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
export function countWords(prose) {
|
|
36
|
+
return prose.match(/[\p{L}\d][\p{L}\d'’-]*/gu)?.length ?? 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sentences of prose. A boundary is `.`, `!`, `?` or `…` before an upper-case letter, a quote or a
|
|
40
|
+
* bracket, never before a digit, so "art. 27" and "approx. 300" do not cut a sentence.
|
|
41
|
+
*/
|
|
42
|
+
export function splitSentences(prose) {
|
|
43
|
+
return prose
|
|
44
|
+
.split(/(?<=[.!?…])\s+(?=[\p{Lu}„“"(])/u)
|
|
45
|
+
.map((sentence) => sentence.trim())
|
|
46
|
+
.filter((sentence) => sentence.length > 0);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The numbers that need a source: amounts, percentages and numbers from 1000 up. Ages, years
|
|
50
|
+
* (1900–2100), small counts and legal references ("art. 27") need none, or every text about law
|
|
51
|
+
* would fail.
|
|
52
|
+
*/
|
|
53
|
+
export function findSignificantNumbers(prose, notation) {
|
|
54
|
+
const found = [];
|
|
55
|
+
for (const match of prose.matchAll(new RegExp(notation.number.source, "g"))) {
|
|
56
|
+
const raw = match[0];
|
|
57
|
+
const before = prose.slice(Math.max(0, match.index - 12), match.index);
|
|
58
|
+
const after = prose.slice(match.index + raw.length, match.index + raw.length + 12);
|
|
59
|
+
if (/[\p{L}\d]$/u.test(before) || notation.referenceBefore.test(before))
|
|
60
|
+
continue;
|
|
61
|
+
if (notation.unitAfter.test(after) || (notation.unitBefore?.test(before) ?? false)) {
|
|
62
|
+
found.push(raw);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const value = parseNumber(raw, notation);
|
|
66
|
+
const looksLikeYear = /^\d{4}$/.test(raw) && value >= 1900 && value <= 2100;
|
|
67
|
+
if (value >= 1000 && !looksLikeYear)
|
|
68
|
+
found.push(raw);
|
|
69
|
+
}
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
/** "28 260,50" (pl) or "28,260.50" (en) → 28260.5. */
|
|
73
|
+
export function parseNumber(raw, notation) {
|
|
74
|
+
const digits = raw.replace(new RegExp(notation.thousandsSeparator.source, "g"), "");
|
|
75
|
+
return Number(notation.decimalSeparator === "," ? digits.replace(",", ".") : digits);
|
|
76
|
+
}
|
|
77
|
+
/** A number without thousands separators, to compare a text with a table. */
|
|
78
|
+
export function normalizeNumber(raw, notation) {
|
|
79
|
+
return raw.replace(new RegExp(notation.thousandsSeparator.source, "g"), "");
|
|
80
|
+
}
|
|
81
|
+
/** A case-insensitive pattern with word edges that know non-ASCII letters (`\b` knows only ASCII). */
|
|
82
|
+
export function wordPattern(source) {
|
|
83
|
+
return new RegExp(`(?<![\\p{L}\\d])(?:${source})(?![\\p{L}\\d])`, "giu");
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/quality/text.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BlogArticle } from "../contract.js";
|
|
2
|
+
export interface GlossaryTerm {
|
|
3
|
+
readonly slug: string;
|
|
4
|
+
readonly forms: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export interface TermMatch {
|
|
7
|
+
readonly index: number;
|
|
8
|
+
readonly text: string;
|
|
9
|
+
readonly slug: string;
|
|
10
|
+
}
|
|
11
|
+
/** Every occurrence of a form in the text, left to right, without overlaps. */
|
|
12
|
+
export type TermMatcher = (text: string) => TermMatch[];
|
|
13
|
+
export declare function createTermMatcher(terms: readonly GlossaryTerm[]): TermMatcher;
|
|
14
|
+
/** A form that two or more terms claim; `slugs` sorted, the form as the first of them writes it. */
|
|
15
|
+
export interface TermFormConflict {
|
|
16
|
+
readonly form: string;
|
|
17
|
+
readonly slugs: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Forms claimed by more than one term. Two forms collide for the matcher exactly when they are equal
|
|
21
|
+
* after a capital first letter ("ike" and "Ike" do; "IKE" and "Ike" do not), so that is the key. A term
|
|
22
|
+
* that lists one form twice is no conflict. Sorted by form, for stable messages.
|
|
23
|
+
*/
|
|
24
|
+
export declare function findTermFormConflicts(terms: readonly GlossaryTerm[]): TermFormConflict[];
|
|
25
|
+
/** The glossary of a site from its stored rows: every `kind = "term"` row with its forms. */
|
|
26
|
+
export declare function toGlossary(articles: readonly Pick<BlogArticle, "kind" | "slug" | "termForms">[]): GlossaryTerm[];
|
|
27
|
+
//# sourceMappingURL=glossary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glossary.d.ts","sourceRoot":"","sources":["../../src/render/glossary.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,+EAA+E;AAC/E,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;AAUxD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,WAAW,CA4B7E;AAED,oGAAoG;AACpG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAoBxF;AAED,6FAA6F;AAC7F,wBAAgB,UAAU,CAAC,QAAQ,EAAE,SAAS,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC,EAAE,GAAG,YAAY,EAAE,CAIhH"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
function escapeRegExp(text) {
|
|
2
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
+
}
|
|
4
|
+
function capitalize(text) {
|
|
5
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
6
|
+
}
|
|
7
|
+
export function createTermMatcher(terms) {
|
|
8
|
+
const slugByForm = new Map();
|
|
9
|
+
for (const term of terms) {
|
|
10
|
+
for (const form of term.forms) {
|
|
11
|
+
const trimmed = form.trim();
|
|
12
|
+
if (trimmed === "")
|
|
13
|
+
continue;
|
|
14
|
+
// The first term with a given form wins: two terms with the same phrase are an editorial
|
|
15
|
+
// mistake (`findTermFormConflicts`: the publish run refuses it and `check` reports it), and the
|
|
16
|
+
// link has to be deterministic for rows stored before that check.
|
|
17
|
+
for (const variant of [trimmed, capitalize(trimmed)]) {
|
|
18
|
+
if (!slugByForm.has(variant))
|
|
19
|
+
slugByForm.set(variant, term.slug);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (slugByForm.size === 0)
|
|
24
|
+
return () => [];
|
|
25
|
+
const alternatives = [...slugByForm.keys()]
|
|
26
|
+
.sort((a, b) => b.length - a.length || a.localeCompare(b))
|
|
27
|
+
.map(escapeRegExp)
|
|
28
|
+
.join("|");
|
|
29
|
+
const pattern = new RegExp(`(?<![\\p{L}\\p{N}])(?:${alternatives})(?![\\p{L}\\p{N}])`, "gu");
|
|
30
|
+
return (text) => [...text.matchAll(pattern)].map((match) => ({
|
|
31
|
+
index: match.index,
|
|
32
|
+
text: match[0],
|
|
33
|
+
// Every alternative of the pattern is a key of the map.
|
|
34
|
+
slug: slugByForm.get(match[0]) ?? "",
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Forms claimed by more than one term. Two forms collide for the matcher exactly when they are equal
|
|
39
|
+
* after a capital first letter ("ike" and "Ike" do; "IKE" and "Ike" do not), so that is the key. A term
|
|
40
|
+
* that lists one form twice is no conflict. Sorted by form, for stable messages.
|
|
41
|
+
*/
|
|
42
|
+
export function findTermFormConflicts(terms) {
|
|
43
|
+
const claims = new Map();
|
|
44
|
+
for (const term of terms) {
|
|
45
|
+
for (const form of term.forms) {
|
|
46
|
+
const trimmed = form.trim();
|
|
47
|
+
if (trimmed === "")
|
|
48
|
+
continue;
|
|
49
|
+
const key = capitalize(trimmed);
|
|
50
|
+
const bySlug = claims.get(key) ?? new Map();
|
|
51
|
+
if (!bySlug.has(term.slug))
|
|
52
|
+
bySlug.set(term.slug, trimmed);
|
|
53
|
+
claims.set(key, bySlug);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [...claims.values()]
|
|
57
|
+
.filter((bySlug) => bySlug.size > 1)
|
|
58
|
+
.map((bySlug) => {
|
|
59
|
+
const slugs = [...bySlug.keys()].sort();
|
|
60
|
+
// `slugs` holds the keys of the map, so the first one is there.
|
|
61
|
+
return { form: bySlug.get(slugs[0] ?? "") ?? "", slugs };
|
|
62
|
+
})
|
|
63
|
+
.sort((a, b) => a.form.localeCompare(b.form));
|
|
64
|
+
}
|
|
65
|
+
/** The glossary of a site from its stored rows: every `kind = "term"` row with its forms. */
|
|
66
|
+
export function toGlossary(articles) {
|
|
67
|
+
return articles
|
|
68
|
+
.filter((article) => article.kind === "term" && article.termForms.length > 0)
|
|
69
|
+
.map((article) => ({ slug: article.slug, forms: article.termForms }));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=glossary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glossary.js","sourceRoot":"","sources":["../../src/render/glossary.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ImageDimensions {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ArticleImagePolicy {
|
|
6
|
+
/** Hosts whose https images are allowed besides the site's own paths (`cdn.example.com` covers its subdomains). */
|
|
7
|
+
readonly hosts?: readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Width and height in pixels of an allowed source, or `null` when the app does not know them. The
|
|
10
|
+
* source is the normalised URL the page requests (percent-encoded: a space is `%20`).
|
|
11
|
+
*/
|
|
12
|
+
readonly dimensions: (src: string) => ImageDimensions | null;
|
|
13
|
+
}
|
|
14
|
+
export type ImageProblem = "source" | "alt" | "dimensions";
|
|
15
|
+
export type ImageVerdict = {
|
|
16
|
+
readonly ok: true;
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ok: false;
|
|
21
|
+
readonly problems: readonly ImageProblem[];
|
|
22
|
+
};
|
|
23
|
+
export interface ArticleImage {
|
|
24
|
+
readonly src: string;
|
|
25
|
+
readonly alt: string;
|
|
26
|
+
}
|
|
27
|
+
export interface FoundImage extends ArticleImage {
|
|
28
|
+
/** 1-based line of the block that holds the image. */
|
|
29
|
+
readonly line: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether an image follows the policy, and why not. Without a policy no image does. `dimensions` is
|
|
33
|
+
* asked only for an allowed source; a throw there is the app's bug and propagates.
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkArticleImage(image: ArticleImage, policy: ArticleImagePolicy | undefined): ImageVerdict;
|
|
36
|
+
/**
|
|
37
|
+
* The images of a text with their lines, for the quality gate. It parses like the renderer, but lets
|
|
38
|
+
* every link scheme through, so an image the renderer would drop as text (`javascript:`) is listed and
|
|
39
|
+
* reported too.
|
|
40
|
+
*/
|
|
41
|
+
export declare function findArticleImages(markdown: string): FoundImage[];
|
|
42
|
+
//# sourceMappingURL=images.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/render/images.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,mHAAmH;IACnH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,GAAG,IAAI,CAAC;CAC9D;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,KAAK,GAAG,YAAY,CAAC;AAE3D,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;CAAE,CAAC;AAEvE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAW,SAAQ,YAAY;IAC9C,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAsCD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,GAAG,SAAS,GAAG,YAAY,CAU3G;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAchE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Images in article bodies under the app's image policy: one check for the renderer (emit or not) and
|
|
2
|
+
// the quality gate (report why not). An image follows the policy when its source stays on the site (a
|
|
3
|
+
// root-relative path) or comes over https from a host the app allows, its alt text is not empty and the
|
|
4
|
+
// app knows its width and height, so the page reserves its box before it loads.
|
|
5
|
+
import MarkdownIt from "markdown-it";
|
|
6
|
+
import footnote from "markdown-it-footnote";
|
|
7
|
+
// A placeholder origin: a root-relative source must resolve to it, or it leaves the site.
|
|
8
|
+
const SITE_BASE = "https://site.invalid";
|
|
9
|
+
function isOnSite(src) {
|
|
10
|
+
if (!src.startsWith("/"))
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
return new URL(src, SITE_BASE).origin === SITE_BASE;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isOnAllowedHost(src, hosts) {
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(src);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "")
|
|
28
|
+
return false;
|
|
29
|
+
const host = url.hostname.toLowerCase();
|
|
30
|
+
return hosts.some((allowed) => {
|
|
31
|
+
const own = allowed.toLowerCase();
|
|
32
|
+
return host === own || host.endsWith(`.${own}`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function isAllowedSource(src, policy) {
|
|
36
|
+
if (policy === undefined)
|
|
37
|
+
return false;
|
|
38
|
+
return isOnSite(src) || isOnAllowedHost(src, policy.hosts ?? []);
|
|
39
|
+
}
|
|
40
|
+
function isPixelCount(value) {
|
|
41
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether an image follows the policy, and why not. Without a policy no image does. `dimensions` is
|
|
45
|
+
* asked only for an allowed source; a throw there is the app's bug and propagates.
|
|
46
|
+
*/
|
|
47
|
+
export function checkArticleImage(image, policy) {
|
|
48
|
+
const problems = [];
|
|
49
|
+
const isAllowed = isAllowedSource(image.src, policy);
|
|
50
|
+
if (!isAllowed)
|
|
51
|
+
problems.push("source");
|
|
52
|
+
if (image.alt.trim() === "")
|
|
53
|
+
problems.push("alt");
|
|
54
|
+
const size = isAllowed && policy !== undefined ? policy.dimensions(image.src) : null;
|
|
55
|
+
const hasSize = size !== null && isPixelCount(size.width) && isPixelCount(size.height);
|
|
56
|
+
if (isAllowed && !hasSize)
|
|
57
|
+
problems.push("dimensions");
|
|
58
|
+
if (problems.length > 0 || size === null)
|
|
59
|
+
return { ok: false, problems };
|
|
60
|
+
return { ok: true, width: size.width, height: size.height };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The images of a text with their lines, for the quality gate. It parses like the renderer, but lets
|
|
64
|
+
* every link scheme through, so an image the renderer would drop as text (`javascript:`) is listed and
|
|
65
|
+
* reported too.
|
|
66
|
+
*/
|
|
67
|
+
export function findArticleImages(markdown) {
|
|
68
|
+
const md = new MarkdownIt({ html: false, linkify: false, typographer: false });
|
|
69
|
+
md.use(footnote);
|
|
70
|
+
md.validateLink = () => true;
|
|
71
|
+
const images = [];
|
|
72
|
+
for (const token of md.parse(markdown, {})) {
|
|
73
|
+
if (token.type !== "inline" || token.children === null)
|
|
74
|
+
continue;
|
|
75
|
+
const line = (token.map?.[0] ?? 0) + 1;
|
|
76
|
+
for (const child of token.children) {
|
|
77
|
+
if (child.type !== "image")
|
|
78
|
+
continue;
|
|
79
|
+
images.push({ src: String(child.attrGet("src") ?? ""), alt: md.renderer.renderInlineAsText(child.children ?? [], md.options, {}), line });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return images;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=images.js.map
|