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

⬆️ Add simple upgrade/downgrade package for MyST AST #1802

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sour-spiders-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"myst-compat": patch
---

Add myst-compat package
4 changes: 4 additions & 0 deletions packages/myst-compat/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['curvenote'],
};
3 changes: 3 additions & 0 deletions packages/myst-compat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# myst-compat

Utilites for up/downgrading MyST ASTs
30 changes: 30 additions & 0 deletions packages/myst-compat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "myst-compat",
"sideEffects": false,
"version": "0.0.0",
"type": "module",
"exports": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
"lint": "eslint \"src/**/!(*.spec).ts\" -c ./.eslintrc.cjs",
"lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\"",
"test": "vitest run",
"test:watch": "vitest watch",
"build:esm": "tsc",
"build": "npm-run-all -l clean -p build:esm"
},
"dependencies": {
"unist-util-select": "^4.0.3",
"unist-util-visit": "^4.1.2",
"vfile": "^5.0.0",
"vfile-message": "^3.0.0"
},
"devDependencies": {
"@jupyterlab/nbformat": "^3.5.2"
}
}
16 changes: 16 additions & 0 deletions packages/myst-compat/src/downgrade/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Parent } from 'mdast';

import { downgrade as downgrade2To1 } from './version_2_1.js';

export function downgrade(from: string, to: string, ast: Parent): void {
if (to === '1' && from === '2') {
downgrade2To1(ast);
return;
} else if (to === from) {
return;
} else {
throw new Error(`Unable to downgrade between ${from} and ${to}`);
}
}

export { downgrade2To1 };
260 changes: 260 additions & 0 deletions packages/myst-compat/src/downgrade/version_2_1.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { describe, expect, it } from 'vitest';
import { downgrade } from './version_2_1.js';
import type { Parent } from 'mdast';

const SIMPLE_AST: Parent = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters for now (as in we can get this in asap), however, I think this would be easier to manage with separate test dir with json files and then a global test runner.

type: 'root',
children: [
{
// @ts-expect-error: unknown type
type: 'block',
children: [
{
// @ts-expect-error: invalid child type
type: 'paragraph',
children: [
{
type: 'text',
value: 'This is an ',
},
{
type: 'emphasis',
children: [
{
type: 'text',
value: 'interesting',
},
],
},
{
type: 'text',
value: ' file. See ',
},
{
type: 'link',
url: '#other',
children: [],
// @ts-expect-error: unknown field
urlSource: '#other',
},
{
type: 'text',
value: ' for more.',
},
],
},
{
// @ts-expect-error: invalid child type
type: 'code',
lang: 'python',
value: 'Some code',
},
],
},
],
};

const SIMPLE_V2_AST_WITH_FOOTNOTE: Parent = {
type: 'root',
children: [
{
// @ts-expect-error: unknown type
type: 'block',
children: [
{
// @ts-expect-error: unknown type
type: 'paragraph',
children: [
{
type: 'text',
value: 'See the footnote',
},
{
type: 'footnoteReference',
identifier: '1',
label: '1',
// @ts-expect-error: unknown type
enumerator: '1',
},
],
},
{
// @ts-expect-error: unknown type
type: 'footnoteDefinition',
identifier: '1',
label: '1',
children: [
{
// @ts-expect-error: unknown type
type: 'paragraph',
children: [
{
type: 'text',
value: 'A footnote',
},
],
},
],
enumerator: '1',
},
],
},
],
};

