Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert core/validation specs to non-IETF markdown #1425

Merged
merged 11 commits into from
Aug 25, 2023
35 changes: 35 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"root": true,
"env": {
"es6": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"extends": [
"eslint:recommended",
"plugin:import/recommended"
],
"plugins": ["import"],
"rules": {
"array-bracket-spacing": "error",
"arrow-parens": "error",
"comma-dangle": "error",
"dot-location": ["error", "property"],
"eol-last": "error",
"generator-star-spacing": ["error", { "before": false, "after": true }],
"indent": ["error", 2, { "ignoreComments": true, "SwitchCase": 1 }],
"linebreak-style": "error",
"no-console": ["error", { "allow": ["error"] }],
"no-trailing-spaces": "error",
"no-unused-vars": ["error", { "argsIgnorePattern": "_.*" }],
"prefer-const": ["error", { "destructuring": "all" }],
"semi": "error",
"quotes": ["error", "double", { "allowTemplateLiterals": true }]
},
"settings": {
"import/resolver": "node"
}
}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ relative-json-pointer.txt

# For the Python enviornment
.venv

# For the node-based build tools
node_modules/

.env
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ XML2RFC ?= xml2rfc
VENV ?= .venv

OUT = \
jsonschema-core.html jsonschema-core.txt \
jsonschema-validation.html jsonschema-validation.txt \
relative-json-pointer.html relative-json-pointer.txt

all: $(VENV) $(OUT)
Expand Down
108 changes: 85 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,91 @@ For the current status of issues and pull requests, please see the following lab

