diff --git a/docs/src/basics/configuration.dj b/docs/src/basics/configuration.dj index 2814786..71e4710 100644 --- a/docs/src/basics/configuration.dj +++ b/docs/src/basics/configuration.dj @@ -1,6 +1,7 @@ --- order: -1 --- +{#config-reference} # Configuration reference {tag=aside .caution} diff --git a/docs/src/features/version_directives.dj b/docs/src/features/version_directives.dj new file mode 100644 index 0000000..e23907e --- /dev/null +++ b/docs/src/features/version_directives.dj @@ -0,0 +1,43 @@ +# Version directives + +{added-in-version="0.0.5"} +::: +::: + +You can mark some content as applying to a specific version using the `added-in-version` attribute on an empty div. If you specified a `projectInfo.version`{.language-yaml} in your [configuration](#config-reference), and the specified version is greater, then the message will be future-tense. + +```djot +{added-in-version="0.0.1"} +::: +::: + +{added-in-version="10.0"} +::: +::: +``` + +Output: + +{added-in-version="0.0.1"} +::: +::: + +{added-in-version="10.0"} +::: +::: + +You can also put content inside if you want to add commentary. + +```djot +{added-in-version="10.0"} +::: +We're really excited about this feature! +::: +``` + +Output: + +{added-in-version="10.0"} +::: +We're really excited about this feature! +::: \ No newline at end of file diff --git a/src/engine/executeConfig.ts b/src/engine/executeConfig.ts index 7b4628e..b6dab23 100644 --- a/src/engine/executeConfig.ts +++ b/src/engine/executeConfig.ts @@ -23,6 +23,7 @@ import { SyntaxHighlightingPlugin } from "../plugins/syntaxHighlighting.js"; import { fileURLToPath } from "url"; import { IndextermsPlugin } from "../plugins/indextermsPlugin.js"; import { GFMAlertsPlugin } from "../plugins/gfmAlertsPlugin.js"; +import { VersionDirectivesPlugin } from "../plugins/versionDirectives.js"; function pluralize(n: number, singular: string, plural: string): string { return n === 1 ? `1 ${singular}` : `${n} ${plural}`; @@ -37,6 +38,7 @@ function makeBuiltinPlugins(config: DjockeyConfigResolved): DjockeyPlugin[] { new AutoTitlePlugin(), new SyntaxHighlightingPlugin(config), new GFMAlertsPlugin(), + new VersionDirectivesPlugin(config), ]; } diff --git a/src/plugins/versionDirectives.ts b/src/plugins/versionDirectives.ts new file mode 100644 index 0000000..337f6fd --- /dev/null +++ b/src/plugins/versionDirectives.ts @@ -0,0 +1,84 @@ +import { DjockeyConfig, DjockeyDoc, DjockeyPlugin } from "../types.js"; +import { applyFilter } from "../engine/djotFiltersPlus.js"; +import { getAnyAttribute } from "../util.js"; +import { Block } from "@djot/djot"; + +function normalizeSemverString(semverString: string): [number, number, number] { + const parts: number[] = semverString + .split(".") + .map((s) => parseInt(s, 10)) + .slice(0, 3); + while (parts.length < 3) parts.push(0); + return parts as [number, number, number]; +} + +function isSemverGreaterOrEqual(a: string, b: string): boolean { + const aParts = normalizeSemverString(a); + const bParts = normalizeSemverString(b); + for (let i = 0; i < 3; i++) { + if (aParts[i] > bParts[i]) return true; + if (aParts[i] < bParts[i]) return false; + } + return true; +} + +type Cls = + | "added-in-version" + | "changed-in-version" + | "removed-in-version" + | "deprecated-in-version"; +const PREFIXES_CURRENT: Record = { + "added-in-version": "Added in version", + "changed-in-version": "Changed in version", + "removed-in-version": "Removed in version", + "deprecated-in-version": "Deprecated in version", +}; + +const PREFIXES_FUTURE: Record = { + "added-in-version": "Will be added in version", + "changed-in-version": "Will be changed in version", + "removed-in-version": "Will be reemoved in version", + "deprecated-in-version": "Will be deprecated in version", +}; + +export class VersionDirectivesPlugin implements DjockeyPlugin { + name = "Version Directives"; + + constructor(public config: DjockeyConfig) {} + + onPass_write(doc: DjockeyDoc) { + const projectVersion = this.config.projectInfo?.version; + applyFilter(doc.docs.content, () => ({ + div: (node) => { + const keyAndValue = getAnyAttribute( + node, + Object.keys(PREFIXES_CURRENT) + ); + if (!keyAndValue) return; + const [key, value] = keyAndValue; + + // If no project version, assume it's already release. Otherwise, put it in the future. + const prefix = + !projectVersion || isSemverGreaterOrEqual(projectVersion, value) + ? PREFIXES_CURRENT[key as Cls] + : PREFIXES_FUTURE[key as Cls]; + return buildAST(key, `${prefix} ${value}`, node.children); + }, + })); + } +} + +function buildAST(cls: string, text: string, children: Block[]): Block { + return { + tag: "div", + attributes: { class: `version-modified ${cls}` }, + children: [ + { + tag: "para", + attributes: { class: "primary" }, + children: [{ tag: "str", text }], + }, + ...structuredClone(children), + ], + }; +} diff --git a/src/util.ts b/src/util.ts index 18ba3b1..9edbff8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -73,6 +73,22 @@ export function getHasClass(node: HasAttributes, cls: string): boolean { return values.has(cls); } +export function getAttribute(node: HasAttributes, k: string): string | null { + if (!node.attributes || node.attributes[k] === undefined) return null; + return node.attributes[k]; +} + +export function getAnyAttribute( + node: HasAttributes, + keys: string[] +): [string, string] | null { + if (!node.attributes) return null; + for (const k of keys) { + if (node.attributes[k] !== undefined) return [k, node.attributes[k]]; + } + return null; +} + export function makeStubDjotDoc(children: Block[]): Doc { return { tag: "doc", diff --git a/templates/html/static/dj-asides.css b/templates/html/static/dj-markupfeatures.css similarity index 85% rename from templates/html/static/dj-asides.css rename to templates/html/static/dj-markupfeatures.css index 4e3c266..423aa94 100644 --- a/templates/html/static/dj-asides.css +++ b/templates/html/static/dj-markupfeatures.css @@ -1,3 +1,5 @@ +/* ASIDES */ + aside { border-width: var(--aside-border-width); border-radius: var(--aside-border-radius); @@ -75,3 +77,18 @@ aside.warning::before { aside.warning { border-color: var(--aside-color-border-danger); } + +/* VERSION MODIFIED */ + +.version-modified { + font-style: italic; +} + +.version-modified p.primary { + font-size: var(--fs-medium); + margin-bottom: 0.5em; +} + +.version-modified p { + font-size: var(--fs-small); +}