const SIMPLE_V1_AST_WITH_FOOTNOTE: Parent = {
type: 'root',
children: [
{
// @ts-expect-error: unknown type
type: 'block',
children: [
{
// @ts-expect-error: unknown type
type: 'paragraph',
children: [
{
type: 'text',
value: 'See the footnote',
},
{
type: 'footnoteReference',
identifier: '1',
label: '1',
// @ts-expect-error: unknown type
number: 1,
},
],
},
{
// @ts-expect-error: unknown type
type: 'footnoteDefinition',
identifier: '1',
label: '1',
children: [
{
// @ts-expect-error: unknown type
type: 'paragraph',
children: [
{
type: 'text',
value: 'A footnote',
},
],
},
],
number: 1,
},
],
},
],
};
const SIMPLE_V2_AST_WITH_INVALID_FOOTNOTE: Parent = {
type: 'root',
children: [
{
// @ts-expect-error: unknown type
type: 'block',
children: [
{
// @ts-expect-error: unknown type
type: 'paragraph',
children: [
{
type: 'text',
value: 'See the footnote',
},
{
type: 'footnoteReference',
identifier: '1',
label: '1',
// @ts-expect-error: unknown type
enumerator: '%s.1',
},
],
},
{
// @ts-expect-error: unknown type
type: 'footnoteDefinition',
identifier: '1',
label: '1',
children: [
{
// @ts-expect-error: unknown type
type: 'paragraph',
children: [
{
type: 'text',
value: 'A footnote',
},
],
},
],
enumerator: '%s.1',
},
],
},
],
};

const SIMPLE_V1_AST_WITH_INVALID_FOOTNOTE: Parent = {
type: 'root',
children: [
{
// @ts-expect-error: unknown type
type: 'block',
children: [
{
// @ts-expect-error: unknown type
type: 'paragraph',
children: [
{
type: 'text',
value: 'See the footnote',
},
{
type: 'footnoteReference',
identifier: '1',
label: '1',
},
],
},
{
// @ts-expect-error: unknown type
type: 'footnoteDefinition',
identifier: '1',
label: '1',
children: [
{
// @ts-expect-error: unknown type
type: 'paragraph',
children: [
{
type: 'text',
value: 'A footnote',
},
],
},
],
},
],
},
],
};
describe('downgrade 2->1', () => {
it('leaves a simple AST unchanged', () => {
const ast = structuredClone(SIMPLE_AST);
downgrade(ast);
expect(ast).toStrictEqual(SIMPLE_AST);
});
it('downgrades a v2 AST with footnotes', () => {
const ast = structuredClone(SIMPLE_V2_AST_WITH_FOOTNOTE);
downgrade(ast);
expect(ast).toStrictEqual(SIMPLE_V1_AST_WITH_FOOTNOTE);
});
it('downgrades a v2 AST with invalid footnotes', () => {
const ast = structuredClone(SIMPLE_V2_AST_WITH_INVALID_FOOTNOTE);
downgrade(ast);
expect(ast).toStrictEqual(SIMPLE_V1_AST_WITH_INVALID_FOOTNOTE);
});
});
60 changes: 60 additions & 0 deletions packages/myst-compat/src/downgrade/version_2_1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Parent } from 'mdast';
import type {
FootnoteDefinition as FootnoteDefinition2,
FootnoteReference as FootnoteReference2,
} from '../types/v2.js';
import type {
FootnoteDefinition as FootnoteDefinition1,
FootnoteReference as FootnoteReference1,
} from '../types/v1.js';
import { visit, CONTINUE, SKIP } from 'unist-util-visit';

function maybeParseInt(value: string | undefined): number | undefined {
if (value === undefined) {
return undefined;
}
const result = parseInt(value);
if (String(result) === value) {
return result;
} else {
return undefined;
}
}

export function downgrade(ast: Parent) {
// Walk over footnote nodes
visit(
ast as any,
['footnoteDefinition', 'footnoteReference'],
(
node: FootnoteDefinition2 | FootnoteReference2,
index: number | null,
parent: Parent | null,
) => {
if (parent) {
switch (node.type) {
case 'footnoteDefinition': {
const { enumerator, ...rest } = node;
const nextNode: FootnoteDefinition1 = rest;
const maybeNumber = maybeParseInt(enumerator);
if (maybeNumber !== undefined) {
nextNode.number = maybeNumber;
}
parent.children[index!] = nextNode as any;
return CONTINUE;
}
case 'footnoteReference': {
const { enumerator, ...rest } = node;
const nextNode: FootnoteReference1 = rest;
const maybeNumber = maybeParseInt(enumerator);
if (maybeNumber !== undefined) {
nextNode.number = maybeNumber;
}
parent.children[index!] = nextNode as any;
return SKIP;
}
}
}
},
);
}
Loading
Loading