Labels are assigned based on [Sensible Github Labels](https://github.com/Relequestual/sensible-github-labels).

## Contents

* Makefile - scripts to build the Internet-Draft txt/html
* _Internet-Draft sources_
* jsonschema-core.xml - source for JSON Schema's "core" I-D
* jsonschema-validation.xml - source for the validation vocabulary I-D
* relative-json-pointer.xml - source for the Relative JSON Pointer I-D
* _meta-schemas and recommended output formats_
* schema.json - JSON Schema "core" and Validation meta-schema

The Makefile will create the necessary Python venv for you as part of the regular make target.

`make clean` will remove all output including the venv. To clean just the spec output and
keep the venv, use `make spec-clean`.

If you want to run `xml2rfc` manually after running make for the first time, you will
need to activate the virtual environment:

```sh
source .venv/bin/activate
```

The version of "xml2rfc" that this project uses is updated by modifying `requirements.in` and running `pip-compile requirements.in`.
## Authoring and Building

### Specification
To build the spec files to HTML from the Markdown sources, run `npm run build`.
You can also build each individually with `npm run build-core` and `npm run
build-validation`.

The spec is built using [Remark](https://remark.js.org/), a markdown engine with
good support for plugins and lots of existing plugins we can use.

#### Plugins
The following is a not-necessarily-complete list of configured plugins and the
features they make available to you.

- [remark-lint](https://github.com/remarkjs/remark-lint) -- Enforce markdown
styles guide.
- [remark-validate-links](https://github.com/remarkjs/remark-validate-links) --
Check for broken links.
- [remark-gfm](https://github.com/remarkjs/remark-gfm) -- Adds support for
Github Flavored Markdown specific markdown features such as autolink literals,
footnotes, strikethrough, tables, and tasklists.
- [remark-heading-id](https://github.com/imcuttle/remark-heading-id) -- Adds
support for `{#my-anchor}` syntax to add an `id` to an element so it can be
referenced using URI fragment syntax.
- [remark-headings](/json-schema-org/json-schema-spec/blob/main/remark-headings.js)
-- A collection of enhancements for headings.
- Adds hierarchical section numbers to headings.
- Use the `[Appendix]` prefix on headings that should be numbered as an
appendix.
- Adds id anchors to headers that don't have one
- Example: `#section-2-13`
- Example: `#appendix-a`
- Makes the heading a link utilizing its anchor
- [remark-reference-links](/json-schema-org/json-schema-spec/blob/main/remark-reference-link.js)
-- Adds new syntax for referencing a section of the spec using the section
number as the link text.
- Example:
```markdown
## Foo {#foo}

## Bar
This is covered in {{foo}} // --> Renders to "This is covered in [Section 2.3](#foo)"
- Link text will use "Section" or "Appendix" as needed
```
- [remark-table-of-contents](/json-schema-org/json-schema-spec/blob/main/remark-table-of-contents.js)
-- Adds a table of contents in a section with a header called "Table of
Contents".
- [remark-code-titles](/json-schema-org/json-schema-spec/blob/main/remark-code-titles.js)
-- Add titles to code blocks
- Example:
```markdown
\`\`\`jsonschema "My Fun Title"
{ "type": "string" }
\`\`\`
```
- The languages `jsonschema` and `json` have special styling
- The title will be parsed as a JSON string, but you have to double escape
escaped characters. So, to get `My "quoted" title`, you would need to be
`"My \\\\"quoted\\\\" title"`.
- [remark-torchlight](https://github.com/torchlight-api/remark-torchlight) --
Syntax highlighting and more using https://torchlight.dev. Features include
line numbers and line highlighting.
- [remark-flexible-containers](https://github.com/ipikuka/remark-flexible-containers)
-- Add a callout box using the following syntax. Supported container types are
`warning`, `note`, and `experimental`.

```
::: {type} {title}
{content}
:::
```

### Internet-Drafts
To build components that are being maintained as IETF Internet-Drafts, run
`make`. The Makefile will create the necessary Python venv for you as part of
the regular make target.

`make clean` will remove all output including the venv. To clean just the spec
output and keep the venv, use `make spec-clean`.

If you want to run `xml2rfc` manually after running make for the first time, you
will need to activate the virtual environment: `source .venv/bin/activate`.

The version of "xml2rfc" that this project uses is updated by modifying
`requirements.in` and running `pip-compile requirements.in`.

Descriptions of the xml2rfc, I-D documents, and RFC processes:

Expand Down
152 changes: 152 additions & 0 deletions build/build.js

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions build/remark-code-titles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { visit } from "unist-util-visit";
import { text } from "mdast-builder";


const hexdig = `[0-9a-fA-F]`;
const char = `(?:\\\\["\\/\\\\brfnt]|\\\\u${hexdig}{4}|[^"\\\\])`;
const jsonStringPattern = new RegExp(`^"${char}*"`);

const remarkNumberHeadings = () => (tree) => {
visit(tree, "code", (codeNode, index, parent) => {
// Support title without a language
if (codeNode.lang && codeNode.lang[0] === "\"") {
codeNode.meta = `${codeNode.lang} ${codeNode.meta}`;
codeNode.lang = null;
}

let title = "";
const titleClasses = ["remark-code-title"];

const language = codeNode.lang ?? "";
if (language.toLowerCase() === "jsonschema") {
codeNode.lang = "json";
title = "JSON Schema";
titleClasses.push("code-title-jsonschema");
} else if (language.toLowerCase() === "json") {
title = "JSON";
titleClasses.push("code-title-json");
} else {
titleClasses.push("code-title-unknown");
}

if ("meta" in codeNode) {
const match = jsonStringPattern.exec(codeNode.meta);
if (match) {
const customTitle = JSON.parse(match[0]);
title = title ? `${title} - ${customTitle}` : customTitle;
codeNode.meta = codeNode.meta.slice(match[0].length).trim();
}
}

const containerChildren = [];
if (title) {
const titleNode = div([text(title)], { className: titleClasses });
containerChildren.push(titleNode);
}
containerChildren.push(codeNode);

const wrappedCodeNode = div(containerChildren, { className: ["remark-code-container"] });

parent.children.splice(index, 1, wrappedCodeNode);
});
};

const div = (children, properties) => {
return {
type: "container",
children,
data: {
hName: "div",
hProperties: properties
}
};
};

export default remarkNumberHeadings;
97 changes: 97 additions & 0 deletions build/remark-headings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { visit } from "unist-util-visit";
import { link, text } from "mdast-builder";
import { findAndReplace } from "mdast-util-find-and-replace";
import { toString as nodeToString } from "mdast-util-to-string";


const defaultOptions = {
startDepth: 1,
skip: [],
appendixToken: "[Appendix]",
appendixPrefix: "Appendix"
};

const remarkNumberHeadings = (options) => (tree, file) => {
options = { ...defaultOptions, ...options };
options.skip = new RegExp(`^(${options.skip.join("|")})$`, "u");

// Auto-number headings
let sectionNumbers = [];

visit(tree, "heading", (headingNode) => {
if (headingNode.depth < options.startDepth) {
return;
}

const headingText = nodeToString(headingNode);
if (options.skip.test(headingText)) {
return;
}

if (!("data" in headingNode)) {
headingNode.data = {};
}

if (!("hProperties" in headingNode.data)) {
headingNode.data.hProperties = {};
}

if (headingText.startsWith(options.appendixToken)) {
findAndReplace(headingNode, [options.appendixToken]);

const currentIndex = typeof sectionNumbers[headingNode.depth] === "string"
? sectionNumbers[headingNode.depth]
: "@";
sectionNumbers[headingNode.depth] = String.fromCharCode(currentIndex.charCodeAt(0) + 1);
sectionNumbers = sectionNumbers.slice(0, headingNode.depth + 1);

const sectionNumber = sectionNumbers.slice(options.startDepth, headingNode.depth + 1).join(".");
headingNode.data.section = `${options.appendixPrefix} ${sectionNumber}`;

headingNode.children.splice(0, 0, text(`${headingNode.data.section}. `));
} else {
sectionNumbers[headingNode.depth] = (sectionNumbers[headingNode.depth] ?? 0) + 1;
sectionNumbers = sectionNumbers.slice(0, headingNode.depth + 1);

const sectionNumber = sectionNumbers.slice(options.startDepth, headingNode.depth + 1).join(".");
const prefix = typeof sectionNumbers[options.startDepth] === "string"
? options.appendixPrefix
: "Section";
headingNode.data.section = `${prefix} ${sectionNumber}`;

headingNode.children.splice(0, 0, text(`${sectionNumber}. `));
}

if (!("id" in headingNode.data)) {
const sectionSlug = headingNode.data?.id
?? headingNode.data.section.replaceAll(/[ .]/g, "-").toLowerCase();
headingNode.data.hProperties.id = sectionSlug;
headingNode.data.id = sectionSlug;
}
});

// Build headings data used by ./remark-reference-links.js
if (!("data" in file)) {
file.data = {};
}

file.data.headings = {};

visit(tree, "heading", (headingNode) => {
if (headingNode.data?.id) {
if (headingNode.data.id in file.data.headings) {
file.message(`Found duplicate heading id "${headingNode.data.id}"`);
}
file.data.headings[headingNode.data.id] = headingNode;
}
});

// Make heading a link
visit(tree, "heading", (headingNode) => {
if (headingNode.data?.id) {
headingNode.children = [link(`#${headingNode.data.id}`, "", headingNode.children)];
}
});
};

export default remarkNumberHeadings;
21 changes: 21 additions & 0 deletions build/remark-reference-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { text, link } from "mdast-builder";
import { toString as nodeToString } from "mdast-util-to-string";
import { findAndReplace } from "mdast-util-find-and-replace";


const referenceLink = /\{\{(?<id>.*?)\}\}/ug;

const remarkReferenceLinks = () => (tree, file) => {
findAndReplace(tree, [referenceLink, (value, id) => {
// file.data.headings comes from ./remark-headings.js
if (!(id in file.data.headings)) {
throw Error(`ReferenceLinkError: No header found with id "${id}"`);
}

const headerText = nodeToString(file.data.headings[id]);
const linkText = text(file.data.headings[id].data.section);
return link(`#${id}`, headerText, [linkText]);
}]);
};

export default remarkReferenceLinks;
Loading
Loading