Skip to content

Commit

Permalink
⚙️ Use simple validators in TOC (#1172)
Browse files Browse the repository at this point in the history
* refactor: use simple validators for TOC

* chore: add changeset

* Update packages/myst-toc/src/toc.ts

Co-authored-by: Franklin Koch <franklinwkoch@gmail.com>

* Update packages/myst-toc/src/toc.ts

Co-authored-by: Franklin Koch <franklinwkoch@gmail.com>

* refactor: apply suggestions

* chore: add changeset

* chore: drop unused dep

---------

Co-authored-by: Franklin Koch <franklinwkoch@gmail.com>
  • Loading branch information
agoose77 and fwkoch authored May 7, 2024
1 parent 9bd9ec8 commit 8463044
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 141 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-lizards-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"myst-toc": minor
---

Change type aliases for parent-entries
5 changes: 5 additions & 0 deletions .changeset/silly-bats-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"myst-toc": minor
---

Use simple-validators for validation
12 changes: 4 additions & 8 deletions packages/myst-toc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,18 @@
"url": "git+https://github.com/executablebooks/mystmd.git"
},
"scripts": {
"clean": "rimraf dist ./src/schema.json",
"clean": "rimraf dist",
"lint": "eslint \"src/**/!(*.spec).ts\" -c ./.eslintrc.cjs",
"lint:format": "npx prettier --check \"src/**/*.ts\"",
"test": "npm-run-all build:schema && vitest run",
"test": "vitest run",
"test:watch": "vitest watch",
"build:esm": "tsc",
"build:schema": "npx ts-json-schema-generator --path src/types.ts --type TOC -o ./src/schema.json",
"build": "npm-run-all -s -l clean build:schema build:esm"
"build": "npm-run-all -s -l clean build:esm"
},
"bugs": {
"url": "https://github.com/executablebooks/mystmd/issues"
},
"dependencies": {
"ajv": "^8.12.0"
},
"devDependencies": {
"ts-json-schema-generator": "^1.5.0"
"simple-validators": "^1.0.4"
}
}
224 changes: 210 additions & 14 deletions packages/myst-toc/src/toc.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,217 @@
import type { TOC } from './types.js';
import schema from './schema.json';
import _Ajv from 'ajv';
import type {
TOC,
Entry,
FileEntry,
FileParentEntry,
URLEntry,
URLParentEntry,
PatternEntry,
ParentEntry,
CommonEntry,
} from './types.js';

import type { ValidationOptions } from 'simple-validators';
import {
defined,
incrementOptions,
validateBoolean,
validateList,
validateObjectKeys,
validateObject,
validateString,
validationError,
} from 'simple-validators';

const COMMON_ENTRY_KEYS = ['title', 'hidden', 'numbering', 'id', 'part', 'class'];

function validateCommonEntry(entry: Record<string, any>, opts: ValidationOptions): CommonEntry {
const output: CommonEntry = {};
if (defined(entry.title)) {
output.title = validateString(entry.title, incrementOptions('title', opts));
}

if (defined(entry.hidden)) {
output.hidden = validateBoolean(entry.hidden, incrementOptions('hidden', opts));
}

if (defined(entry.numbering)) {
output.numbering = validateString(entry.numbering, incrementOptions('numbering', opts));
}

if (defined(entry.id)) {
output.id = validateString(entry.id, incrementOptions('id', opts));
}

if (defined(entry.part)) {
output.part = validateString(entry.part, incrementOptions('part', opts));
}

if (defined(entry.class)) {
output.class = validateString(entry.class, incrementOptions('class', opts));
}

return output;
}

export function validateFileEntry(
entry: any,
opts: ValidationOptions,
): FileEntry | FileParentEntry | undefined {
const intermediate = validateObjectKeys(
entry,
{
required: ['file'],
optional: [...COMMON_ENTRY_KEYS, 'children'],
},
opts,
);
if (!intermediate) {
return undefined;
}

const file = validateString(intermediate.file, incrementOptions('file', opts));
if (!file) {
return undefined;
}

const commonEntry = validateCommonEntry(intermediate, opts);

let output: FileEntry | FileParentEntry = { file, ...commonEntry };
if (defined(entry.children)) {
const children = validateList(
intermediate.children,
incrementOptions('children', opts),
(item, ind) => validateEntry(item, incrementOptions(`children.${ind}`, opts)),
);
output = { children, ...output };
}

return output;
}

