diff --git a/.gitmodules b/.gitmodules index 431b428..e23cd53 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "test/upstream"] path = test/upstream url = https://github.com/kdl-org/kdl +[submodule "test/upstream-v1"] + path = test/upstream-v1 + url = https://github.com/kdl-org/kdl diff --git a/documentation/src/SUMMARY.md b/documentation/src/SUMMARY.md index 36db843..067bc95 100644 --- a/documentation/src/SUMMARY.md +++ b/documentation/src/SUMMARY.md @@ -7,6 +7,7 @@ - [Parsing KDL](./api/parse.md) - [Formatting KDL](./api/format.md) - [JSON-in-KDL](./api/json.md) +- [Compabitility with KDL v1](./api/v1-compat.md) - [API Reference](./api/reference/index.md) - [`@bgotink/kdl`](./api/reference/index/index.md) - [Document](./api/reference/index/classes/Document.md) @@ -18,6 +19,7 @@ - [InvalidKdlError](./api/reference/index/classes/InvalidKdlError.md) - [`@bgotink/kdl/json`](./api/reference/json/index.md) - [InvalidJsonInKdlError](./api/reference/json/classes/InvalidJsonInKdlError.md) + - [`@bgotink/kdl/v1-compat`](./api/reference/v1-compat/index.md) # Internals diff --git a/documentation/src/api/v1-compat.md b/documentation/src/api/v1-compat.md new file mode 100644 index 0000000..33bbe0f --- /dev/null +++ b/documentation/src/api/v1-compat.md @@ -0,0 +1,50 @@ +# Compabitility with KDL v1 + +The `parse` function in this package only supports KDL v2 documents. +If your program wants to be compatible with KDL v1 as well, this package exposes an extra entry point: [`@bgotink/kdl/v1-compat`](./reference/v1-compat/index.md). + +## Loading compatibility code + +The compatibility endpoint is a lot larger and slower than the regular `@bgotink/kdl` endpoint. +Programs that want to provide compatiblity are therefore encouraged to lazy-load the compatibility code, for example: + +```js +import {readFile} from "fs/promises"; +import {parse} from "@bgotink/kdl"; + +export async function loadConfiguration(path) { + const content = await readFile(path); + + try { + return parse(content); + } catch (e) { + const {parseWithoutFormatting} = await import("@bgotink/kdl/v1-compat"); + try { + return parseWithoutFormatting(content); + } catch { + throw e; + } + } +} +``` + +### Numbers + +Here's a comparison of `@bgotink/kdl` and `@bgotink/kdl/v1-compat` in numbers: + +| what | `@bgotink/kdl` | `@bgotink/kdl/v1-compat` | +| ---------------------------------- | -------------- | ------------------------ | +| size | 81,920 bytes | 552,783 bytes | +| size (bundled + minified) | 24,593 bytes | 261,295 bytes | +| size (bundled + minified + zipped) | 7,489 bytes | 63,315 bytes | +| benchmark | 27k docs/sec | 17k docs/sec | + +## Two compatibility functions + +The compatibility endpoint exposes two functions that parse a KDL v1 text into a [`Document`](./reference/index/classes/Document.md). +Even though both functions yield documents that are functionally equivalent, they serve very different purposes: + +- [`parseWithoutFormatting`](./reference/v1-compat/index.md#parsewithoutformatting) reads the KDL v1 text without storing any formatting, whitespace, comments, etc. + The resulting document can be used everywhere a regular KDL v2 document can be used, but formatting the document will throw out any formatting its author added. +- [`parseAndTransform`](./reference/v1-compat/index.md#parseandtransform) reads the KDL v1 text and transforms any formatting, whitespace, comments, etc. into their KDL v2 equivalent. + The resulting document can safely be formatted to overwrite the original KDL v1 text to automate the migration from KDL v1 to KDL v2. diff --git a/package.json b/package.json index f43305f..f7336d5 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,12 @@ "exports": { ".": "./src/index.js", "./json": "./src/json.js", + "./v1-compat": "./src/v1-compat.js", "./package.json": "./package.json" }, + "imports": { + "#v1": "@bgotink/kdl-v1" + }, "scripts": { "postinstall": "is-ci || husky", "build": "node scripts/build.js", @@ -33,6 +37,7 @@ "book": "scripts/book.sh" }, "devDependencies": { + "@bgotink/kdl-v1": "npm:@bgotink/kdl@0.1.6", "@types/benchmark": "^2.1.5", "@types/node": "^22", "benchmark": "^2.1.4", diff --git a/scripts/bench.js b/scripts/bench.js index 226e88c..04209ad 100644 --- a/scripts/bench.js +++ b/scripts/bench.js @@ -1,7 +1,9 @@ import bench from "benchmark"; import * as kdljs from "kdljs"; +import * as v1 from "@bgotink/kdl-v1"; import * as self from "../src/index.js"; +import * as compat from "../src/v1-compat.js"; const suite = new bench.Suite(); @@ -44,6 +46,18 @@ suite.add("kdljs #parse", () => { kdljs.parse(documentV1); }); +suite.add("v1 #parse", () => { + v1.parse(documentV1); +}); + +suite.add("v1 compat #parseAndTransform", () => { + compat.parseAndTransform(documentV1); +}); + +suite.add("v1 compat #parseWithoutFormatting", () => { + compat.parseWithoutFormatting(documentV1); +}); + suite.add("development #parse", () => { self.parse(document); }); diff --git a/scripts/book.sh b/scripts/book.sh index f854f93..0abf4a5 100755 --- a/scripts/book.sh +++ b/scripts/book.sh @@ -21,7 +21,7 @@ yarn typedoc \ --membersWithOwnFile Class \ --hideBreadcrumbs \ --hidePageHeader \ - src/index.js src/json.d.ts + src/index.js src/json.d.ts src/v1-compat.js cd documentation mdbook "${1:-build}" diff --git a/scripts/build.js b/scripts/build.js index 4dc6dcc..e111eac 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -7,6 +7,7 @@ import { rmSync, mkdirSync, copyFileSync, + cpSync, } from "node:fs"; import process from "node:process"; import {fileURLToPath} from "node:url"; @@ -24,7 +25,8 @@ execSync("tsc -p tsconfig.compile.json"); // JavaScript -execSync("cp -a src/* out"); +cpSync("src", "out", {recursive: true}); +copyFileSync(new URL(import.meta.resolve("#v1")), "out/v1.cjs"); // Write metadata @@ -41,6 +43,9 @@ delete packageJson.packageManager; // Set exports packageJson.main = "./index.js"; packageJson.types = "./index.d.ts"; +packageJson.imports = { + "#v1": "./v1.cjs", +}; packageJson.exports = { ".": { types: "./index.d.ts", @@ -50,6 +55,10 @@ packageJson.exports = { types: "./json.d.ts", default: "./json.js", }, + "./v1-compat": { + types: "./v1-compat.d.ts", + default: "./v1-compat.js", + }, "./package.json": "./package.json", }; diff --git a/src/error.js b/src/error.js index 9afec3f..62de37f 100644 --- a/src/error.js +++ b/src/error.js @@ -12,6 +12,8 @@ export class InvalidKdlError extends Error { name = "InvalidKdlError"; /** + * The location of the error, if it can be tied to a single location + * * @readonly * @type {Token=} */ diff --git a/src/format.js b/src/format.js index 61c0b84..053192b 100644 --- a/src/format.js +++ b/src/format.js @@ -1,9 +1,8 @@ import {InvalidKdlError} from "./index.js"; import {Document, Entry, Identifier, Node, Value} from "./model.js"; import {Tag} from "./model/tag.js"; +import {isValidBareIdentifier} from "./string-utils.js"; -const rePlainIdentifier = - /(?![+-]?[0-9])(?:(?!﹦|=|🟰)[^(){}\[\]/\\"#;=\x09-\x0D\x20\x85\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000])+/; const reInlineWhitespace = /[\uFEFF\u0009\u000B\u0020\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/; @@ -47,8 +46,7 @@ function formatValue(value) { } if (typeof value.value === "string") { - const plainMatch = rePlainIdentifier.exec(value.value); - if (plainMatch && plainMatch[0].length === plainMatch.input.length) { + if (isValidBareIdentifier(value.value)) { return value.value; } } @@ -74,8 +72,7 @@ function formatIdentifier(identifier) { return identifier.representation; } - const plainMatch = rePlainIdentifier.exec(identifier.name); - if (plainMatch && plainMatch[0].length === plainMatch.input.length) { + if (isValidBareIdentifier(identifier.name)) { return identifier.name; } diff --git a/src/parse.d.ts b/src/parse.d.ts index b1d0890..1d04dbd 100644 --- a/src/parse.d.ts +++ b/src/parse.d.ts @@ -3,7 +3,7 @@ import type {LineSpace, NodeSpace} from "./model/whitespace.js"; export class InvalidKdlError extends Error {} -interface ParserResult { +export interface ParserResult { value: Value; identifier: Identifier; entry: Entry; diff --git a/src/parser/tokenize.js b/src/parser/tokenize.js index 78f672e..19b2b93 100644 --- a/src/parser/tokenize.js +++ b/src/parser/tokenize.js @@ -93,7 +93,7 @@ function isNewLine(codePoint) { } /** @param {number} codePoint */ -function isInvalidCharacter(codePoint) { +export function isInvalidCharacter(codePoint) { return ( // Everything < \x20 except those that count as whitespace codePoint < 0x08 || @@ -113,7 +113,7 @@ function isInvalidCharacter(codePoint) { } /** @param {number} codePoint */ -function isIdentifierChar(codePoint) { +export function isIdentifierChar(codePoint) { // All other functions check whether the code point is one of a set of values, // this check does the opposite, it checks that the code point doesn't have // certain values. diff --git a/src/string-utils.js b/src/string-utils.js index 49c7bab..9151c48 100644 --- a/src/string-utils.js +++ b/src/string-utils.js @@ -1,4 +1,5 @@ import {InvalidKdlError} from "./error.js"; +import {isIdentifierChar, isInvalidCharacter} from "./parser/tokenize.js"; /** @import {ParserCtx} from "./parser/parse.js" */ /** @import {Token} from "./parser/tokenize.js" */ @@ -8,6 +9,35 @@ const escapeWhitespace = const escape = /\\(?:$|u\{([0-9a-fA-F]{1,5}|10[0-9a-fA-F]{4})\}|u(\{[^}]{1,6}\}?|[0-9a-fA-F]{1,5}|10[0-9a-fA-F]{4})|.)/g; +/** @param {string} identifier */ +export function isValidBareIdentifier(identifier) { + // no values that can be confused with keywords + if ( + identifier === "true" || + identifier === "false" || + identifier === "null" || + identifier === "inf" || + identifier === "-inf" || + identifier === "nan" + ) { + return false; + } + + // no empty strings + if (identifier === "") { + return false; + } + + for (const part of identifier) { + const c = /** @type {number} */ (part.codePointAt(0)); + if (isInvalidCharacter(c) || !isIdentifierChar(c)) { + return false; + } + } + + return !/^[+-]?\.?[0-9]/.test(identifier); +} + const escapedValues = new Map([ ["\\n", "\n"], ["\\r", "\r"], @@ -19,7 +49,7 @@ const escapedValues = new Map([ ["\\s", " "], ]); -const reAllNewlines = /\x0D\x0A|[\x0A\x0C\x0D\x85\u2028\u2029]/; +export const reNewline = /\x0D\x0A|[\x0A\x0C\x0D\x85\u2028\u2029]/; const reEntirelyInlineWhitespace = /^[\uFEFF\u0009\u000B\u0020\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]*$/; @@ -31,7 +61,7 @@ const reEntirelyInlineWhitespace = * @returns {string} */ export function postProcessRawStringValue(ctx, value, token) { - if (reAllNewlines.test(value)) { + if (reNewline.test(value)) { ctx.errors.push( new InvalidKdlError( `Raw strings with single quotes cannot contain any newlines, use triple-quotes for multiline strings`, @@ -50,7 +80,7 @@ export function postProcessRawStringValue(ctx, value, token) { * @returns {string} */ export function postProcessMultilineRawStringValue(ctx, value, token) { - const lines = value.split(reAllNewlines); + const lines = value.split(reNewline); if (lines.length === 1) { ctx.errors.push( @@ -115,7 +145,7 @@ export function postProcessMultilineRawStringValue(ctx, value, token) { export function postProcessStringValue(ctx, value, token) { value = removeWhitespaceEscapes(value); - if (reAllNewlines.test(value)) { + if (reNewline.test(value)) { // mustn't be a multiline string... ctx.errors.push( new InvalidKdlError( @@ -135,7 +165,7 @@ export function postProcessStringValue(ctx, value, token) { * @returns {string} */ export function postProcessMultilineStringValue(ctx, value, token) { - const lines = removeWhitespaceEscapes(value).split(reAllNewlines); + const lines = removeWhitespaceEscapes(value).split(reNewline); if (lines.length === 1) { ctx.errors.push( diff --git a/src/v1-compat.js b/src/v1-compat.js new file mode 100644 index 0000000..292236b --- /dev/null +++ b/src/v1-compat.js @@ -0,0 +1,364 @@ +import {parse as parseV1} from "#v1"; + +import { + Document, + Entry, + Identifier, + Node, + Tag, + Value, + format, +} from "./index.js"; +import {reNewline, isValidBareIdentifier} from "./string-utils.js"; + +/** + * @param {string | undefined} representation + * @param {string} value + */ +function _mapStringRepresentation(representation, value) { + if (!representation || representation === value) { + // Currently a ident string, check if the string can still be an ident string in v2 + return isValidBareIdentifier(value) ? value : JSON.stringify(value); + } + + if (representation.startsWith("r#")) { + // Raw strings no longer have an r prefix, so r#"test"# becomes #"test"# + representation = representation.slice(1); + } else if (representation.startsWith('r"')) { + // Raw strings no longer have an r prefix, so r"test" becomes #"test"# + representation = `#${representation.slice(1)}#`; + } else { + representation = representation.replaceAll("\\/", "/"); + } + + // One edge case that's no longer supported in v2 with the introduction of triple-quote multiline strings + if (representation === '#"""#') { + return '"\\""'; + } + + if (!reNewline.test(representation)) { + // Single line string -> we're done here! + return representation; + } + + // Multiline string, that's no longer allowed without triple-quotes. + const numberOfHashes = representation.indexOf('"'); + const hashes = "#".repeat(numberOfHashes); + return ( + hashes + + '"""\n' + + representation.slice(numberOfHashes + 1, -(numberOfHashes + 1)) + + '\n"""' + + hashes + ); +} + +/** + * @param {import("#v1").Identifier} identifier + * @returns {Identifier} + */ +function mapIdentifierWithFormatting(identifier) { + const result = new Identifier(identifier.name); + + result.representation = _mapStringRepresentation( + identifier.representation, + identifier.name, + ); + + return result; +} + +/** + * @param {import("#v1").Identifier} identifier + * @returns {Identifier} + */ +function mapIdentifier(identifier) { + return new Identifier(identifier.name); +} + +/** + * @param {import("#v1").Identifier} identifier + * @returns {Tag} + */ +function mapTagWithFormatting(identifier) { + const result = new Tag(identifier.name); + + result.representation = _mapStringRepresentation( + identifier.representation, + identifier.name, + ); + + return result; +} + +/** + * @param {import("#v1").Identifier} identifier + * @returns {Tag} + */ +function mapTag(identifier) { + return new Tag(identifier.name); +} + +/** + * @param {import("#v1").Value} value + * @returns {Value} + */ +function mapValueWithFormatting(value) { + const result = new Value(value.value); + + if (typeof result.value === "boolean" || result.value === null) { + result.representation = `#${result.value}`; + } else if (typeof value.value === "string") { + result.representation = _mapStringRepresentation( + value.representation, + value.value, + ); + } else { + result.representation = value.representation; + } + + return result; +} + +/** + * @param {import("#v1").Value} value + * @returns {Value} + */ +function mapValue(value) { + return new Value(value.value); +} + +/** + * @param {import("#v1").Entry} entry + * @returns {Entry} + */ +function mapEntryWithFormatting(entry) { + const result = new Entry( + mapValueWithFormatting(entry.value), + entry.name && mapIdentifierWithFormatting(entry.name), + ); + + result.tag = entry.tag && mapTagWithFormatting(entry.tag); + + result.equals = entry.name ? "=" : undefined; + + result.leading = mapWhitespaceInNode(entry.leading); + result.trailing = mapWhitespaceInNode(entry.trailing); + + return result; +} + +/** + * @param {import("#v1").Entry} entry + * @returns {Entry} + */ +function mapEntry(entry) { + const result = new Entry( + mapValue(entry.value), + entry.name && mapIdentifier(entry.name), + ); + + result.tag = entry.tag && mapTag(entry.tag); + + return result; +} + +/** + * @param {import("#v1").Node} node + * @returns {Node} + */ +function mapNodeWithFormatting(node) { + const result = new Node(mapIdentifierWithFormatting(node.name)); + + result.tag = node.tag && mapTagWithFormatting(node.tag); + + result.entries = node.entries.map((entry) => mapEntryWithFormatting(entry)); + + result.leading = mapWhitespaceInDocument(node.leading); + result.beforeChildren = mapWhitespaceInNode(node.beforeChildren); + result.children = node.children && mapDocumentWithFormatting(node.children); + + // node.trailing contains not just trailing whitespace but also the node terminator + + let trailing = node.trailing; + if (trailing != null) { + let terminator = ""; + + if (trailing?.endsWith(";")) { + terminator = ";"; + trailing = trailing.slice(0, -1); + } else if (reNewline.test(trailing.slice(-1))) { + // \r\n is a single newline + if (trailing.slice(-2) === "\r\n") { + terminator = "\r\n"; + trailing = trailing.slice(0, -2); + } else { + terminator = trailing.slice(-1); + trailing = trailing.slice(0, -1); + } + } + + result.trailing = (trailing && mapWhitespaceInNode(trailing)) + terminator; + } + + return result; +} + +/** + * @param {import("#v1").Node} node + * @returns {Node} + */ +function mapNode(node) { + const result = new Node(mapIdentifier(node.name)); + + result.tag = node.tag && mapTag(node.tag); + + result.entries = node.entries.map((entry) => mapEntry(entry)); + result.children = + node.children?.nodes.length ? mapDocument(node.children) : null; + + return result; +} + +/** + * @param {import("#v1").Document} document + * @returns {Document} + */ +function mapDocumentWithFormatting(document) { + const result = new Document( + document.nodes.map((node) => mapNodeWithFormatting(node)), + ); + + result.trailing = mapWhitespaceInDocument(document.trailing); + + return result; +} + +/** + * @param {import("#v1").Document} document + * @returns {Document} + */ +function mapDocument(document) { + return new Document(document.nodes.map((node) => mapNode(node))); +} + +/** + * @param {string=} whitespace + * @returns {string=} + */ +function mapWhitespaceInNode(whitespace) { + if (!whitespace?.includes("/-")) { + return whitespace; + } + + return parseV1(whitespace, {as: "whitespace in node"}) + .map((part) => { + if (part.type !== "slashdash") { + return part.content; + } + + if (!part.content.endsWith("}")) { + return `/-${format(mapEntryWithFormatting(parseV1(part.content.slice(2), {as: "entry"})))}`; + } + + const startOfChildren = part.content.indexOf("{") + 1; + + return ( + part.content.slice(0, startOfChildren) + + format(parseAndTransform(part.content.slice(startOfChildren, -1))) + + "}" + ); + }) + .join(""); +} +/** + * @param {string=} whitespace + * @returns {string=} + */ +function mapWhitespaceInDocument(whitespace) { + if (!whitespace?.includes("/-")) { + return whitespace; + } + + return parseV1(whitespace, {as: "whitespace in document"}) + .map((part) => { + if (part.type !== "slashdash") { + return part.content; + } + + const node = parseV1(part.content.slice(2), {as: "node"}); + return `/-${format(mapNodeWithFormatting(node))}`; + }) + .join(""); +} + +/** + * Parse the given KDL v1 text as document without storing any formatting information + * + * {@link format Formatting} the given document will result in a KDL v2 text + * which represents the same document as the original KDL v1 text but where all + * comments, whitespace, etc. is removed and standardized. + * If you want to format the document to transform the given KDL v1 text into + * KDL v2 text, use {@link parseAndTransform} instead. + * + * This function incurs significant overhead, as it loads a parser for KDL v1. + * This parser is an order of magnitude larger and slower than the v2 parser, + * so programs implementing compatibility with both KDL v2 and v1 are + * encouraged to lazy load this compat function only when the KDL document + * fails to parse as KDL v2. + * + * @see {@link parseAndTransform} + * @param {Parameters[0]} text + * @returns {Document} + */ +export function parseWithoutFormatting(text) { + if (typeof text !== "string") { + if (typeof TextDecoder !== "function") { + throw new TypeError( + "Uint8Array input is only supported on platforms that include TextDecoder", + ); + } + + const decoder = new TextDecoder("utf-8", {fatal: true}); + + text = decoder.decode(text); + } + + return mapDocument(parseV1(text)); +} + +/** + * Parse the given KDL v1 text and turn it into a KDL v2 document + * + * {@link format Formatting} the given document will result in a KDL v2 text + * equivalent to the original KDL v1 text. + * + * This function is designed to be used to transform a KDL v1 document into an + * equivalent KDL v2 document. If that's not the intention, callers are + * encouraged to use {@link parseWithoutFormatting} instead + * + * This function incurs significant overhead, as it loads a parser for KDL v1. + * This parser is an order of magnitude larger and slower than the v2 parser, + * so programs implementing compatibility with both KDL v2 and v1 are + * encouraged to lazy load this compat function only when the KDL document + * fails to parse as KDL v2. + * + * @see {@link parseWithoutFormatting} + * @param {Parameters[0]} text + * @returns {Document} + */ +export function parseAndTransform(text) { + if (typeof text !== "string") { + if (typeof TextDecoder !== "function") { + throw new TypeError( + "Uint8Array input is only supported on platforms that include TextDecoder", + ); + } + + const decoder = new TextDecoder("utf-8", {fatal: true}); + + text = decoder.decode(text); + } + + return mapDocumentWithFormatting(parseV1(text)); +} diff --git a/test/compat-upstream.js b/test/compat-upstream.js new file mode 100644 index 0000000..35640e7 --- /dev/null +++ b/test/compat-upstream.js @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import {readFileSync, readdirSync, existsSync} from "node:fs"; +import {test} from "uvu"; + +import {format, parse} from "../src/index.js"; +import {parseWithoutFormatting, parseAndTransform} from "../src/v1-compat.js"; + +const testCasesFolder = new URL( + "upstream-v1/tests/test_cases/", + import.meta.url, +); + +const knownBrokenTests = new Set([ + // All JavaScript numbers are floats, meaning the largest + // integer number we can represent is lower than what + // the KDL tests expect + "hex.kdl", + "hex_int.kdl", + + // These tests use numbers that require a bigdecimal + // implementation to represent the value, which our + // javascript engine doesn't provide (yet?) + "sci_notation_large.kdl", + "sci_notation_small.kdl", +]); + +for (const testCase of readdirSync(new URL("input", testCasesFolder))) { + const inputFile = new URL(`input/${testCase}`, testCasesFolder); + const expectedOutputFile = new URL( + `expected_kdl/${testCase}`, + testCasesFolder, + ); + + if (knownBrokenTests.has(testCase) || !existsSync(expectedOutputFile)) { + continue; + } + + const input = readFileSync(inputFile, "utf8"); + const expectedOutput = readFileSync(expectedOutputFile, "utf8"); + + test(testCase, () => { + // There are two parts to this test: + // + // - Check whether the expected output and the input parse into the same + // document. This validates that we correctly handle a bunch of edge cases + // correctly. + // - Check whether the returned document (with formatting info attached) will + // be formatted correctly as KDL v2. Note the formatting function doesn't + // validate the formatting info, so if the info is wrong the format + // function would happily output invalid KDL files. + + if (testCase !== "repeated_prop.kdl") { + // Skip this assertion for repeated_prop as we store the repeated props so + // the two values wouldn't be deepEqual. + + assert.deepEqual( + parseWithoutFormatting(input), + parseWithoutFormatting(expectedOutput), + ); + } + + const document = parseAndTransform(input); + + assert.deepEqual(parse(format(document)), document); + }); +} + +test.run(); diff --git a/test/compat.js b/test/compat.js new file mode 100644 index 0000000..78986ca --- /dev/null +++ b/test/compat.js @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import {test} from "uvu"; + +import {clearFormat, parse} from "../src/index.js"; +import {parseWithoutFormatting, parseAndTransform} from "../src/v1-compat.js"; + +/** @type {{name: string; v1: string; v2: string}[]} */ +const tests = [ + { + name: "old keywords", + v1: String.raw` + node true /- false null + `, + v2: String.raw` + node #true /- #false #null + `, + }, + { + name: "new keywords", + v1: String.raw` + inf + nan + node inf=0 nan=0 + `, + v2: String.raw` + "inf" + "nan" + node "inf"=0 "nan"=0 + `, + }, + { + name: "single-line strings", + v1: String.raw` + node "test" r"test" r#"test"# + `, + v2: String.raw` + node "test" #"test"# #"test"# + `, + }, + { + name: "multiline-line strings", + v1: String.raw` +node "test +lines" r"test +lines" r#"test + lines"# + `, + v2: String.raw` +node """ +test +lines +""" #""" +test +lines +"""# #""" +test + lines +"""# + `, + }, +]; + +for (const {name, v1, v2} of tests) { + test(name, () => { + assert.deepEqual(parseWithoutFormatting(v1), clearFormat(parse(v2))); + assert.deepEqual(parseAndTransform(v1), parse(v2)); + }); +} + +test.run(); diff --git a/test/upstream-v1 b/test/upstream-v1 new file mode 160000 index 0000000..ef93a6b --- /dev/null +++ b/test/upstream-v1 @@ -0,0 +1 @@ +Subproject commit ef93a6b10c4e16d94194280bb6687661d7024476 diff --git a/yarn.lock b/yarn.lock index fbbacf9..314f224 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,10 +5,18 @@ __metadata: version: 8 cacheKey: 10c0 +"@bgotink/kdl-v1@npm:@bgotink/kdl@0.1.6": + version: 0.1.6 + resolution: "@bgotink/kdl@npm:0.1.6" + checksum: a04f09648c3ec6ecba65322cdab63617af93590475eca31bf0e0f5601d2fc56d7ebd40eaa83546ff2632d728d988043b797c5638ab16c913c07c16d7ec56a7ae + languageName: node + linkType: hard + "@bgotink/kdl@workspace:.": version: 0.0.0-use.local resolution: "@bgotink/kdl@workspace:." dependencies: + "@bgotink/kdl-v1": "npm:@bgotink/kdl@0.1.6" "@types/benchmark": "npm:^2.1.5" "@types/node": "npm:^22" benchmark: "npm:^2.1.4"