From 2fc03dabfcf0836aa18b2a4cc6cd8a8ffe2071bf Mon Sep 17 00:00:00 2001 From: "hdcode.dev" <215285761+hdcodedev@users.noreply.github.com> Date: Mon, 2 Nov 2026 00:42:19 +0100 Subject: [PATCH 1/2] docs(wiki): resolve artifact versions in snippets --- docs-app/lib/artifact-version.cjs | 63 +++++++++++++ docs-app/lib/artifact-version.test.mjs | 123 +++++++++++++++++++++++++ docs-app/lib/content.ts | 17 ++++ 3 files changed, 203 insertions(+) create mode 100644 docs-app/lib/artifact-version.cjs create mode 100644 docs-app/lib/artifact-version.test.mjs diff --git a/docs-app/lib/artifact-version.cjs b/docs-app/lib/artifact-version.cjs new file mode 100644 index 0000000..830ecd3 --- /dev/null +++ b/docs-app/lib/artifact-version.cjs @@ -0,0 +1,63 @@ +const SNAPSHOT_VERSION = /^\d+\.\d+\.\d+-SNAPSHOT$/; +const SOURCE_SHA = /^[0-9a-f]{40}$/i; + +/** + * A release route is immutable and uses its own SemVer ID. Snapshot routes use + * the version and source SHA recorded by the snapshot publication workflow. + */ +function resolveArtifactVersion(versionId, snapshotManifestText) { + if (versionId !== 'snapshot') { + return /^\d+\.\d+\.\d+$/.test(versionId) ? versionId : null; + } + + try { + const manifest = JSON.parse(snapshotManifestText); + if ( + typeof manifest.charts_version === 'string' && + SNAPSHOT_VERSION.test(manifest.charts_version) && + typeof manifest.source_sha === 'string' && + SOURCE_SHA.test(manifest.source_sha) && + typeof manifest.docs_source_sha === 'string' && + SOURCE_SHA.test(manifest.docs_source_sha) && + manifest.docs_source_sha === manifest.source_sha + ) { + return manifest.charts_version; + } + } catch { + // Preserve the template when publication metadata is missing or invalid. + } + + return null; +} + +/** Resolve only Kotlin fenced blocks, leaving other Markdown and code untouched. */ +function resolveKotlinArtifactVersion(markdown, artifactVersion) { + if (!artifactVersion) return markdown; + + const lines = markdown.split(/(?<=\n)/); + let fence = null; + let inKotlin = false; + + return lines.map((line) => { + const value = line.replace(/\r?\n$/, ''); + if (!fence) { + const opening = /^(\s*)(`{3,}|~{3,})(.*)$/.exec(value); + if (opening) { + fence = { marker: opening[2][0], length: opening[2].length }; + inKotlin = /^\s*kotlin(?:\s|$)/i.test(opening[3]); + } + return line; + } + + const closing = new RegExp(`^\\s*${fence.marker}{${fence.length},}\\s*$`); + if (closing.test(value)) { + fence = null; + inKotlin = false; + return line; + } + return inKotlin ? line.replaceAll('{{artifactVersion}}', artifactVersion) : line; + }).join(''); +} + +exports.resolveArtifactVersion = resolveArtifactVersion; +exports.resolveKotlinArtifactVersion = resolveKotlinArtifactVersion; diff --git a/docs-app/lib/artifact-version.test.mjs b/docs-app/lib/artifact-version.test.mjs new file mode 100644 index 0000000..b9a1b2d --- /dev/null +++ b/docs-app/lib/artifact-version.test.mjs @@ -0,0 +1,123 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import artifactVersion from './artifact-version.cjs'; + +const { resolveArtifactVersion, resolveKotlinArtifactVersion } = artifactVersion; + +const snapshotManifest = JSON.stringify({ + source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', + docs_source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', + charts_version: '3.0.1-SNAPSHOT', +}); + +test('resolves snapshot docs to the artifact version recorded by publication', () => { + assert.equal(resolveArtifactVersion('snapshot', snapshotManifest), '3.0.1-SNAPSHOT'); +}); + +test('resolves released docs to their immutable release version', () => { + assert.equal(resolveArtifactVersion('3.0.0', snapshotManifest), '3.0.0'); + assert.equal(resolveArtifactVersion('2.4.0', snapshotManifest), '2.4.0'); +}); + +test('preserves the placeholder when snapshot metadata is missing, malformed, or inconsistent', () => { + assert.equal(resolveArtifactVersion('snapshot', ''), null); + assert.equal(resolveArtifactVersion('snapshot', '{'), null); + assert.equal(resolveArtifactVersion('snapshot', JSON.stringify({ + source_sha: 'not-a-commit', + docs_source_sha: 'not-a-commit', + charts_version: '3.0.1-SNAPSHOT', + })), null); + assert.equal(resolveArtifactVersion('snapshot', JSON.stringify({ + source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', + docs_source_sha: '60864f821601ef8b12afe84586638553ce8f8090', + charts_version: '3.0.1-SNAPSHOT', + })), null); + assert.equal(resolveArtifactVersion('snapshot', JSON.stringify({ + source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', + docs_source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', + charts_version: 'snapshot', + })), null); + assert.equal(resolveArtifactVersion('main', snapshotManifest), null); +}); + +test('resolves umbrella, modular, and BOM coordinates across multiple Kotlin snippets', () => { + const markdown = [ + '## Installation', + '', + '```kotlin', + 'implementation("io.github.hdcharts:charts:{{artifactVersion}}")', + '```', + '', + '```kotlin', + 'implementation("io.github.hdcharts:gauge:{{artifactVersion}}")', + '```', + '', + '```kotlin', + 'implementation(platform("io.github.hdcharts:bom:{{artifactVersion}}"))', + 'implementation("io.github.hdcharts:line")', + '```', + ].join('\n'); + + const result = resolveKotlinArtifactVersion(markdown, '3.0.1-SNAPSHOT'); + assert.equal((result.match(/3\.0\.1-SNAPSHOT/g) ?? []).length, 3); + assert.doesNotMatch(result, /\{\{artifactVersion\}\}/); + + const legacy = resolveKotlinArtifactVersion(markdown, '2.4.0'); + assert.match(legacy, /io\.github\.hdcharts:charts:2\.4\.0/); + assert.match(legacy, /io\.github\.hdcharts:bom:2\.4\.0/); +}); + +test('leaves unrelated placeholders and non-Kotlin code fences unchanged', () => { + const markdown = [ + 'Outside {{artifactVersion}} {{version}}', + '', + '```kotlin', + 'implementation("x:y:{{artifactVersion}}")', + 'val route = "{{version}}"', + '```', + '', + '```gradle', + 'implementation("x:y:{{artifactVersion}}")', + '```', + '', + '```text', + '{{artifactVersion}}', + '```', + ].join('\n'); + + const result = resolveKotlinArtifactVersion(markdown, '3.0.1-SNAPSHOT'); + assert.match(result, /Outside \{\{artifactVersion\}\} \{\{version\}\}/); + assert.match(result, /val route = "\{\{version\}\}"/); + assert.match(result, /```gradle\nimplementation\("x:y:\{\{artifactVersion\}\}"\)/); + assert.match(result, /```text\n\{\{artifactVersion\}\}/); + assert.match(result, /x:y:3\.0\.1-SNAPSHOT/); +}); + +test('copy input is the resolved code parsed from a Kotlin fence', async () => { + const markdown = '```kotlin\nimplementation("io.github.hdcharts:charts:{{artifactVersion}}")\n```'; + const resolved = resolveKotlinArtifactVersion(markdown, '3.0.1-SNAPSHOT'); + const tree = unified().use(remarkParse).parse(resolved); + const code = tree.children.find((node) => node.type === 'code'); + + assert.equal(code.lang, 'kotlin'); + assert.equal(code.value, 'implementation("io.github.hdcharts:charts:3.0.1-SNAPSHOT")'); +}); + +test('supports tilde fences and keeps historical release dependency coordinates intact', () => { + const historical = [ + '~~~kotlin', + 'implementation("io.github.dautovicharis:charts:{{artifactVersion}}")', + 'implementation(platform("io.github.dautovicharis:charts-bom:{{artifactVersion}}"))', + '~~~', + ].join('\n'); + const result = resolveKotlinArtifactVersion(historical, '2.4.0'); + assert.match(result, /io\.github\.dautovicharis:charts:2\.4\.0/); + assert.match(result, /io\.github\.dautovicharis:charts-bom:2\.4\.0/); +}); + +test('keeps fenced Kotlin template source intact when metadata is unavailable', () => { + const markdown = '```kotlin\nimplementation("io.github.hdcharts:charts:{{artifactVersion}}")\n```'; + assert.equal(resolveKotlinArtifactVersion(markdown, null), markdown); +}); diff --git a/docs-app/lib/content.ts b/docs-app/lib/content.ts index b7d3332..d675513 100644 --- a/docs-app/lib/content.ts +++ b/docs-app/lib/content.ts @@ -4,6 +4,7 @@ import matter from 'gray-matter'; import { DocPage, DocVersion, NavItem, PageFrontmatter } from './types'; import { createHeadingSlugger, slugifyHeading } from './anchors'; import { getAllVersions, getVersion } from './versions'; +import artifactVersion from './artifact-version.cjs'; /** * Base path to wiki content @@ -11,6 +12,8 @@ import { getAllVersions, getVersion } from './versions'; const CONTENT_BASE = path.join(process.cwd(), '..', 'content'); const RELEASE_NOTES_BASE = path.join(process.cwd(), '..', 'release-notes'); const CURRENT_RELEASE_NOTES_VERSION_PATH = path.join(RELEASE_NOTES_BASE, 'current-version.txt'); +const SNAPSHOT_MANIFEST_PATH = path.join(process.cwd(), 'public', 'snapshot-manifest.json'); +const { resolveArtifactVersion, resolveKotlinArtifactVersion } = artifactVersion; const CHANGESETS_DIR_NAME = 'changes'; const MIGRATIONS_DIR_NAME = 'migrations'; @@ -26,6 +29,18 @@ function readTextFile(filePath: string): string { return fs.readFileSync(/* turbopackIgnore: true */ filePath, 'utf-8'); } +function getArtifactVersion(versionId: string): string | null { + if (versionId !== 'snapshot') { + return resolveArtifactVersion(versionId, ''); + } + + try { + return resolveArtifactVersion(versionId, readTextFile(SNAPSHOT_MANIFEST_PATH)); + } catch { + return null; + } +} + /** * Get the wiki content directory for a version */ @@ -689,6 +704,8 @@ export function getPage(versionId: string, slug: string): DocPage | null { .replace(/\{\{version\}\}/g, versionId) .replace(/\{\{versionLabel\}\}/g, versionLabel); + pageContent = resolveKotlinArtifactVersion(pageContent, getArtifactVersion(versionId)); + if (slug === '') { const changesetsMarkdown = renderChangesetsMarkdown(getVersionChangesets(versionId)); if (changesetsMarkdown) { From 2e6d0f6475a20cb0ef8bf9312b24d7aa04d8a973 Mon Sep 17 00:00:00 2001 From: "hdcode.dev" <215285761+hdcodedev@users.noreply.github.com> Date: Mon, 2 Nov 2026 00:47:02 +0100 Subject: [PATCH 2/2] docs(wiki): preserve release dependency versions --- docs-app/lib/artifact-version.cjs | 10 +---- docs-app/lib/artifact-version.test.mjs | 56 +++++++++++++++++--------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/docs-app/lib/artifact-version.cjs b/docs-app/lib/artifact-version.cjs index 830ecd3..c535896 100644 --- a/docs-app/lib/artifact-version.cjs +++ b/docs-app/lib/artifact-version.cjs @@ -1,9 +1,8 @@ const SNAPSHOT_VERSION = /^\d+\.\d+\.\d+-SNAPSHOT$/; -const SOURCE_SHA = /^[0-9a-f]{40}$/i; /** * A release route is immutable and uses its own SemVer ID. Snapshot routes use - * the version and source SHA recorded by the snapshot publication workflow. + * the version recorded by the snapshot publication workflow. */ function resolveArtifactVersion(versionId, snapshotManifestText) { if (versionId !== 'snapshot') { @@ -14,12 +13,7 @@ function resolveArtifactVersion(versionId, snapshotManifestText) { const manifest = JSON.parse(snapshotManifestText); if ( typeof manifest.charts_version === 'string' && - SNAPSHOT_VERSION.test(manifest.charts_version) && - typeof manifest.source_sha === 'string' && - SOURCE_SHA.test(manifest.source_sha) && - typeof manifest.docs_source_sha === 'string' && - SOURCE_SHA.test(manifest.docs_source_sha) && - manifest.docs_source_sha === manifest.source_sha + SNAPSHOT_VERSION.test(manifest.charts_version) ) { return manifest.charts_version; } diff --git a/docs-app/lib/artifact-version.test.mjs b/docs-app/lib/artifact-version.test.mjs index b9a1b2d..45fea30 100644 --- a/docs-app/lib/artifact-version.test.mjs +++ b/docs-app/lib/artifact-version.test.mjs @@ -1,4 +1,6 @@ import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; import test from 'node:test'; import { unified } from 'unified'; import remarkParse from 'remark-parse'; @@ -7,8 +9,6 @@ import artifactVersion from './artifact-version.cjs'; const { resolveArtifactVersion, resolveKotlinArtifactVersion } = artifactVersion; const snapshotManifest = JSON.stringify({ - source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', - docs_source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', charts_version: '3.0.1-SNAPSHOT', }); @@ -19,26 +19,16 @@ test('resolves snapshot docs to the artifact version recorded by publication', ( test('resolves released docs to their immutable release version', () => { assert.equal(resolveArtifactVersion('3.0.0', snapshotManifest), '3.0.0'); assert.equal(resolveArtifactVersion('2.4.0', snapshotManifest), '2.4.0'); + assert.equal(resolveArtifactVersion('3.1.0', snapshotManifest), '3.1.0'); }); test('preserves the placeholder when snapshot metadata is missing, malformed, or inconsistent', () => { assert.equal(resolveArtifactVersion('snapshot', ''), null); assert.equal(resolveArtifactVersion('snapshot', '{'), null); assert.equal(resolveArtifactVersion('snapshot', JSON.stringify({ - source_sha: 'not-a-commit', - docs_source_sha: 'not-a-commit', - charts_version: '3.0.1-SNAPSHOT', - })), null); - assert.equal(resolveArtifactVersion('snapshot', JSON.stringify({ - source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', - docs_source_sha: '60864f821601ef8b12afe84586638553ce8f8090', - charts_version: '3.0.1-SNAPSHOT', - })), null); - assert.equal(resolveArtifactVersion('snapshot', JSON.stringify({ - source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', - docs_source_sha: '28709d9a845932ec6cb020bb63e69451c89744dd', charts_version: 'snapshot', })), null); + assert.equal(resolveArtifactVersion('snapshot', snapshotManifest), '3.0.1-SNAPSHOT'); assert.equal(resolveArtifactVersion('main', snapshotManifest), null); }); @@ -105,16 +95,42 @@ test('copy input is the resolved code parsed from a Kotlin fence', async () => { assert.equal(code.value, 'implementation("io.github.hdcharts:charts:3.0.1-SNAPSHOT")'); }); -test('supports tilde fences and keeps historical release dependency coordinates intact', () => { +test('keeps historical release dependency examples and version tags unchanged', () => { + const historicalPath = path.join('..', 'content', '2.4.0', 'wiki', 'getting-started.md'); + const historicalPage = fs.readFileSync(historicalPath, 'utf8'); + assert.equal(resolveKotlinArtifactVersion(historicalPage, '2.4.0'), historicalPage); + const historical = [ '~~~kotlin', - 'implementation("io.github.dautovicharis:charts:{{artifactVersion}}")', - 'implementation(platform("io.github.dautovicharis:charts-bom:{{artifactVersion}}"))', + 'implementation("io.github.dautovicharis:charts:2.4.0")', + 'implementation(platform("io.github.dautovicharis:charts-bom:2.4.0"))', '~~~', ].join('\n'); - const result = resolveKotlinArtifactVersion(historical, '2.4.0'); - assert.match(result, /io\.github\.dautovicharis:charts:2\.4\.0/); - assert.match(result, /io\.github\.dautovicharis:charts-bom:2\.4\.0/); + assert.equal(resolveKotlinArtifactVersion(historical, '2.4.0'), historical); +}); + +test('leaves every published release Markdown page byte-for-byte unchanged', () => { + const contentRoot = path.join('..', 'content'); + const releaseIds = fs.readdirSync(contentRoot).filter((entry) => /^\d+\.\d+\.\d+$/.test(entry)); + + function markdownFiles(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const filePath = path.join(directory, entry.name); + if (entry.isDirectory()) return markdownFiles(filePath); + return /\.mdx?$/.test(entry.name) ? [filePath] : []; + }); + } + + for (const releaseId of releaseIds) { + for (const filePath of markdownFiles(path.join(contentRoot, releaseId))) { + const markdown = fs.readFileSync(filePath, 'utf8'); + assert.equal( + resolveKotlinArtifactVersion(markdown, resolveArtifactVersion(releaseId, '')), + markdown, + filePath, + ); + } + } }); test('keeps fenced Kotlin template source intact when metadata is unavailable', () => {