export function validateURLEntry(
entry: any,
opts: ValidationOptions,
): URLEntry | URLParentEntry | undefined {
const intermediate = validateObjectKeys(
entry,
{
required: ['url'],
optional: [...COMMON_ENTRY_KEYS, 'children'],
},
opts,
);
if (!intermediate) {
return undefined;
}

const url = validateString(intermediate.url, incrementOptions('url', opts));
if (!url) {
return undefined;
}

const commonEntry = validateCommonEntry(intermediate, opts);

let output: URLEntry | URLParentEntry = { url, ...commonEntry };
if (defined(entry.children)) {
const children = validateList(
intermediate.children,
incrementOptions('children', opts),
(item, ind) => validateEntry(item, incrementOptions(`children.${ind}`, opts)),
);
output = { children, ...output };
}

return output;
}

export function validatePatternEntry(
entry: any,
opts: ValidationOptions,
): PatternEntry | undefined {
const intermediate = validateObjectKeys(
entry,
{
required: ['pattern'],
optional: [...COMMON_ENTRY_KEYS, 'children'],
},
opts,
);
if (!intermediate) {
return undefined;
}

const pattern = validateString(intermediate.pattern, incrementOptions('pattern', opts));
if (!pattern) {
return undefined;
}

const commonEntry = validateCommonEntry(intermediate, opts);
return { pattern, ...commonEntry };
}

export function validateParentEntry(entry: any, opts: ValidationOptions): ParentEntry | undefined {
const intermediate = validateObjectKeys(
entry,
{
required: ['title', 'children'],
optional: [...COMMON_ENTRY_KEYS],
},
opts,
);
if (!intermediate) {
return undefined;
}

const title = validateString(intermediate.title, incrementOptions('title', opts));
if (!title) {
return undefined;
}

const children = validateList(
intermediate.children,
incrementOptions('children', opts),
(item, ind) => validateEntry(item, incrementOptions(`children.${ind}`, opts)),
);

if (!children) {
return undefined;
}

const commonEntry = validateCommonEntry(intermediate, opts);

return {
children,
title,
...commonEntry,
};
}

export function validateEntry(entry: any, opts: ValidationOptions): Entry | undefined {
const intermediate = validateObject(entry, opts);
if (!intermediate) {
return undefined;
}
if (defined(intermediate.file)) {
return validateFileEntry(intermediate, opts);
} else if (defined(intermediate.url)) {
return validateURLEntry(intermediate, opts);
} else if (defined(intermediate.pattern)) {
return validatePatternEntry(intermediate, opts);
} else if (defined(intermediate.title)) {
return validateParentEntry(intermediate, opts);
} else {
return validationError("expected an entry with 'file', 'url', 'pattern', or 'title'", opts);
}
}

/**
* validate a MyST table of contents
*
* @param toc: structured TOC data
*/
export function validateTOC(toc: Record<string, unknown>): TOC {
// eslint-disable-next-line
// @ts-ignore
const Ajv = _Ajv.default;
const ajv = new Ajv();
const validate = ajv.compile(schema);
if (!validate(toc)) {
throw new Error(`The given contents do not form a valid TOC.`);
}

return toc as unknown as TOC;
export function validateTOC(toc: any, opts: ValidationOptions): TOC | undefined {
return validateList(toc, opts, (item, ind) =>
validateEntry(item, incrementOptions(`${ind}`, opts)),
) as unknown as TOC | undefined;
}
25 changes: 17 additions & 8 deletions packages/myst-toc/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,39 @@ export type FileEntry = {
} & CommonEntry;

/**
* Entry with a URL to an external URL
* Entry with a path to a single document with or without the file extension,
* and an array of children
*/
export type FileParentEntry = FileEntry & Omit<ParentEntry, 'title'>;

/**
* Entry with a url to an external resource
*/
export type URLEntry = {
url: string;
} & CommonEntry;

/**
* Entry with a url to an external resource,
* and an array of children
*/
export type URLParentEntry = URLEntry & Omit<ParentEntry, 'title'>;

/**
* Entry representing several documents through a glob
*/
export type PatternEntry = {
pattern: string;
} & CommonEntry;

/**
* Entry representing a single document
*/
export type DocumentEntry = FileEntry | URLEntry;

/**
* All possible types of Entry
*/
export type Entry =
| DocumentEntry
| (DocumentEntry & Omit<ParentEntry, 'title'>)
| FileEntry
| URLEntry
| FileParentEntry
| URLParentEntry
| PatternEntry
| ParentEntry;

Expand Down
Loading

0 comments on commit 8463044

Please sign in to comment.