From e43b442ebb9951116e7a5a388c1ea95e78eb5b7a Mon Sep 17 00:00:00 2001 From: Yisakor M Date: Thu, 13 Aug 2026 20:02:13 -0700 Subject: [PATCH 1/3] Add OpenProcessing example sync tooling --- package.json | 3 +- src/scripts/openprocessing.ts | 121 ++++++++++++++++ src/scripts/sync-openprocessing.ts | 198 ++++++++++++++++++++++++++ src/utils/exampleAttribution.ts | 81 +++++++++++ test/scripts/openprocessing.test.ts | 39 +++++ test/utils/exampleAttribution.test.ts | 62 ++++++++ 6 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 src/scripts/openprocessing.ts create mode 100644 src/scripts/sync-openprocessing.ts create mode 100644 src/utils/exampleAttribution.ts create mode 100644 test/scripts/openprocessing.test.ts create mode 100644 test/utils/exampleAttribution.test.ts diff --git a/package.json b/package.json index fe0122165a..3e2d1f8d1f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "build:search": "tsx ./src/scripts/builders/search.ts", "build:p5-version": "tsx ./src/scripts/p5-version.ts", "custom:dev": "tsx ./src/scripts/branchTest.ts", - "custom:cleanup": "tsx ./src/scripts/resetBranchTest.ts" + "custom:cleanup": "tsx ./src/scripts/resetBranchTest.ts", + "sync:openprocessing": "tsx ./src/scripts/sync-openprocessing.ts" }, "engines": { "node": ">=22.0.0" diff --git a/src/scripts/openprocessing.ts b/src/scripts/openprocessing.ts new file mode 100644 index 0000000000..388f148844 --- /dev/null +++ b/src/scripts/openprocessing.ts @@ -0,0 +1,121 @@ +const OPENPROCESSING_BASE_URL = "https://openprocessing.org/api"; + +export interface OpenProcessingSketch { + visualID: number; + title?: string; + [key: string]: unknown; +} + +export interface OpenProcessingCuration { + visualID?: number; + id?: number; + sketches?: OpenProcessingSketch[]; + [key: string]: unknown; +} + +interface OpenProcessingConfig { + token: string; + curationId: string; +} + +function getConfig(): OpenProcessingConfig { + const token = process.env.OPENPROCESSING_TOKEN; + const curationId = process.env.OPENPROCESSING_CURATION_ID; + + if (!token) { + throw new Error( + "Missing OPENPROCESSING_TOKEN environment variable." + ); + } + + if (!curationId) { + throw new Error( + "Missing OPENPROCESSING_CURATION_ID environment variable." + ); + } + + return { + token, + curationId, + }; +} + +async function request( + path: string, + options: RequestInit = {} +): Promise { + const { token } = getConfig(); + + const response = await fetch(`${OPENPROCESSING_BASE_URL}${path}`, { + ...options, + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); + + if (!response.ok) { + const body = await response.text(); + + throw new Error( + `OpenProcessing request failed: ${response.status} ${response.statusText}\n${body}` + ); + } + + if (response.status === 204) { + return undefined as T; + } + + const text = await response.text(); + + if (!text) { + return undefined as T; + } + + return JSON.parse(text) as T; +} + +export async function getCuration(): Promise { + const { curationId } = getConfig(); + + return request( + `/curation/${curationId}` + ); +} + +export async function getCurationSketches(): Promise< + OpenProcessingSketch[] +> { + const { curationId } = getConfig(); + + return request( + `/curation/${curationId}/sketches` + ); +} + +export async function addSketchToCuration( + visualID: number +): Promise { + const { curationId } = getConfig(); + + await request( + `/curation/${curationId}/sketches/${visualID}`, + { + method: "POST", + } + ); +} + +export async function removeSketchFromCuration( + visualID: number +): Promise { + const { curationId } = getConfig(); + + await request( + `/curation/${curationId}/sketches/${visualID}`, + { + method: "DELETE", + } + ); +} \ No newline at end of file diff --git a/src/scripts/sync-openprocessing.ts b/src/scripts/sync-openprocessing.ts new file mode 100644 index 0000000000..1479687aea --- /dev/null +++ b/src/scripts/sync-openprocessing.ts @@ -0,0 +1,198 @@ +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import yaml from "js-yaml"; + +import { + addAttributionToCode, + type RemixEntry, +} from "../utils/exampleAttribution"; + +import { getCurationSketches } from "./openprocessing"; + +interface ExampleFrontmatter { + title: string; + oneLineDescription: string; + remix?: RemixEntry[]; +} + +interface PreparedExample { + id: string; + title: string; + description: string; + code: string; +} + +const EXAMPLES_DIRECTORY = "src/content/examples/en"; + +async function findDescriptionFiles(directory: string): Promise { + const entries = await readdir(directory, { + withFileTypes: true, + }); + + const files: string[] = []; + + for (const entry of entries) { + const fullPath = join(directory, entry.name); + + if (entry.isDirectory()) { + files.push(...(await findDescriptionFiles(fullPath))); + } else if (entry.name === "description.mdx") { + files.push(fullPath); + } + } + + return files; +} + +function parseFrontmatter(contents: string): ExampleFrontmatter { + const match = contents.match(/^---\s*\n([\s\S]*?)\n---/); + + if (!match) { + throw new Error("Example does not contain YAML frontmatter."); + } + + const data = yaml.load(match[1]); + + if ( + typeof data !== "object" || + data === null || + !("title" in data) || + !("oneLineDescription" in data) + ) { + throw new Error("Example frontmatter is missing required fields."); + } + + return data as ExampleFrontmatter; +} + +async function prepareExample( + descriptionPath: string +): Promise { + const descriptionContents = await readFile( + descriptionPath, + "utf-8" + ); + + const data = parseFrontmatter(descriptionContents); + + const codePath = join( + descriptionPath.substring( + 0, + descriptionPath.lastIndexOf("/") + ), + "code.js" + ); + + let code = await readFile(codePath, "utf-8"); + + // Match the website's existing asset-path behavior. + code = code.replaceAll(/\(["']assets/g, "('/assets"); + + const attributedCode = addAttributionToCode( + code, + data.title, + data.remix ?? [] + ); + + const id = descriptionPath + .replace(`${EXAMPLES_DIRECTORY}/`, "") + .replace("/description.mdx", ""); + + return { + id, + title: data.title, + description: data.oneLineDescription, + code: attributedCode, + }; +} + +async function getEnglishExamples(): Promise { + const descriptionFiles = await findDescriptionFiles( + EXAMPLES_DIRECTORY + ); + + return Promise.all( + descriptionFiles.map((file) => prepareExample(file)) + ); +} + +async function main(): Promise { + console.log("Loading p5.js examples..."); + + const examples = await getEnglishExamples(); + + console.log(`Found ${examples.length} English examples.`); + + console.log("Loading OpenProcessing curation..."); + + const curationSketches = await getCurationSketches(); + + console.log( + `Found ${curationSketches.length} sketches in the OpenProcessing curation.` + ); + + const curationTitles = new Set( + curationSketches + .map((sketch) => sketch.title) + .filter((title): title is string => Boolean(title)) + ); + + const websiteTitles = new Set( + examples.map((example) => example.title) + ); + + const missingExamples = examples.filter( + (example) => !curationTitles.has(example.title) + ); + + const extraSketches = curationSketches.filter( + (sketch) => + sketch.title !== undefined && + !websiteTitles.has(sketch.title) + ); + + console.log(""); + console.log("Sync summary"); + console.log("------------"); + console.log(`Website examples: ${examples.length}`); + console.log(`Curation sketches: ${curationSketches.length}`); + console.log(`Missing from curation: ${missingExamples.length}`); + console.log(`Extra in curation: ${extraSketches.length}`); + + if (missingExamples.length > 0) { + console.log(""); + console.log("Missing examples:"); + + for (const example of missingExamples) { + console.log(`- ${example.title}`); + } + } + + if (extraSketches.length > 0) { + console.log(""); + console.log("Extra sketches:"); + + for (const sketch of extraSketches) { + console.log( + `- ${sketch.title ?? "Untitled"} (${sketch.visualID})` + ); + } + } + + console.log(""); + console.log( + "Dry run complete. No OpenProcessing sketches were modified." + ); +} + +main().catch((error: unknown) => { + console.error("OpenProcessing sync failed."); + + if (error instanceof Error) { + console.error(error.message); + } else { + console.error(error); + } + + process.exitCode = 1; +}); \ No newline at end of file diff --git a/src/utils/exampleAttribution.ts b/src/utils/exampleAttribution.ts new file mode 100644 index 0000000000..cc2da910be --- /dev/null +++ b/src/utils/exampleAttribution.ts @@ -0,0 +1,81 @@ +export interface Attribution { + name: string; + URL?: string; +} + +export interface RemixEntry { + description?: string; + attribution?: Attribution[]; + collectivelyAttributedSince?: number; + code?: { + label?: string; + URL?: string; + }[]; +} + +/** + * Creates a plain-text attribution that can be embedded in an + * OpenProcessing sketch. + */ +export function generateExampleAttribution( + title: string, + remixData?: RemixEntry[] +): string { + const lines: string[] = []; + + lines.push(title); + lines.push(""); + + remixData?.forEach((item) => { + if (item.collectivelyAttributedSince || !item.attribution?.length) { + return; + } + + const description = item.description ?? "Based on"; + + const authors = item.attribution + .map((author) => + author.URL ? `${author.name} (${author.URL})` : author.name + ) + .join(", "); + + lines.push(`${description}: ${authors}`); + }); + + const collectiveYear = remixData?.reduce( + (year, item) => item.collectivelyAttributedSince ?? year, + undefined + ); + + if (collectiveYear) { + lines.push( + `From ${collectiveYear} onwards, edited and maintained by p5.js Contributors and Processing Foundation.` + ); + } else { + lines.push( + "Edited and maintained by p5.js Contributors and Processing Foundation." + ); + } + + lines.push("Licensed under CC BY-NC-SA 4.0."); + + return lines.join("\n"); +} + +/** + * Adds attribution as JavaScript comments above the sketch code. + */ +export function addAttributionToCode( + code: string, + title: string, + remixData?: RemixEntry[] +): string { + const attribution = generateExampleAttribution(title, remixData); + + const comment = attribution + .split("\n") + .map((line) => `// ${line}`) + .join("\n"); + + return `${comment}\n\n${code}`; +} \ No newline at end of file diff --git a/test/scripts/openprocessing.test.ts b/test/scripts/openprocessing.test.ts new file mode 100644 index 0000000000..9394eda9f9 --- /dev/null +++ b/test/scripts/openprocessing.test.ts @@ -0,0 +1,39 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { getCurationSketches } from "../../src/scripts/openprocessing"; + +const originalToken = process.env.OPENPROCESSING_TOKEN; +const originalCurationId = process.env.OPENPROCESSING_CURATION_ID; + +afterEach(() => { + if (originalToken === undefined) { + delete process.env.OPENPROCESSING_TOKEN; + } else { + process.env.OPENPROCESSING_TOKEN = originalToken; + } + + if (originalCurationId === undefined) { + delete process.env.OPENPROCESSING_CURATION_ID; + } else { + process.env.OPENPROCESSING_CURATION_ID = originalCurationId; + } +}); + +describe("OpenProcessing configuration", () => { + it("requires an API token", async () => { + delete process.env.OPENPROCESSING_TOKEN; + process.env.OPENPROCESSING_CURATION_ID = "91157"; + + await expect(getCurationSketches()).rejects.toThrow( + "Missing OPENPROCESSING_TOKEN environment variable." + ); + }); + + it("requires a curation ID", async () => { + process.env.OPENPROCESSING_TOKEN = "test-token"; + delete process.env.OPENPROCESSING_CURATION_ID; + + await expect(getCurationSketches()).rejects.toThrow( + "Missing OPENPROCESSING_CURATION_ID environment variable." + ); + }); +}); \ No newline at end of file diff --git a/test/utils/exampleAttribution.test.ts b/test/utils/exampleAttribution.test.ts new file mode 100644 index 0000000000..2e0f7167a6 --- /dev/null +++ b/test/utils/exampleAttribution.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import { + addAttributionToCode, + generateExampleAttribution, +} from "../../src/utils/exampleAttribution"; + +describe("generateExampleAttribution", () => { + it("generates attribution for remix authors", () => { + const result = generateExampleAttribution("Kaleidoscope", [ + { + description: "Revised by", + attribution: [ + { + name: "Kasey Lichtyler", + URL: "https://www.klich.co/", + }, + ], + }, + ]); + + expect(result).toContain("Kaleidoscope"); + expect(result).toContain( + "Revised by: Kasey Lichtyler (https://www.klich.co/)" + ); + expect(result).toContain("p5.js Contributors"); + expect(result).toContain("CC BY-NC-SA 4.0"); + }); + + it("supports collective attribution years", () => { + const result = generateExampleAttribution("Example", [ + { + collectivelyAttributedSince: 2023, + }, + ]); + + expect(result).toContain( + "From 2023 onwards, edited and maintained by p5.js Contributors and Processing Foundation." + ); + }); + + it("adds attribution as comments above sketch code", () => { + const code = `function setup() { + createCanvas(400, 400); +}`; + + const result = addAttributionToCode(code, "Kaleidoscope", [ + { + description: "Revised by", + attribution: [ + { + name: "Kasey Lichtyler", + }, + ], + }, + ]); + + expect(result).toContain("// Kaleidoscope"); + expect(result).toContain("// Revised by: Kasey Lichtyler"); + expect(result).toContain("// Licensed under CC BY-NC-SA 4.0."); + expect(result).toContain("function setup()"); + }); +}); \ No newline at end of file From fe4e8c640aaaf6dcb16dcc063f6d56a91ded6a4c Mon Sep 17 00:00:00 2001 From: Yisakor M Date: Thu, 20 Aug 2026 21:10:21 -0700 Subject: [PATCH 2/3] Address OpenProcessing sync review feedback --- src/layouts/ExampleLayout.astro | 200 +++++++++++++++-------------- src/scripts/sync-openprocessing.ts | 9 +- src/utils/exampleAttribution.ts | 116 +++++++++++++---- src/utils/examplePaths.ts | 14 ++ test/utils/examplePaths.test.ts | 28 ++++ 5 files changed, 239 insertions(+), 128 deletions(-) create mode 100644 src/utils/examplePaths.ts create mode 100644 test/utils/examplePaths.test.ts diff --git a/src/layouts/ExampleLayout.astro b/src/layouts/ExampleLayout.astro index 96c11f7d1e..8ea13c17cc 100644 --- a/src/layouts/ExampleLayout.astro +++ b/src/layouts/ExampleLayout.astro @@ -1,17 +1,23 @@ --- import type { CollectionEntry } from "astro:content"; import { render } from "astro:content"; + +import EditableSketch from "@components/EditableSketch/index.astro"; import Head from "@components/Head/index.astro"; +import RelatedItems from "@components/RelatedItems/index.astro"; + import { getCurrentLocale, getUiTranslator } from "../i18n/utils"; import { generateJumpToState, + getFallbackRemixData, getRelatedEntriesinCollection, } from "../pages/_utils"; -import BaseLayout from "./BaseLayout.astro"; -import EditableSketch from "@components/EditableSketch/index.astro"; -import RelatedItems from "@components/RelatedItems/index.astro"; +import { + EXAMPLE_ATTRIBUTION, + getExampleAttributionData, +} from "../utils/exampleAttribution"; -import { getFallbackRemixData } from "../pages/_utils"; +import BaseLayout from "./BaseLayout.astro"; interface Props { example: CollectionEntry<"examples">; @@ -43,29 +49,17 @@ const relatedReferences = const { Content } = await render(example); -// Use the fallback function to retrieve English remix data if the current locale doesn't have any -let remixData = (await getFallbackRemixData( +// Use the fallback function to retrieve English remix data if the current +// locale doesn't have any. +const remixData = (await getFallbackRemixData( example.id, currentLocale, example.data.remix )) as typeof example.data.remix; -// Extract the collective attribution year. If multiple provided, uses last shown. -const collectivelyAttributedSince = remixData?.reduce( - (acc: number | null, item) => { - if (item.collectivelyAttributedSince) { - return item.collectivelyAttributedSince; - } - return acc; - }, - null -); - -// Boolean value on whether the remix history contains links to code -const remixHistoryHasCodeLinks = remixData?.some( - (item) => Array.isArray(item.code) && item.code.length > 0 -); - +// Shared attribution rules used by both the website layout and +// OpenProcessing synchronization. +const attributionData = getExampleAttributionData(remixData ?? []); --- +
- Creative Commons BY-NC-SA license badge + Creative Commons BY-NC-SA license badge

+ {example.data.title}:{" "} + + { + attributionData.remixes.map((item, i) => ( + + {i > 0 && " "} + {t("attribution", item.description)}{" "} + {item.attribution.map((author, j) => ( + <> + {author.URL ? ( + {t(author.name)} + ) : ( + t(author.name) + )} + {j < item.attribution.length - 1 ? ", " : "."} + + ))} + + )) + } - {example.data.title}:{" "} - - {remixData?.map((item, i) => { - const parts = []; - - // Each remix entry requires at least one attribution - // If a remix entry contains a collective attribution starting year, it is ignored here + { + attributionData.collectivelyAttributedSince ? ( + <> + {t( + "attribution", + `From ${attributionData.collectivelyAttributedSince} onwards, edited and maintained by` + )}{" "} + + ) : ( + <> + {t("attribution", "Edited and maintained by")}{" "} + + ) + } - if (!item.collectivelyAttributedSince && item.attribution) { - parts.push(<>{t("attribution", item.description)}); + + + {EXAMPLE_ATTRIBUTION.contributors.name} + {" "} + {t("attribution", "and")}{" "} + + {EXAMPLE_ATTRIBUTION.foundation.name} + . + {" "}Licensed under{" "} + + {EXAMPLE_ATTRIBUTION.license.name} + . + +

- if (item.attribution?.length) { - parts.push( - <> - {" "} - {item.attribution.map((a, j) => ( +

+ { + attributionData.codeLinks.length > 0 ? ( + <> + {t( + "attribution", + "You can find the code history of these examples here" + )} + {": "} + + {attributionData.codeLinks.map( + (codeItem, i, codeItemsList) => ( <> - {a.URL ? {t(a.name)} : t(a.name)} - { - item.attribution?.length - ? j < item.attribution.length - 1 ? ", " : "." - : "" - } + {codeItem.label} + {i < codeItemsList.length - 1 ? ", " : ". "} - ))} - - ); - } + ) + )} + + {t( + "attribution", + "You can suggest improvements by" + )}{" "} + + {t( + "attribution", + "contributing to the current website" + )} + + ! + + ) : ( + <> + ) } +

+
- return {i > 0 && " "}{parts}; - })} - - {collectivelyAttributedSince ? ( - <>{t("attribution", `From ${collectivelyAttributedSince} onwards, edited and maintained by`)}{" "} - ) : ( - <>{t("attribution", "Edited and maintained by")}{" "} - )} - - p5.js Contributors{" "} - {t("attribution", "and")}{" "} - Processing Foundation. - Licensed under{" "} - CC BY-NC-SA 4.0. - -

- -

- {remixHistoryHasCodeLinks ? ( - <> - {t("attribution", "You can find the code history of these examples here")}{": "} - {remixData - .map(item => item?.code) - .flat() - .filter(codeItem => codeItem && codeItem.URL) - .map((codeItem, i, codeItemsList) => ( - <> - {codeItem?.label}{ - i < (codeItemsList?.length ?? 0) - 1 ? ", " : ". " - } - - ))} - {t("attribution", "You can suggest improvements by")}{" "} - {t("attribution", "contributing to the current website")}! - - ) : ( - <> - )} -

- - + +

{example.data.arialabel}

- -

{example.data.arialabel}

- -
{ relatedReferences.length > 0 ? ( @@ -179,9 +192,10 @@ const remixHistoryHasCodeLinks = remixData?.some( /> ) : null } +
- + \ No newline at end of file diff --git a/src/scripts/sync-openprocessing.ts b/src/scripts/sync-openprocessing.ts index 1479687aea..f1aa1644ad 100644 --- a/src/scripts/sync-openprocessing.ts +++ b/src/scripts/sync-openprocessing.ts @@ -6,6 +6,7 @@ import { addAttributionToCode, type RemixEntry, } from "../utils/exampleAttribution"; +import { getExampleCodePath } from "../utils/examplePaths"; import { getCurationSketches } from "./openprocessing"; @@ -75,13 +76,7 @@ async function prepareExample( const data = parseFrontmatter(descriptionContents); - const codePath = join( - descriptionPath.substring( - 0, - descriptionPath.lastIndexOf("/") - ), - "code.js" - ); + const codePath = getExampleCodePath(descriptionPath); let code = await readFile(codePath, "utf-8"); diff --git a/src/utils/exampleAttribution.ts b/src/utils/exampleAttribution.ts index cc2da910be..b036f7c6d6 100644 --- a/src/utils/exampleAttribution.ts +++ b/src/utils/exampleAttribution.ts @@ -3,61 +3,118 @@ export interface Attribution { URL?: string; } +export interface RemixCodeLink { + label?: string; + URL?: string; +} + export interface RemixEntry { description?: string; attribution?: Attribution[]; collectivelyAttributedSince?: number; - code?: { - label?: string; - URL?: string; + code?: RemixCodeLink[]; +} + +export interface ExampleAttributionData { + remixes: { + description: string; + attribution: Attribution[]; }[]; + collectivelyAttributedSince?: number; + codeLinks: RemixCodeLink[]; +} + +export const EXAMPLE_ATTRIBUTION = { + contributors: { + name: "p5.js Contributors", + URL: "https://github.com/processing/p5.js?tab=readme-ov-file#contributors", + }, + foundation: { + name: "Processing Foundation", + URL: "https://processingfoundation.org/people", + }, + license: { + name: "CC BY-NC-SA 4.0", + URL: "https://creativecommons.org/licenses/by-nc-sa/4.0/", + }, +}; + +/** + * Resolves attribution information for an example. + * + * This function is the shared source of truth for both the website + * attribution display and OpenProcessing sketch attribution. + */ +export function getExampleAttributionData( + remixData: RemixEntry[] = [] +): ExampleAttributionData { + const remixes = remixData + .filter( + (item) => + !item.collectivelyAttributedSince && + Boolean(item.attribution?.length) + ) + .map((item) => ({ + description: item.description ?? "Based on", + attribution: item.attribution ?? [], + })); + + const collectivelyAttributedSince = remixData.reduce< + number | undefined + >( + (year, item) => + item.collectivelyAttributedSince ?? year, + undefined + ); + + const codeLinks = remixData + .flatMap((item) => item.code ?? []) + .filter((item) => Boolean(item.URL)); + + return { + remixes, + collectivelyAttributedSince, + codeLinks, + }; } /** - * Creates a plain-text attribution that can be embedded in an + * Creates plain-text attribution that can be embedded in an * OpenProcessing sketch. */ export function generateExampleAttribution( title: string, remixData?: RemixEntry[] ): string { - const lines: string[] = []; - - lines.push(title); - lines.push(""); - - remixData?.forEach((item) => { - if (item.collectivelyAttributedSince || !item.attribution?.length) { - return; - } + const attributionData = getExampleAttributionData(remixData); - const description = item.description ?? "Based on"; + const lines: string[] = [title, ""]; - const authors = item.attribution + for (const remix of attributionData.remixes) { + const authors = remix.attribution .map((author) => - author.URL ? `${author.name} (${author.URL})` : author.name + author.URL + ? `${author.name} (${author.URL})` + : author.name ) .join(", "); - lines.push(`${description}: ${authors}`); - }); - - const collectiveYear = remixData?.reduce( - (year, item) => item.collectivelyAttributedSince ?? year, - undefined - ); + lines.push(`${remix.description}: ${authors}`); + } - if (collectiveYear) { + if (attributionData.collectivelyAttributedSince) { lines.push( - `From ${collectiveYear} onwards, edited and maintained by p5.js Contributors and Processing Foundation.` + `From ${attributionData.collectivelyAttributedSince} onwards, edited and maintained by ${EXAMPLE_ATTRIBUTION.contributors.name} and ${EXAMPLE_ATTRIBUTION.foundation.name}.` ); } else { lines.push( - "Edited and maintained by p5.js Contributors and Processing Foundation." + `Edited and maintained by ${EXAMPLE_ATTRIBUTION.contributors.name} and ${EXAMPLE_ATTRIBUTION.foundation.name}.` ); } - lines.push("Licensed under CC BY-NC-SA 4.0."); + lines.push( + `Licensed under ${EXAMPLE_ATTRIBUTION.license.name}.` + ); return lines.join("\n"); } @@ -70,7 +127,10 @@ export function addAttributionToCode( title: string, remixData?: RemixEntry[] ): string { - const attribution = generateExampleAttribution(title, remixData); + const attribution = generateExampleAttribution( + title, + remixData + ); const comment = attribution .split("\n") diff --git a/src/utils/examplePaths.ts b/src/utils/examplePaths.ts new file mode 100644 index 0000000000..89c35d318b --- /dev/null +++ b/src/utils/examplePaths.ts @@ -0,0 +1,14 @@ +import type path from "node:path"; +import { dirname, join } from "node:path"; + +type PathFunctions = Pick; + +export function getExampleCodePath( + descriptionPath: string, + pathFunctions: PathFunctions = { dirname, join } +): string { + return pathFunctions.join( + pathFunctions.dirname(descriptionPath), + "code.js" + ); +} \ No newline at end of file diff --git a/test/utils/examplePaths.test.ts b/test/utils/examplePaths.test.ts new file mode 100644 index 0000000000..14f3e670a6 --- /dev/null +++ b/test/utils/examplePaths.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { posix, win32 } from "node:path"; + +import { getExampleCodePath } from "../../src/utils/examplePaths"; + +describe("getExampleCodePath", () => { + it("creates a code path for POSIX paths", () => { + const descriptionPath = + "src/content/examples/en/07_Repetition/03_Kaleidoscope/description.mdx"; + + expect( + getExampleCodePath(descriptionPath, posix) + ).toBe( + "src/content/examples/en/07_Repetition/03_Kaleidoscope/code.js" + ); + }); + + it("creates a code path for Windows paths", () => { + const descriptionPath = + "src\\content\\examples\\en\\07_Repetition\\03_Kaleidoscope\\description.mdx"; + + expect( + getExampleCodePath(descriptionPath, win32) + ).toBe( + "src\\content\\examples\\en\\07_Repetition\\03_Kaleidoscope\\code.js" + ); + }); +}); \ No newline at end of file From f72d5f5cce678150213e98fb840b224ee7eaba58 Mon Sep 17 00:00:00 2001 From: Yisakor Mirany Date: Thu, 20 Aug 2026 21:25:38 -0700 Subject: [PATCH 3/3] Refactor import statement for path module --- src/utils/examplePaths.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/examplePaths.ts b/src/utils/examplePaths.ts index 89c35d318b..c3aad4daea 100644 --- a/src/utils/examplePaths.ts +++ b/src/utils/examplePaths.ts @@ -1,5 +1,4 @@ -import type path from "node:path"; -import { dirname, join } from "node:path"; +import path, { dirname, join } from "node:path"; type PathFunctions = Pick; @@ -11,4 +10,4 @@ export function getExampleCodePath( pathFunctions.dirname(descriptionPath), "code.js" ); -} \ No newline at end of file +}