diff --git a/package-lock.json b/package-lock.json index 7cf08fd..af272e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,9 +21,11 @@ "sharp": "^0.33.0" }, "bin": { - "ghoststandard": "dist/cli.js" + "ghoststandard": "dist/cli.js", + "gs": "dist/cli.js" }, "devDependencies": { + "@atproto/lexicon": "^0.7.5", "@types/better-sqlite3": "^7.6.10", "@types/node": "^22.0.0", "tsx": "^4.19.0", @@ -46,6 +48,28 @@ "zod": "^3.23.8" } }, + "node_modules/@atproto/api/node_modules/@atproto/lexicon": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.14.tgz", + "integrity": "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.4.2", + "@atproto/syntax": "^0.4.0", + "iso-datestring-validator": "^2.2.2", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/api/node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, "node_modules/@atproto/common-web": { "version": "0.4.21", "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.21.tgz", @@ -90,25 +114,96 @@ } }, "node_modules/@atproto/lexicon": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.14.tgz", - "integrity": "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.7.5.tgz", + "integrity": "sha512-2xIeiCAfZYql0fZXw5f2Oo//T/q72crVV+cD+SnENH2/HQ0b+zdWp97aCetL0IOC3wDM1zbLWslBjoxAaJFNqQ==", + "dev": true, "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.2", - "@atproto/syntax": "^0.4.0", - "iso-datestring-validator": "^2.2.2", - "multiformats": "^9.9.0", + "@atproto/common-web": "^0.5.4", + "@atproto/syntax": "^0.7.0", + "multiformats": "^13.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@atproto/lexicon/node_modules/@atproto/common-web": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.5.4.tgz", + "integrity": "sha512-06DqJc5k8H9p2njfYVEfxMT0RWlHzVElX60wiwQSgsaVWwP5IIF6MgXXX/44VwK0bir5hKpQ/J75MpCD5v8RFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@atproto/lex-data": "^0.1.4", + "@atproto/lex-json": "^0.1.3", + "@atproto/syntax": "^0.7.0", "zod": "^3.23.8" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@atproto/lexicon/node_modules/@atproto/lex-data": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.1.4.tgz", + "integrity": "sha512-f9U95sk0zUtxHktvK59peU+Shd0cIUYV68p//GS7sfdkAxUIeaspvX6CY+Quv9Oa4aozmsXI52SjaNn1wuU8RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "multiformats": "^13.0.0", + "tslib": "^2.8.1", + "uint8arrays": "^5.0.0", + "unicode-segmenter": "^0.14.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@atproto/lexicon/node_modules/@atproto/lex-json": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.1.3.tgz", + "integrity": "sha512-Ch2w9bCLFOwWINFFxpZo6DBW7+ZxkehciU3P8N94gSyENLe3/5WWXHdHvkiH7EXZrpI7fKY8nVWn4GIL0ttqxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@atproto/lex-data": "^0.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22" } }, "node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.7.0.tgz", + "integrity": "sha512-bKilYbACYg87TgKa60NNoM1cysQxN9ikgIrgKQH4OpeXVN2TTb0qxqLDbxiU/rp+ugdrf4JYl6UyrQSM+IagHg==", + "dev": true, "license": "MIT", "dependencies": { + "iso-datestring-validator": "^2.2.2", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@atproto/lexicon/node_modules/multiformats": { + "version": "13.4.2", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.2.tgz", + "integrity": "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==", + "dev": true, + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@atproto/lexicon/node_modules/uint8arrays": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.1.tgz", + "integrity": "sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" } }, "node_modules/@atproto/syntax": { @@ -127,6 +222,28 @@ "zod": "^3.23.8" } }, + "node_modules/@atproto/xrpc/node_modules/@atproto/lexicon": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.14.tgz", + "integrity": "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.4.2", + "@atproto/syntax": "^0.4.0", + "iso-datestring-validator": "^2.2.2", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/xrpc/node_modules/@atproto/syntax": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", diff --git a/package.json b/package.json index 44acb59..d5abe0a 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,15 @@ "license": "MIT", "type": "module", "bin": { - "gs": "./dist/cli.js", + "gs": "./dist/cli.js", "ghoststandard": "./dist/cli.js" }, "scripts": { "dev": "tsx watch --clear-screen=false src/server.ts", "cli": "tsx src/cli.ts", - "build": "tsc", + "build": "tsc -p tsconfig.build.json", "start": "node dist/server.js", + "test": "node --import tsx --test \"src/lib/**/*.test.ts\"", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -29,6 +30,7 @@ "sharp": "^0.33.0" }, "devDependencies": { + "@atproto/lexicon": "^0.7.5", "@types/better-sqlite3": "^7.6.10", "@types/node": "^22.0.0", "tsx": "^4.19.0", diff --git a/src/lexicons/markpub/README.md b/src/lexicons/markpub/README.md new file mode 100644 index 0000000..8928de5 --- /dev/null +++ b/src/lexicons/markpub/README.md @@ -0,0 +1,10 @@ +# Vendored markpub lexicons (test fixtures) + +Source: https://github.com/AramZS/markpub.at (MIT), +`dist/lexicons/at/markpub/`, fetched 2026-07-06 at commit `9b53a3a8f93d`. + +Used ONLY by `src/lib/content-shape.test.ts` to validate that the content +union member we emit stays valid against the real schema — not loaded at +runtime. Refresh by re-copying from the repo when markpub evolves; if +validation breaks after a refresh, adapt the emitted shape in +`src/lib/content-shape.ts`. diff --git a/src/lexicons/markpub/markdown.json b/src/lexicons/markpub/markdown.json new file mode 100644 index 0000000..b18cc7e --- /dev/null +++ b/src/lexicons/markpub/markdown.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "at.markpub.markdown", + "defs": { + "main": { + "type": "object", + "properties": { + "text": { + "description": "An object that includes the text in markdown. May include anything that is valid markdown syntax for your flavor. Make sure it is properly escaped if necessary.", + "type": "ref", + "ref": "at.markpub.text" + }, + "flavor": { + "description": "Markdown may be rendered in many flavors. The most common are CommonMark and Github Flavored Markdown (GFM). Generally your markdown is one of those two and if you don't know then it is likely CommonMark. At this time the lexicon only recognizes these two flavors. Submit a PR if you think one should be added.", + "type": "string", + "knownValues": [ + "gfm", + "commonmark" + ] + }, + "renderingRules": { + "description": "Using this field is highly suggested. Different rendering systems for Markdown may introduce slight or significant changes to the resulting HTML. This setting allows you to specify your renderer so systems can understand the rules you expect. Keep in mind that no consuming entity is obligated to honor this preference. While rendering views may infer rules established by different Markdown renderers with this field, they can and should use the rendering system of their choice. Do **not** use rendering systems you don't know. Some examples might be `marked`, `pandoc`, `markdown-it` `mdxt`, etc. Generally, this lexicon assumes that you are pulling the Markdown from an existing site that includes an existing rendering process. The processor used for that process to build your site's pages is the one you should include here. If you don't know then just leave this field out.", + "type": "string" + }, + "extensions": { + "description": "The Markdown community expects certain extensions to mainline Markdown flavors. This setting allows you to note to a renderer what extensions might be expected. The most common is LaTeX. Rendering systems may choose to render these extensions in a variety of ways or present them raw. It is not required that you include YAML in the markdown text itself, if you choose to include a YAML metadata block, note it in this field.", + "type": "array", + "items": { + "type": "string" + } + }, + "frontMatter": { + "description": "If your markdown includes a front matter block, you can include it here as a set of objects describing the data. This is optional but may be more convenient for some systems to have it separate. It is expected that if you include this field that the front matter block is not included in the raw markdown text field. It would likely be parsed out of a YAML block and transformed into this array of objects or an array holding a single object.", + "type": "array", + "items": { + "type": "union", + "closed": false, + "refs": [] + } + } + }, + "required": [ + "text" + ] + } + } +} \ No newline at end of file diff --git a/src/lexicons/markpub/text.json b/src/lexicons/markpub/text.json new file mode 100644 index 0000000..b890de6 --- /dev/null +++ b/src/lexicons/markpub/text.json @@ -0,0 +1,44 @@ +{ + "lexicon": 1, + "id": "at.markpub.text", + "defs": { + "main": { + "type": "object", + "properties": { + "markdown": { + "description": "The raw text in markdown. May include anything that is valid markdown syntax for your flavor. Make sure it is properly escaped if necessary.", + "type": "string" + }, + "textBlob": { + "description": "Text may be blob-ified as raw Markdown and stored on a PDS, pass it here by reference. If you use this property it is assumed that it overrides the \"rawMarkdown\" property. It is a PDS address for a markdown text file. You still must provide some value, even if it is a preview for \"rawMarkdown\" in this field.", + "type": "blob", + "maxSize": 1000000, + "accept": [ + "text/*" + ] + }, + "facets": { + "description": "Facets here represent rendered versions of Markdown strings. A bold Markdown string `**bold**` might be represented by a richtext facet of #bold, in which case it is suggested to be presented without the Markdown markup as `<strong>bold</strong>`. Facets select their character ranges based on position in the Markdown text but should be rendered without the related characters. For example: `### Header` will be selected using a character range that includes the hashes like (0,9) but will be rendered without the hashes like `<h3>Header</h3>`. It is recommended that processors to not transform in-place for this reason. The goal of having an open union for facets is that constructing systems may choose to use the facets they prefer from across the ecosystem, either here or in other places like `pub.leaflet.richtext.facet`", + "type": "array", + "items": { + "type": "union", + "closed": false, + "refs": [] + } + }, + "lenses": { + "description": "Lenses are lexicons that define translatable facets for rendering layers with limited facets. `pub.leaflet.richtext.facet#bold` and `at.markpub.facets.baseFormatting#strong` expect the same output. A lens would then include a union with both those facets and a renderer that understands either of them could translate between the two.", + "type": "array", + "items": { + "type": "union", + "closed": false, + "refs": [] + } + } + }, + "required": [ + "markdown" + ] + } + } +} \ No newline at end of file diff --git a/src/lib/content-shape.test.ts b/src/lib/content-shape.test.ts new file mode 100644 index 0000000..53afa84 --- /dev/null +++ b/src/lib/content-shape.test.ts @@ -0,0 +1,18 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { Lexicons } from '@atproto/lexicon'; + +// Vendored fixtures are loaded via fs (not import) so no tsconfig JSON-module +// setting is load-bearing for the test suite. +const lexDoc = (name: string) => + JSON.parse(readFileSync(new URL(`../lexicons/markpub/${name}`, import.meta.url), 'utf8')); +const lexicons = new Lexicons([lexDoc('markdown.json'), lexDoc('text.json')]); + +test('the vendored lexicon genuinely rejects invalid shapes (negative control)', () => { + const result = lexicons.validate('at.markpub.markdown', { + $type: 'at.markpub.markdown', + flavor: 'gfm', + }); + assert.ok(!result.success); // required `text` field missing — validation must fail +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..af8cdbb --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +}