Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs-app/lib/artifact-version.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const SNAPSHOT_VERSION = /^\d+\.\d+\.\d+-SNAPSHOT$/;

/**
* A release route is immutable and uses its own SemVer ID. Snapshot routes use
* the version 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)
) {
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;
139 changes: 139 additions & 0 deletions docs-app/lib/artifact-version.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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';
import artifactVersion from './artifact-version.cjs';

const { resolveArtifactVersion, resolveKotlinArtifactVersion } = artifactVersion;

const snapshotManifest = JSON.stringify({
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');
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({
charts_version: 'snapshot',
})), null);
assert.equal(resolveArtifactVersion('snapshot', snapshotManifest), '3.0.1-SNAPSHOT');
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('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:2.4.0")',
'implementation(platform("io.github.dautovicharis:charts-bom:2.4.0"))',
'~~~',
].join('\n');
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', () => {
const markdown = '```kotlin\nimplementation("io.github.hdcharts:charts:{{artifactVersion}}")\n```';
assert.equal(resolveKotlinArtifactVersion(markdown, null), markdown);
});
17 changes: 17 additions & 0 deletions docs-app/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ 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
*/
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';

Expand All @@ -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
*/
Expand Down Expand Up @@ -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) {
Expand Down