From 4535c9768be525ec97b5d83a5ae46dae1f914cff Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Mon, 10 Feb 2025 23:53:54 -0800 Subject: [PATCH] Add jref-util tests and related bugfix/cleanup --- src/hyperjump/embedded.test.js | 31 +- src/hyperjump/get.test.js | 2 +- src/hyperjump/hyperjump.js | 8 +- .../media-types/jref-media-type-plugin.js | 8 +- .../media-types/json-media-type-plugin.js | 8 +- .../uri-schemes/file-scheme-plugin.js | 2 +- .../uri-schemes/http-scheme-plugin.js | 2 +- src/jref/jref-ast.d.ts | 3 + src/jref/jref-parse.js | 14 +- src/jref/jref-stringify.js | 2 +- src/jref/jref-util.js | 31 +- src/jref/jref-util.test.js | 595 ++++++++++++++++++ src/jref/jref.js | 2 +- src/json/json-lexer.js | 2 +- src/json/jsonast-util.js | 61 +- src/json/jsonast-util.test.js | 5 +- src/json/rejson-parse.js | 14 +- src/json/rejson-stringify.js | 2 +- src/json/rejson.js | 2 +- 19 files changed, 688 insertions(+), 106 deletions(-) create mode 100644 src/jref/jref-util.test.js diff --git a/src/hyperjump/embedded.test.js b/src/hyperjump/embedded.test.js index 9850dcb..40e12c3 100644 --- a/src/hyperjump/embedded.test.js +++ b/src/hyperjump/embedded.test.js @@ -6,6 +6,8 @@ import { fromJref, toJref } from "../jref/index.js"; /** * @import { DocumentNode } from "./index.js" + * @import { JrefNode } from "../jref/index.js" + * @import { Reviver } from "../jref/index.js" */ @@ -16,22 +18,25 @@ describe("JSON Browser", () => { const testMediaType = "application/prs.hyperjump-embedded-test"; beforeAll(() => { - /** @type (uri: string, text: string, embedded?: Record) => DocumentNode */ + /** @type (uri: string, text: string, embedded?: Record) => DocumentNode */ const parseToDocument = (uri, text, embedded = {}) => { + /** @type Reviver */ + const embeddedReviver = (node, key) => { + if (key === "$embedded" && node.type === "json" && node.jsonType === "object") { + for (const propertyNode of node.children) { + const embeddedUri = toAbsoluteIri(propertyNode.children[0].value); + const embeddedJref = toJref(propertyNode.children[1], uri); + embedded[embeddedUri] = parseToDocument(embeddedUri, embeddedJref, embedded); + } + return; + } else { + return node; + } + }; + return { uri: uri, - children: [fromJref(text, uri, (node, key) => { - if (key === "$embedded" && node.type === "json" && node.jsonType === "object") { - for (const propertyNode of node.children) { - const embeddedUri = toAbsoluteIri(propertyNode.children[0].value); - const embeddedJref = toJref(propertyNode.children[1], uri); - embedded[embeddedUri] = parseToDocument(embeddedUri, embeddedJref, embedded); - } - return; - } else { - return node; - } - })], + children: [fromJref(text, uri, embeddedReviver)], fragmentKind: "json-pointer", embedded: embedded }; diff --git a/src/hyperjump/get.test.js b/src/hyperjump/get.test.js index 1eeb54b..96fd9ae 100644 --- a/src/hyperjump/get.test.js +++ b/src/hyperjump/get.test.js @@ -41,7 +41,7 @@ describe("JSON Browser", () => { const browser = new Hyperjump(); const subject = await browser.get(uri); - expect(toJref(subject, uri)).to.eql(jref); + expect(toJref(subject, uri, undefined, " ")).to.eql(jref); }); test("follow fragment-only reference", async () => { diff --git a/src/hyperjump/hyperjump.js b/src/hyperjump/hyperjump.js index fe2c5fa..4c45dec 100644 --- a/src/hyperjump/hyperjump.js +++ b/src/hyperjump/hyperjump.js @@ -9,10 +9,10 @@ import { JrefMediaTypePlugin } from "./media-types/jref-media-type-plugin.js"; import { pointerGet, pointerStep } from "../jref/jref-util.js"; /** - * @import { JrefNode } from "../jref/jref-ast.d.ts" - * @import { JsonCompatible, JsonType } from "../json/jsonast.d.ts" - * @import { UriSchemePlugin } from "./uri-schemes/uri-scheme-plugin.d.ts" - * @import { DocumentNode, MediaTypePlugin } from "./media-types/media-type-plugin.d.ts" + * @import { JrefNode } from "../jref/jref-ast.js" + * @import { JsonCompatible, JsonType } from "../json/jsonast.js" + * @import { UriSchemePlugin } from "./uri-schemes/uri-scheme-plugin.js" + * @import { DocumentNode, MediaTypePlugin } from "./media-types/media-type-plugin.js" */ diff --git a/src/hyperjump/media-types/jref-media-type-plugin.js b/src/hyperjump/media-types/jref-media-type-plugin.js index e0aaeef..debbcc7 100644 --- a/src/hyperjump/media-types/jref-media-type-plugin.js +++ b/src/hyperjump/media-types/jref-media-type-plugin.js @@ -1,8 +1,8 @@ import { fromJref } from "../../jref/jref-util.js"; /** - * @import { MediaTypePlugin } from "./media-type-plugin.d.ts" - * @import { JrefDocumentNode, JrefNode } from "../../jref/jref-ast.d.ts" + * @import { MediaTypePlugin } from "./media-type-plugin.js" + * @import { JrefDocumentNode } from "../../jref/jref-ast.js" */ @@ -22,9 +22,7 @@ export class JrefMediaTypePlugin { async parse(response) { return { type: "jref-document", - children: [ - /** @type JrefNode */ (fromJref(await response.text(), response.url)) - ], + children: [fromJref(await response.text(), response.url)], uri: response.url, fragmentKind: "json-pointer" }; diff --git a/src/hyperjump/media-types/json-media-type-plugin.js b/src/hyperjump/media-types/json-media-type-plugin.js index 5f6cb96..fa08f72 100644 --- a/src/hyperjump/media-types/json-media-type-plugin.js +++ b/src/hyperjump/media-types/json-media-type-plugin.js @@ -1,8 +1,8 @@ import { fromJson } from "../../json/jsonast-util.js"; /** - * @import { MediaTypePlugin } from "./media-type-plugin.d.ts" - * @import { JsonDocumentNode, JsonNode } from "../../json/jsonast.js" + * @import { MediaTypePlugin } from "./media-type-plugin.js" + * @import { JsonDocumentNode } from "../../json/jsonast.js" */ @@ -22,9 +22,7 @@ export class JsonMediaTypePlugin { async parse(response) { return { type: "json-document", - children: [ - /** @type JsonNode */ (fromJson(await response.text())) - ] + children: [fromJson(await response.text())] }; } diff --git a/src/hyperjump/uri-schemes/file-scheme-plugin.js b/src/hyperjump/uri-schemes/file-scheme-plugin.js index e4cc7c2..6c0e040 100644 --- a/src/hyperjump/uri-schemes/file-scheme-plugin.js +++ b/src/hyperjump/uri-schemes/file-scheme-plugin.js @@ -6,7 +6,7 @@ import { Readable } from "node:stream"; /** * @import { Hyperjump } from "../index.js" - * @import { UriSchemePlugin } from "./uri-scheme-plugin.d.ts" + * @import { UriSchemePlugin } from "./uri-scheme-plugin.js" */ diff --git a/src/hyperjump/uri-schemes/http-scheme-plugin.js b/src/hyperjump/uri-schemes/http-scheme-plugin.js index 57b6ff6..b5b0c40 100644 --- a/src/hyperjump/uri-schemes/http-scheme-plugin.js +++ b/src/hyperjump/uri-schemes/http-scheme-plugin.js @@ -1,6 +1,6 @@ /** * @import { Hyperjump } from "../index.js" - * @import { UriSchemePlugin } from "./uri-scheme-plugin.d.ts" + * @import { UriSchemePlugin } from "./uri-scheme-plugin.js" */ diff --git a/src/jref/jref-ast.d.ts b/src/jref/jref-ast.d.ts index 47dddac..89337c0 100644 --- a/src/jref/jref-ast.d.ts +++ b/src/jref/jref-ast.d.ts @@ -2,6 +2,7 @@ import type { Data, Position } from "unist"; import { JsonArrayNode, JsonBooleanNode, + JsonCompatible, JsonNullNode, JsonNumberNode, JsonObjectNode, @@ -26,6 +27,8 @@ export type JrefNode = JsonObjectNode | JsonNullNode | JrefReferenceNode; +export type JrefCompatible = JsonCompatible | JrefReferenceNode; + export type JrefDocumentNode = { type: "jref-document"; children: JrefNode[]; diff --git a/src/jref/jref-parse.js b/src/jref/jref-parse.js index 7fef3dd..efc4db3 100644 --- a/src/jref/jref-parse.js +++ b/src/jref/jref-parse.js @@ -6,7 +6,7 @@ import { fromJref } from "./jref-util.js"; * @import { Plugin } from "unified" * @import { VFile } from "vfile" * @import { Options } from "vfile-message" - * @import { JrefDocumentNode } from "./jref-ast.d.ts" + * @import { JrefDocumentNode } from "./jref-ast.js" */ @@ -16,20 +16,12 @@ export function jrefParse() { this.parser = function (document, file) { try { const uri = pathToFileURL(file.path).toString(); - /** @type JrefDocumentNode */ - const jrefDocument = { + return { type: "jref-document", - children: [], + children: [fromJref(document, uri)], uri: uri, fragmentKind: "json-pointer" }; - - const node = fromJref(document, uri); - if (node) { - jrefDocument.children.push(node); - } - - return jrefDocument; } catch (error) { if (error instanceof VFileMessage) { return file.fail(error.message, /** @type Options */ (error)); diff --git a/src/jref/jref-stringify.js b/src/jref/jref-stringify.js index b7bc39c..102ef36 100644 --- a/src/jref/jref-stringify.js +++ b/src/jref/jref-stringify.js @@ -3,7 +3,7 @@ import { toJref } from "./jref-util.js"; /** * @import { Plugin } from "unified" * @import { Node } from "unist" - * @import { JrefDocumentNode } from "./jref-ast.d.ts" + * @import { JrefDocumentNode } from "./jref-ast.js" * @import { Replacer } from "./jref-util.js" */ diff --git a/src/jref/jref-util.js b/src/jref/jref-util.js index 60cb05b..658d520 100644 --- a/src/jref/jref-util.js +++ b/src/jref/jref-util.js @@ -7,42 +7,39 @@ import { } from "../json/jsonast-util.js"; /** - * @import { JsonObjectNode } from "../json/jsonast.d.ts" - * @import { JrefNode } from "./jref-ast.d.ts" + * @import { JsonObjectNode } from "../json/jsonast.js" + * @import { JrefCompatible, JrefNode } from "./jref-ast.js" */ /** - * @template [A = JrefNode] - * @typedef {(node: JrefNode, key?: string) => A | undefined} Reviver + * @template {JrefNode | undefined} A + * @typedef {(node: JrefCompatible>, key?: string) => A} Reviver */ /** @type Reviver */ const defaultReviver = (value) => value; -/** @type (jref: string, uri: string, reviver?: Reviver) => JrefNode | undefined */ +/** @type (jref: string, uri: string, reviver?: Reviver) => A */ export const fromJref = (jref, uri, reviver = defaultReviver) => { return fromJson(jref, (node, key) => { - /** @type JrefNode */ - let jrefNode = node; - if (node.jsonType === "object") { const href = isReference(node); if (href) { - jrefNode = { + return reviver({ type: "jref-reference", value: resolveIri(href, uri), documentUri: uri, position: node.position - }; + }, key); } } - return reviver(jrefNode, key); + return reviver(node, key); }); }; -/** @type (node: JsonObjectNode) => string | undefined */ +/** @type (node: JsonObjectNode) => string | undefined */ const isReference = (objectNode) => { for (const propertyNode of objectNode.children) { if (propertyNode.children[0].value === "$ref") { @@ -55,16 +52,16 @@ const isReference = (objectNode) => { }; /** - * @typedef {(key: string | undefined, value: JrefNode) => JrefNode} Replacer + * @typedef {(value: JrefNode, key?: string) => JrefNode} Replacer */ /** @type Replacer */ -const defaultReplacer = (_key, node) => node; +const defaultReplacer = (node) => node; /** @type (node: JrefNode, uri: string, replacer?: Replacer, space?: string) => string */ -export const toJref = (node, uri, replacer = defaultReplacer, space = " ") => { - return toJson(node, (key, node) => { - node = replacer.call(this, key, node); +export const toJref = (node, uri, replacer = defaultReplacer, space = "") => { + return toJson(node, (node, key) => { + node = replacer(node, key); if (node.type === "jref-reference") { /** @type JsonObjectNode */ diff --git a/src/jref/jref-util.test.js b/src/jref/jref-util.test.js new file mode 100644 index 0000000..fe5a0fa --- /dev/null +++ b/src/jref/jref-util.test.js @@ -0,0 +1,595 @@ +import { describe, test, expect } from "vitest"; +import { fromJref, toJref } from "./jref-util.js"; +import { resolveIri } from "@hyperjump/uri"; + +/** + * @import { JrefNode } from "../jref/jref-ast.js" + */ + + +describe("JRef", () => { + const testContext = "https://test.hyperjump.com/"; + + describe("parse", () => { + describe("scalars", () => { + test("null", () => { + const subject = fromJref(`null`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "null", + value: null, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 5, offset: 4 } + } + }); + }); + + test("true", () => { + const subject = fromJref(`true`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "boolean", + value: true, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 5, offset: 4 } + } + }); + }); + + test("false", () => { + const subject = fromJref(`false`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "boolean", + value: false, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 6, offset: 5 } + } + }); + }); + + test("integers", () => { + const subject = fromJref(`1`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "number", + value: 1, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 2, offset: 1 } + } + }); + }); + + test("real numbers", () => { + const subject = fromJref(`1.34`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "number", + value: 1.34, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 5, offset: 4 } + } + }); + }); + + test("negative numbers", () => { + const subject = fromJref(`-1.34`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "number", + value: -1.34, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 6, offset: 5 } + } + }); + }); + + test("exponential numbers", () => { + const subject = fromJref(`-1e34`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "number", + value: -1e34, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 6, offset: 5 } + } + }); + }); + + test("reference", () => { + const subject = fromJref(`{ "$ref": "./foo" }`, testContext); + expect(subject).to.eql({ + type: "jref-reference", + value: resolveIri("./foo", testContext), + documentUri: testContext, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 20, offset: 19 } + } + }); + }); + }); + + describe("array", () => { + test("empty", () => { + const subject = fromJref(`[]`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "array", + children: [], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 3, offset: 2 } + } + }); + }); + + test("non-empty", () => { + const subject = fromJref(`[ + "foo" +]`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "array", + children: [ + { + type: "json", + jsonType: "string", + value: "foo", + position: { + start: { line: 2, column: 3, offset: 4 }, + end: { line: 2, column: 8, offset: 9 } + } + } + ], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 3, column: 2, offset: 11 } + } + }); + }); + + test("reference", () => { + const subject = fromJref(`[ + { "$ref": "./foo" } +]`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "array", + children: [ + { + type: "jref-reference", + value: resolveIri("./foo", testContext), + documentUri: testContext, + position: { + start: { line: 2, column: 3, offset: 4 }, + end: { line: 2, column: 22, offset: 23 } + } + } + ], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 3, column: 2, offset: 25 } + } + }); + }); + }); + + describe("object", () => { + test("empty", () => { + const subject = fromJref(`{}`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "object", + children: [], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 3, offset: 2 } + } + }); + }); + + test("non-empty", () => { + const subject = fromJref(`{ + "foo": 42 +}`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "object", + children: [ + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "foo", + position: { + start: { line: 2, column: 3, offset: 4 }, + end: { line: 2, column: 8, offset: 9 } + } + }, + { + type: "json", + jsonType: "number", + value: 42, + position: { + start: { line: 2, column: 10, offset: 11 }, + end: { line: 2, column: 12, offset: 13 } + } + } + ] + } + ], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 3, column: 2, offset: 15 } + } + }); + }); + + test("reference", () => { + const subject = fromJref(`{ + "foo": { "$ref": "./foo" } +}`, testContext); + expect(subject).to.eql({ + type: "json", + jsonType: "object", + children: [ + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "foo", + position: { + start: { line: 2, column: 3, offset: 4 }, + end: { line: 2, column: 8, offset: 9 } + } + }, + { + type: "jref-reference", + value: resolveIri("./foo", testContext), + documentUri: testContext, + position: { + start: { line: 2, column: 10, offset: 11 }, + end: { line: 2, column: 29, offset: 30 } + } + } + ] + } + ], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 3, column: 2, offset: 32 } + } + }); + }); + }); + + describe("reviver", () => { + test("convert properties that start with 'i' to integers", () => { + const subject = fromJref(`{ "foo": 42, "iBar": "42", "baz": { "$ref": "./foo" } }`, testContext, (node, key) => { + if (key?.startsWith("i") && node.type === "json" && node.jsonType === "string") { + return { + ...node, + jsonType: "number", + value: parseInt(node.value, 10) + }; + } else { + return node; + } + }); + expect(toJref(subject, testContext)).to.equal(`{"foo":42,"iBar":42,"baz":{"$ref":"foo"}}`); + }); + }); + }); + + describe("stringify", () => { + describe("scalars", () => { + test("null", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "null", + value: null + }; + const subject = toJref(node, testContext); + expect(subject).to.equal(`null`); + }); + + test("boolean", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "boolean", + value: true + }; + const subject = toJref(node, testContext); + expect(subject).to.equal(`true`); + }); + + test("number", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "number", + value: 42 + }; + const subject = toJref(node, testContext); + expect(subject).to.equal(`42`); + }); + + test("reference", () => { + /** @type JrefNode */ + const node = { + type: "jref-reference", + value: resolveIri("./foo", testContext), + documentUri: testContext + }; + const subject = toJref(node, testContext); + expect(subject).to.equal(`{"$ref":"foo"}`); + }); + }); + + describe("array", () => { + test("empty", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "array", + children: [] + }; + const subject = toJref(node, testContext); + expect(subject).to.eql(`[]`); + }); + + test("non-empty", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "array", + children: [ + { + type: "json", + jsonType: "string", + value: "foo" + }, + { + type: "json", + jsonType: "number", + value: 42 + } + ] + }; + const subject = toJref(node, testContext); + expect(subject).to.eql(`["foo",42]`); + }); + + test("reference", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "array", + children: [ + { + type: "json", + jsonType: "string", + value: "foo" + }, + { + type: "jref-reference", + value: resolveIri("./foo", testContext), + documentUri: testContext + }, + { + type: "json", + jsonType: "number", + value: 42 + } + ] + }; + const subject = toJref(node, testContext); + expect(subject).to.be.equal(`["foo",{"$ref":"foo"},42]`); + }); + }); + + describe("object", () => { + test("empty", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "object", + children: [] + }; + const subject = toJref(node, testContext); + expect(subject).to.eql(`{}`); + }); + + test("non-empty", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "object", + children: [ + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "foo" + }, + { + type: "json", + jsonType: "number", + value: 42 + } + ] + } + ] + }; + const subject = toJref(node, testContext); + expect(subject).to.eql(`{"foo":42}`); + }); + + test("reference", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "object", + children: [ + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "foo" + }, + { + type: "json", + jsonType: "number", + value: 42 + } + ] + }, + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "bar" + }, + { + type: "jref-reference", + value: resolveIri("./foo", testContext), + documentUri: testContext + } + ] + } + ] + }; + const subject = toJref(node, testContext); + expect(subject).to.equal(`{"foo":42,"bar":{"$ref":"foo"}}`); + }); + }); + + describe("replacer", () => { + test("convert properties that start with 'i' to strings", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "object", + children: [ + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "foo" + }, + { + type: "json", + jsonType: "number", + value: 42 + } + ] + }, + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "iBar" + }, + { + type: "json", + jsonType: "number", + value: 42 + } + ] + }, + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "baz" + }, + { + type: "jref-reference", + value: resolveIri("./foo", testContext), + documentUri: testContext + } + ] + } + ] + }; + const subject = toJref(node, testContext, (node, key) => { + if (key?.startsWith("i") && node.type === "json" && node.jsonType === "number") { + return { + ...node, + jsonType: "string", + value: String(node.value) + }; + } else { + return node; + } + }); + expect(subject).to.equal(`{"foo":42,"iBar":"42","baz":{"$ref":"foo"}}`); + }); + }); + + describe("space", () => { + test("pretty print", () => { + /** @type JrefNode */ + const node = { + type: "json", + jsonType: "object", + children: [ + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "foo" + }, + { + type: "json", + jsonType: "number", + value: 42 + } + ] + }, + { + type: "json-property", + children: [ + { + type: "json-property-name", + value: "bar" + }, + { + type: "jref-reference", + value: resolveIri("./foo", testContext), + documentUri: testContext + } + ] + } + ] + }; + const subject = toJref(node, testContext, undefined, " "); + expect(subject).to.equal(`{ + "foo": 42, + "bar": { + "$ref": "foo" + } +}`); + }); + }); + }); +}); diff --git a/src/jref/jref.js b/src/jref/jref.js index 7598672..c9f167f 100644 --- a/src/jref/jref.js +++ b/src/jref/jref.js @@ -4,7 +4,7 @@ import { jrefStringify } from "./jref-stringify.js"; /** * @import { Processor } from "unified" - * @import { JrefDocumentNode } from "./jref-ast.d.ts" + * @import { JrefDocumentNode } from "./jref-ast.js" */ diff --git a/src/json/json-lexer.js b/src/json/json-lexer.js index 3925af9..9a7caa3 100644 --- a/src/json/json-lexer.js +++ b/src/json/json-lexer.js @@ -72,7 +72,7 @@ export class JsonLexer { }(this.#lexer)); } - /** @type (lexer: JsonLexer) => JsonToken */ + /** @type () => JsonToken */ nextToken() { const result = this.#iterator.next(); if (result.done) { diff --git a/src/json/jsonast-util.js b/src/json/jsonast-util.js index 7556400..d9e6d75 100644 --- a/src/json/jsonast-util.js +++ b/src/json/jsonast-util.js @@ -14,23 +14,23 @@ import { JsonLexer } from "./json-lexer.js"; * JsonPropertyNameNode, * JsonPropertyNode, * JsonStringNode - * } from "./jsonast.d.ts" + * } from "./jsonast.js" */ /** - * @template [A = JsonNode] - * @typedef {(node: JsonCompatible, key: string | undefined) => A | undefined} Reviver + * @template A + * @typedef {(node: JsonCompatible>, key?: string) => A} Reviver */ /** @type Reviver */ const defaultReviver = (value) => value; -/** @type (json: string, reviver?: Reviver) => A | undefined */ +/** @type (json: string, reviver?: Reviver) => A */ export const fromJson = (json, reviver = defaultReviver) => { const lexer = new JsonLexer(json); - const token = lexer.nextToken(lexer); + const token = lexer.nextToken(); const jsonValue = parseValue(token, lexer, undefined, reviver); lexer.done(); @@ -38,7 +38,7 @@ export const fromJson = (json, reviver = defaultReviver) => { return jsonValue; }; -/** @type (token: JsonToken, lexer: JsonLexer, key: string | undefined, reviver: Reviver) => A | undefined */ +/** @type (token: JsonToken, lexer: JsonLexer, key: string | undefined, reviver: Reviver) => A */ const parseValue = (token, lexer, key, reviver) => { let node; @@ -72,7 +72,7 @@ const parseScalar = (token) => { }; }; -/** @type (token: JsonToken, lexer: JsonLexer, key: string | undefined, reviver: Reviver) => JsonPropertyNode | undefined */ +/** @type (token: JsonToken, lexer: JsonLexer, key: string | undefined, reviver: Reviver) => JsonPropertyNode | undefined */ const parseProperty = (token, lexer, _key, reviver) => { if (token.type !== "string") { throw lexer.syntaxError("Expected a propertry", token); @@ -85,11 +85,11 @@ const parseProperty = (token, lexer, _key, reviver) => { position: tokenPosition(token) }; - if (lexer.nextToken(lexer).type !== ":") { + if (lexer.nextToken().type !== ":") { throw lexer.syntaxError("Expected :", token); } - const valueNode = parseValue(lexer.nextToken(lexer), lexer, keyNode.value, reviver); + const valueNode = parseValue(lexer.nextToken(), lexer, keyNode.value, reviver); if (!valueNode) { return; } @@ -106,11 +106,11 @@ const parseProperty = (token, lexer, _key, reviver) => { */ /** - * @type , B>(parseChild: (token: JsonToken, lexer: JsonLexer, key: string | undefined, reviver: Reviver) => B, endToken: string) => (lexer: JsonLexer, node: A, reviver: Reviver) => A + * @type , B>(parseChild: (token: JsonToken, lexer: JsonLexer, key: string | undefined, reviver: Reviver) => B, endToken: string) => (lexer: JsonLexer, node: A, reviver: Reviver) => NonNullable */ const parseCommaSeparated = (parseChild, endToken) => (lexer, node, reviver) => { for (let index = 0; true; index++) { - let token = lexer.nextToken(lexer); + let token = lexer.nextToken(); if (token.type === endToken) { /** @type Position */ (node.position).end = tokenPosition(token).end; @@ -119,7 +119,7 @@ const parseCommaSeparated = (parseChild, endToken) => (lexer, node, reviver) => if (index > 0) { if (token.type === ",") { - token = lexer.nextToken(lexer); + token = lexer.nextToken(); } else { throw lexer.syntaxError(`Expected , or ${endToken}`, token); } @@ -132,7 +132,7 @@ const parseCommaSeparated = (parseChild, endToken) => (lexer, node, reviver) => } }; -/** @type (openToken: JsonToken, lexer: JsonLexer, reviver: Reviver) => JsonArrayNode */ +/** @type (openToken: JsonToken, lexer: JsonLexer, reviver: Reviver) => JsonArrayNode> */ const parseArray = (openToken, lexer, reviver) => { return parseItems(lexer, { type: "json", @@ -142,10 +142,10 @@ const parseArray = (openToken, lexer, reviver) => { }, reviver); }; -/** @type (lexer: JsonLexer, node: JsonArrayNode, reviver: Reviver) => JsonArrayNode */ +/** @type (lexer: JsonLexer, node: JsonArrayNode>, reviver: Reviver) => JsonArrayNode> */ const parseItems = parseCommaSeparated(parseValue, "]"); -/** @type (openToken: JsonToken, lexer: JsonLexer, reviver: Reviver) => JsonObjectNode */ +/** @type (openToken: JsonToken, lexer: JsonLexer, reviver: Reviver) => JsonObjectNode> */ const parseObject = (openToken, lexer, reviver) => { return parseProperties(lexer, { type: "json", @@ -155,7 +155,7 @@ const parseObject = (openToken, lexer, reviver) => { }, reviver); }; -/** @type (lexer: JsonLexer, node: JsonObjectNode, reviver: Reviver) => JsonObjectNode */ +/** @type (lexer: JsonLexer, node: JsonObjectNode>, reviver: Reviver) => JsonObjectNode> */ const parseProperties = parseCommaSeparated(parseProperty, "}"); /** @type (startToken: JsonToken, endToken?: JsonToken) => Position */ @@ -172,34 +172,35 @@ const tokenPosition = (startToken, endToken) => { }, end: { line: endToken.line, - column: endToken.col, - offset: endToken.offset + column: endToken.col + endToken.text.length, + offset: endToken.offset + endToken.text.length } }; }; /** * @template [A = JsonNode] - * @typedef {(key: string | undefined, value: A) => JsonCompatible} Replacer + * @typedef {(node: A, key?: string) => JsonCompatible} Replacer */ /** @type Replacer */ -const defaultReplacer = (_key, node) => node; // eslint-disable-line @typescript-eslint/no-unsafe-return +const defaultReplacer = (node) => node; // eslint-disable-line @typescript-eslint/no-unsafe-return /** @type (node: A, replacer?: Replacer, space?: string) => string */ -export const toJson = (node, replacer = defaultReplacer, space = " ") => { - const replacedNode = replacer.call(undefined, undefined, node); +export const toJson = (node, replacer = defaultReplacer, space = "") => { + const replacedNode = replacer(node); return stringifyValue(replacedNode, replacer, space, 1); }; /** @type (node: JsonCompatible, replacer: Replacer, space: string, depth: number) => string */ const stringifyValue = (node, replacer, space, depth) => { - if (node.jsonType === "array") { - return stringifyArray(node, replacer, space, depth); - } else if (node.jsonType === "object") { - return stringifyObject(node, replacer, space, depth); - } else { - return JSON.stringify(node.value); + switch (node.jsonType) { + case "array": + return stringifyArray(node, replacer, space, depth); + case "object": + return stringifyObject(node, replacer, space, depth); + default: + return JSON.stringify(node.value); } }; @@ -213,7 +214,7 @@ const stringifyArray = (node, replacer, space, depth) => { let result = "[" + padding + space; for (let index = 0; index < node.children.length; index++) { - const itemNode = replacer.call(node, `${index}`, node.children[index]); + const itemNode = replacer(node.children[index], `${index}`); const stringifiedValue = stringifyValue(itemNode, replacer, space, depth + 1); result += stringifiedValue ?? "null"; if (index + 1 < node.children.length) { @@ -236,7 +237,7 @@ const stringifyObject = (node, replacer, space, depth) => { for (let index = 0; index < node.children.length; index++) { const propertyNode = node.children[index]; const [keyNode, valueNode] = propertyNode.children; - const replacedValueNode = replacer.call(node, keyNode.value, valueNode); + const replacedValueNode = replacer(valueNode, keyNode.value); const stringifiedValue = stringifyValue(replacedValueNode, replacer, space, depth + 1); if (stringifiedValue !== undefined) { result += JSON.stringify(keyNode.value) + ":" + colonSpacing + stringifiedValue; diff --git a/src/json/jsonast-util.test.js b/src/json/jsonast-util.test.js index 0eec728..be59334 100644 --- a/src/json/jsonast-util.test.js +++ b/src/json/jsonast-util.test.js @@ -18,14 +18,15 @@ describe("jsonast-util", async () => { if (entry.name.startsWith("y_")) { test(`${entry.name.substring(2)} without spaces`, async () => { const processed = await rejson() - .use(rejsonStringify, { space: "" }) .process(json); const expected = JSON.stringify(JSON.parse(json)) + "\n"; expect(processed.toString()).to.eql(expected); }); test(`${entry.name.substring(2)} with spaces`, async () => { - const processed = await rejson().process(json); + const processed = await rejson() + .use(rejsonStringify, { space: " " }) + .process(json); const expected = JSON.stringify(JSON.parse(json), null, " ") + "\n"; expect(processed.toString()).to.eql(expected); }); diff --git a/src/json/rejson-parse.js b/src/json/rejson-parse.js index 04c9c14..16b2b78 100644 --- a/src/json/rejson-parse.js +++ b/src/json/rejson-parse.js @@ -5,7 +5,7 @@ import { fromJson } from "./jsonast-util.js"; * @import { Plugin } from "unified" * @import { VFile } from "vfile" * @import { Options } from "vfile-message" - * @import { JsonDocumentNode } from "./jsonast.d.ts" + * @import { JsonDocumentNode } from "./jsonast.js" */ @@ -14,18 +14,10 @@ export function rejsonParse() { /** @type (document: string, file: VFile) => JsonDocumentNode */ this.parser = function (document, file) { try { - /** @type JsonDocumentNode */ - const jsonDocument = { + return { type: "json-document", - children: [] + children: [fromJson(document)] }; - - const node = fromJson(document); - if (node) { - jsonDocument.children.push(node); - } - - return jsonDocument; } catch (error) { if (error instanceof VFileMessage) { return file.fail(error.message, /** @type Options */ (error)); diff --git a/src/json/rejson-stringify.js b/src/json/rejson-stringify.js index 12aef39..12f9654 100644 --- a/src/json/rejson-stringify.js +++ b/src/json/rejson-stringify.js @@ -3,7 +3,7 @@ import { toJson } from "./jsonast-util.js"; /** * @import { Plugin } from "unified" * @import { Node } from "unist" - * @import { JsonDocumentNode } from "./jsonast.d.ts" + * @import { JsonDocumentNode } from "./jsonast.js" * @import { Replacer } from "./jsonast-util.js" */ diff --git a/src/json/rejson.js b/src/json/rejson.js index 25b9a65..0c5e5ab 100644 --- a/src/json/rejson.js +++ b/src/json/rejson.js @@ -4,7 +4,7 @@ import { rejsonStringify } from "./rejson-stringify.js"; /** * @import { Processor } from "unified" - * @import { JsonDocumentNode } from "./jsonast.d.ts" + * @import { JsonDocumentNode } from "./jsonast.js" */