From e63378e5957eb0f53771cc8f45f87c60fa16db2d Mon Sep 17 00:00:00 2001 From: Tom French Date: Thu, 25 Jan 2024 12:19:51 +0000 Subject: [PATCH 1/8] chore: integrate code snippets script into docs --- docs/.gitignore | 2 + docs/docs/noir/standard_library/traits.md | 69 +---- docs/docusaurus.config.ts | 1 + docs/package.json | 5 +- docs/scripts/preprocess/include_code.js | 311 ++++++++++++++++++++++ docs/scripts/preprocess/index.js | 135 ++++++++++ noir_stdlib/src/cmp.nr | 6 +- noir_stdlib/src/ops.nr | 23 +- 8 files changed, 491 insertions(+), 61 deletions(-) create mode 100644 docs/scripts/preprocess/include_code.js create mode 100644 docs/scripts/preprocess/index.js diff --git a/docs/.gitignore b/docs/.gitignore index 4f6eee8284e..501e7e465ea 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -3,6 +3,8 @@ # Production /build +processed-docs +processed-docs-cache # Generated files .docusaurus diff --git a/docs/docs/noir/standard_library/traits.md b/docs/docs/noir/standard_library/traits.md index f2960ca5080..841e512e7de 100644 --- a/docs/docs/noir/standard_library/traits.md +++ b/docs/docs/noir/standard_library/traits.md @@ -56,11 +56,8 @@ types such as arrays are filled with default values of their element type. ### `std::cmp::Eq` -```rust -trait Eq { - fn eq(self, other: Self) -> bool; -} -``` +#include_code eq-trait noir_stdlib/src/cmp.nr rust + Returns `true` if `self` is equal to `other`. Implementing this trait on a type allows the type to be used with `==` and `!=`. @@ -97,13 +94,9 @@ impl Eq for (A, B, C, D, E) where A: Eq, B: Eq, C: Eq, D: Eq, E: Eq { .. } ``` -### `std::cmp::Cmp` +### `std::cmp::Ord` -```rust -trait Cmp { - fn cmp(self, other: Self) -> Ordering; -} -``` +#include_code ord-trait noir_stdlib/src/cmp.nr rust `a.cmp(b)` compares two values returning `Ordering::less()` if `a < b`, `Ordering::equal()` if `a == b`, or `Ordering::greater()` if `a > b`. @@ -151,23 +144,10 @@ These traits abstract over addition, subtraction, multiplication, and division r Implementing these traits for a given type will also allow that type to be used with the corresponding operator for that trait (`+` for Add, etc) in addition to the normal method names. -```rust -trait Add { - fn add(self, other: Self) -> Self; -} - -trait Sub { - fn sub(self, other: Self) -> Self; -} - -trait Mul { - fn mul(self, other: Self) -> Self; -} - -trait Div { - fn div(self, other: Self) -> Self; -} -``` +#include_code add-trait noir_stdlib/src/ops.nr rust +#include_code sub-trait noir_stdlib/src/ops.nr rust +#include_code mul-trait noir_stdlib/src/ops.nr rust +#include_code div-trait noir_stdlib/src/ops.nr rust The implementations block below is given for the `Add` trait, but the same types that implement `Add` also implement `Sub`, `Mul`, and `Div`. @@ -189,11 +169,7 @@ impl Add for u64 { .. } ### `std::ops::Rem` -```rust -trait Rem { - fn rem(self, other: Self) -> Self; -} -``` +#include_code rem-trait noir_stdlib/src/ops.nr rust `Rem::rem(a, b)` is the remainder function returning the result of what is left after dividing `a` and `b`. Implementing `Rem` allows the `%` operator @@ -216,19 +192,9 @@ impl Rem for i64 { fn rem(self, other: i64) -> i64 { self % other } } ### `std::ops::{ BitOr, BitAnd, BitXor }` -```rust -trait BitOr { - fn bitor(self, other: Self) -> Self; -} - -trait BitAnd { - fn bitand(self, other: Self) -> Self; -} - -trait BitXor { - fn bitxor(self, other: Self) -> Self; -} -``` +#include_code bitor-trait noir_stdlib/src/ops.nr rust +#include_code bitand-trait noir_stdlib/src/ops.nr rust +#include_code bitxor-trait noir_stdlib/src/ops.nr rust Traits for the bitwise operations `|`, `&`, and `^`. @@ -255,15 +221,8 @@ impl BitOr for i64 { fn bitor(self, other: i64) -> i64 { self | other } } ### `std::ops::{ Shl, Shr }` -```rust -trait Shl { - fn shl(self, other: Self) -> Self; -} - -trait Shr { - fn shr(self, other: Self) -> Self; -} -``` +#include_code shl-trait noir_stdlib/src/ops.nr rust +#include_code shr-trait noir_stdlib/src/ops.nr rust Traits for a bit shift left and bit shift right. diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index aacc318f5be..4e0d053f61e 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -26,6 +26,7 @@ export default { '@docusaurus/preset-classic', { docs: { + path: "processed-docs", sidebarPath: './sidebars.js', routeBasePath: '/docs', remarkPlugins: [math], diff --git a/docs/package.json b/docs/package.json index 1e3efcfe3d1..6c706e4f514 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,8 +3,9 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "docusaurus start", - "build": "yarn version::stables && docusaurus build", + "preprocess": "yarn node ./scripts/preprocess/index.js", + "start": "yarn preprocess && docusaurus start", + "build": "yarn preprocess && yarn version::stables && docusaurus build", "version::stables": "ts-node ./scripts/setStable.ts", "serve": "serve build" }, diff --git a/docs/scripts/preprocess/include_code.js b/docs/scripts/preprocess/include_code.js new file mode 100644 index 00000000000..3359cb167c8 --- /dev/null +++ b/docs/scripts/preprocess/include_code.js @@ -0,0 +1,311 @@ +const fs = require('fs'); +const path = require('path'); +const childProcess = require('child_process'); + +const getLineNumberFromIndex = (fileContent, index) => { + return fileContent.substring(0, index).split('\n').length; +}; + +/** + * Search for lines of the form + */ +function processHighlighting(codeSnippet, identifier) { + const lines = codeSnippet.split('\n'); + /** + * For an identifier = bar: + * + * Matches of the form: `highlight-next-line:foo:bar:baz` will be replaced with "highlight-next-line". + * Matches of the form: `highlight-next-line:foo:baz` will be replaced with "". + */ + const regex1 = /highlight-next-line:([a-zA-Z0-9-._:]+)/; + const replacement1 = 'highlight-next-line'; + const regex2 = /highlight-start:([a-zA-Z0-9-._:]+)/; + const replacement2 = 'highlight-start'; + const regex3 = /highlight-end:([a-zA-Z0-9-._:]+)/; + const replacement3 = 'highlight-end'; + const regex4 = /this-will-error:([a-zA-Z0-9-._:]+)/; + const replacement4 = 'this-will-error'; + + let result = ''; + let mutated = false; + + const processLine = (line, regex, replacement) => { + const match = line.match(regex); + if (match) { + mutated = true; + + const identifiers = match[1].split(':'); + if (identifiers.includes(identifier)) { + line = line.replace(match[0], replacement); + } else { + // Remove matched text completely + line = line.replace(match[0], ''); + } + } else { + // No match: it's an ordinary line of code. + } + return line.trim() == '//' || line.trim() == '#' ? '' : line; + }; + + for (let line of lines) { + mutated = false; + line = processLine(line, regex1, replacement1); + line = processLine(line, regex2, replacement2); + line = processLine(line, regex3, replacement3); + line = processLine(line, regex4, replacement4); + result += line === '' && mutated ? '' : line + '\n'; + } + + return result.trim(); +} + +let lastReleasedVersion; + +/** Returns the last released tag */ +function getLatestTag() { + if (!lastReleasedVersion) { + const manifest = path.resolve(__dirname, '../../../.release-please-manifest.json'); + lastReleasedVersion = JSON.parse(fs.readFileSync(manifest).toString())['.']; + } + return lastReleasedVersion ? `v${lastReleasedVersion}` : undefined; +} + +/** Returns whether to use the latest release or the current version of stuff. */ +function useLastRelease() { + return process.env.NETLIFY || process.env.INCLUDE_RELEASED_CODE; +} + +/** + * Returns the contents of a file. If the build is running for publishing, it will load the contents + * of the file in the last released version. + */ +function readFile(filePath, tag) { + if (tag && tag !== 'master') { + try { + const root = path.resolve(__dirname, '../../../'); + const relPath = path.relative(root, filePath); + return childProcess.execSync(`git show ${tag}:${relPath}`).toString(); + } catch (err) { + console.error(`Error reading file ${filePath} from version ${tag}. Falling back to current content.`); + } + } + return fs.readFileSync(filePath, 'utf-8'); +} + +/** Extracts a code snippet, trying with the last release if applicable, and falling back to current content. */ +function extractCodeSnippet(filePath, identifier, requesterFile) { + if (useLastRelease()) { + try { + return doExtractCodeSnippet(filePath, identifier, false); + } catch (err) { + console.error( + `Error extracting code snippet ${identifier} from ${path.basename( + filePath, + )} requested by ${requesterFile}: ${err}. Falling back to current content.`, + ); + } + } + + return doExtractCodeSnippet(filePath, identifier, true); +} + +/** + * Parse a code file, looking for identifiers of the form: + * `docs:start:${identifier}` and `docs:end:{identifier}`. + * Extract that section of code. + * + * It's complicated if code snippet identifiers overlap (i.e. the 'start' of one code snippet is in the + * middle of another code snippet). The extra logic in this function searches for all identifiers, and + * removes any which fall within the bounds of the code snippet for this particular `identifier` param. + * @returns the code snippet, and start and end line numbers which can later be used for creating a link to github source code. + */ +function doExtractCodeSnippet(filePath, identifier, useCurrent) { + const tag = useCurrent ? 'master' : getLatestTag(); + let fileContent = readFile(filePath, tag); + let lineRemovalCount = 0; + let linesToRemove = []; + + const startRegex = /(?:\/\/|#)\s+docs:start:([a-zA-Z0-9-._:]+)/g; // `g` will iterate through the regex.exec loop + const endRegex = /(?:\/\/|#)\s+docs:end:([a-zA-Z0-9-._:]+)/g; + + /** + * Search for one of the regex statements in the code file. If it's found, return the line as a string and the line number. + */ + const lookForMatch = (regex) => { + let match; + let matchFound = false; + let matchedLineNum = null; + let actualMatch = null; + let lines = fileContent.split('\n'); + while ((match = regex.exec(fileContent))) { + if (match !== null) { + const identifiers = match[1].split(':'); + let tempMatch = identifiers.includes(identifier) ? match : null; + + if (tempMatch === null) { + // If it's not a match, we'll make a note that we should remove the matched text, because it's from some other identifier and should not appear in the snippet for this identifier. + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + if (line.trim() == match[0].trim()) { + linesToRemove.push(i + 1); // lines are indexed from 1 + ++lineRemovalCount; + } + } + } else { + if (matchFound === true) { + throw new Error(`Duplicate for regex ${regex} and identifier ${identifier}`); + } + matchFound = true; + matchedLineNum = getLineNumberFromIndex(fileContent, tempMatch.index); + actualMatch = tempMatch; + } + } + } + + return [actualMatch, matchedLineNum]; + }; + + let [startMatch, startLineNum] = lookForMatch(startRegex); + let [endMatch, endLineNum] = lookForMatch(endRegex); + + // Double-check that the extracted line actually contains the required start and end identifier. + if (startMatch !== null) { + const startIdentifiers = startMatch[1].split(':'); + startMatch = startIdentifiers.includes(identifier) ? startMatch : null; + } + if (endMatch !== null) { + const endIdentifiers = endMatch[1].split(':'); + endMatch = endIdentifiers.includes(identifier) ? endMatch : null; + } + + if (startMatch === null || endMatch === null) { + if (startMatch === null && endMatch === null) { + throw new Error(`Identifier "${identifier}" not found in file "${filePath}"`); + } else if (startMatch === null) { + throw new Error(`Start line "docs:start:${identifier}" not found in file "${filePath}"`); + } else { + throw new Error(`End line "docs:end:${identifier}" not found in file "${filePath}"`); + } + } + + let lines = fileContent.split('\n'); + + // We only want to remove lines which actually fall within the bounds of our code snippet, so narrow down the list of lines that we actually want to remove. + linesToRemove = linesToRemove.filter((lineNum) => { + const removal_in_bounds = lineNum >= startLineNum && lineNum <= endLineNum; + return removal_in_bounds; + }); + + // Remove lines which contain `docs:` comments for unrelated identifiers: + lines = lines.filter((l, i) => { + return !linesToRemove.includes(i + 1); // lines are indexed from 1 + }); + + // Remove lines from the snippet which fall outside the `docs:start` and `docs:end` values. + lines = lines.filter((l, i) => { + return i + 1 > startLineNum && i + 1 < endLineNum - linesToRemove.length; // lines are indexed from 1 + }); + + // We have our code snippet! + let codeSnippet = lines.join('\n'); + + // The code snippet might contain some docusaurus highlighting comments for other identifiers. We should remove those. + codeSnippet = processHighlighting(codeSnippet, identifier); + + return [codeSnippet, startLineNum, endLineNum, tag]; +} + +/** + * Explaining this regex: + * + * E.g. `#include_code snippet_identifier /circuits/my_code.cpp cpp` + * + * #include_code\s+(\S+)\s+(\S+)\s+(\S+) + * - This is the main regex to match the above format. + * - \s+: one or more whitespace characters (space or tab) after `include_code` command. + * - (\S+): one or more non-whitespaced characters. Captures this as the first argument, which is a human-readable identifier for the code block. + * - etc. + * + * Lookaheads are needed to allow us to ignore commented-out lines: + * + * ^(?!