@travetto/doc 3.0.2 → 3.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -83
- package/__index__.ts +3 -4
- package/package.json +3 -3
- package/src/jsx.ts +107 -0
- package/src/mapping/lib-mapping.ts +107 -0
- package/src/mapping/mod-mapping.ts +250 -0
- package/src/render/context.ts +95 -25
- package/src/render/html.ts +151 -71
- package/src/render/markdown.ts +123 -60
- package/src/render/renderer.ts +146 -0
- package/src/types.ts +23 -53
- package/src/util/file.ts +28 -13
- package/src/util/resolve.ts +39 -62
- package/src/util/run.ts +11 -22
- package/support/cli.doc.ts +58 -50
- package/src/doc.ts +0 -24
- package/src/lib.ts +0 -109
- package/src/mod-mapping.ts +0 -258
- package/src/mod.ts +0 -8
- package/src/nodes.ts +0 -334
- package/src/render/util.ts +0 -67
package/src/render/markdown.ts
CHANGED
|
@@ -1,67 +1,130 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { AllChildren, RenderContext } from './context';
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { PackageUtil, RootIndex } from '@travetto/manifest';
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
import { RenderProvider } from '../types';
|
|
5
|
+
import { c, getComponentName } from '../jsx';
|
|
6
|
+
import { MOD_MAPPING } from '../mapping/mod-mapping';
|
|
7
|
+
import { LIB_MAPPING } from '../mapping/lib-mapping';
|
|
8
|
+
import { RenderContext } from './context';
|
|
6
9
|
|
|
7
|
-
export const Markdown:
|
|
10
|
+
export const Markdown: RenderProvider<RenderContext> = {
|
|
8
11
|
ext: 'md',
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
12
|
+
finalize: (text, context) => {
|
|
13
|
+
const brand = `<!-- ${context.generatedStamp} -->\n<!-- ${context.rebuildStamp} -->`;
|
|
14
|
+
const cleaned = text
|
|
15
|
+
.replace(/(\[[^\]]+\]\([^)]+\))([A-Za-z0-9$]+)/g, (_, link, v) => v === 's' ? _ : `${link} ${v}`)
|
|
16
|
+
.replace(/(\S)\n(#)/g, (_, l, r) => `${l}\n\n${r}`);
|
|
17
|
+
return `${brand}\n${cleaned}`;
|
|
18
|
+
},
|
|
19
|
+
strong: async ({ recurse }) => `**${await recurse()}**`,
|
|
20
|
+
hr: async () => '\n------------------\n',
|
|
21
|
+
br: async () => '\n\n',
|
|
22
|
+
em: async ({ recurse }) => `*${await recurse()}*`,
|
|
23
|
+
ul: async ({ recurse }) => `\n${await recurse()}`,
|
|
24
|
+
ol: async ({ recurse }) => `\n${await recurse()}`,
|
|
25
|
+
li: async ({ recurse, stack }) => {
|
|
26
|
+
const parent = stack.reverse().find(x => x.type === 'ol' || x.type === 'ul');
|
|
27
|
+
const depth = stack.filter(x => x.type === 'ol' || x.type === 'ul').length;
|
|
28
|
+
return `${' '.repeat(depth)}${(parent && parent.type === 'ol') ? '1.' : '* '} ${await recurse()}\n`;
|
|
29
|
+
},
|
|
30
|
+
table: async ({ recurse }) => recurse(),
|
|
31
|
+
tbody: async ({ recurse }) => recurse(),
|
|
32
|
+
td: async ({ recurse }) => `|${await recurse()}`,
|
|
33
|
+
tr: async ({ recurse }) => `${await recurse()}|\n`,
|
|
34
|
+
thead: async ({ recurse }) => {
|
|
35
|
+
const row = await recurse();
|
|
36
|
+
return `${row}${row.replace(/[^|\n]/g, '-')}`;
|
|
37
|
+
},
|
|
38
|
+
h2: async ({ recurse }) => `\n## ${await recurse()}\n\n`,
|
|
39
|
+
h3: async ({ recurse }) => `\n### ${await recurse()}\n\n`,
|
|
40
|
+
h4: async ({ recurse }) => `\n#### ${await recurse()}\n\n`,
|
|
41
|
+
Execution: async ({ context, el, props, createState }) => {
|
|
42
|
+
const output = await context.execute(el);
|
|
43
|
+
const displayCmd = props.config?.formatCommand?.(props.cmd, props.args ?? []) ??
|
|
44
|
+
`${el.props.cmd} ${(el.props.args ?? []).join(' ')}`;
|
|
45
|
+
const state = createState('Terminal', {
|
|
46
|
+
language: 'bash',
|
|
47
|
+
title: el.props.title,
|
|
48
|
+
src: [`$ ${displayCmd}`, '', context.cleanText(output)].join('\n')
|
|
49
|
+
});
|
|
50
|
+
return Markdown.Terminal(state);
|
|
51
|
+
},
|
|
52
|
+
Install: async ({ context, el }) =>
|
|
53
|
+
`\n\n**Install: ${el.props.title}**
|
|
54
|
+
\`\`\`bash
|
|
55
|
+
npm install ${el.props.pkg}
|
|
56
|
+
|
|
57
|
+
# or
|
|
58
|
+
|
|
59
|
+
yarn add ${el.props.pkg}
|
|
60
|
+
\`\`\`
|
|
61
|
+
`,
|
|
62
|
+
Code: async ({ context, el }) => {
|
|
63
|
+
const name = getComponentName(el.type);
|
|
64
|
+
const content = await context.resolveCode(el);
|
|
65
|
+
let lang = content.language;
|
|
66
|
+
if (!lang) {
|
|
67
|
+
if (el.type === c.Terminal) {
|
|
68
|
+
lang = 'bash';
|
|
69
|
+
} else if (el.type === c.Code) {
|
|
70
|
+
lang = 'typescript';
|
|
60
71
|
}
|
|
61
|
-
case 'header':
|
|
62
|
-
return `# ${recurse(c.title)}\n${c.description ? `## ${recurse(c.description)}\n` : ''}${('install' in c && c.install) ? recurse(n.Install(c.package, c.package)) : ''}\n`;
|
|
63
|
-
case 'text':
|
|
64
|
-
return c.content.replace(/ /g, ' ');
|
|
65
72
|
}
|
|
73
|
+
return `\n\n**${name}: ${el.props.title}**
|
|
74
|
+
\`\`\`${lang}
|
|
75
|
+
${context.cleanText(content.text)}
|
|
76
|
+
\`\`\`\n\n`;
|
|
77
|
+
},
|
|
78
|
+
Terminal: state => Markdown.Code(state),
|
|
79
|
+
Config: state => Markdown.Code(state),
|
|
80
|
+
|
|
81
|
+
Section: async ({ el, recurse }) => `\n## ${el.props.title}\n${await recurse()}\n`,
|
|
82
|
+
SubSection: async ({ el, recurse }) => `\n### ${el.props.title}\n${await recurse()}\n`,
|
|
83
|
+
SubSubSection: async ({ el, recurse }) => `\n#### ${el.props.title}\n${await recurse()}\n`,
|
|
84
|
+
|
|
85
|
+
Command: state => Markdown.Input(state),
|
|
86
|
+
Method: state => Markdown.Input(state),
|
|
87
|
+
Path: state => Markdown.Input(state),
|
|
88
|
+
Class: state => Markdown.Input(state),
|
|
89
|
+
Field: state => Markdown.Input(state),
|
|
90
|
+
Input: async ({ props }) => `\`${props.name}\``,
|
|
91
|
+
|
|
92
|
+
Anchor: async ({ context, props }) => `[${props.title}](#${context.getAnchorId(props.href)})`,
|
|
93
|
+
File: state => Markdown.Ref(state),
|
|
94
|
+
Ref: async ({ context, props }) => `[${props.title}](${context.link(props.href, props)})`,
|
|
95
|
+
|
|
96
|
+
CodeLink: async ({ context, props, el }) => {
|
|
97
|
+
const target = await context.resolveCodeLink(el);
|
|
98
|
+
return `[${props.title}](${context.link(target.file, target)})`;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
Image: async ({ props, context }) => {
|
|
102
|
+
if (!/^https?:/.test(props.href) && !(await fs.stat(props.href).catch(() => false))) {
|
|
103
|
+
throw new Error(`${props.href} is not a valid location`);
|
|
104
|
+
}
|
|
105
|
+
return `})`;
|
|
106
|
+
},
|
|
107
|
+
Note: async ({ context, recurse }) => `\n\n**Note**: ${context.cleanText(await recurse())}\n`,
|
|
108
|
+
Header: async ({ props }) => `# ${props.title}\n${props.description ? `## ${props.description}\n` : ''}\n`,
|
|
109
|
+
|
|
110
|
+
StdHeader: async state => {
|
|
111
|
+
const mod = state.el.props.mod ?? RootIndex.mainPackage.name;
|
|
112
|
+
const pkg = PackageUtil.readPackage(RootIndex.getModule(mod)!.sourcePath);
|
|
113
|
+
const title = pkg.travetto?.displayName ?? pkg.name;
|
|
114
|
+
const desc = pkg.description;
|
|
115
|
+
let install = '';
|
|
116
|
+
if (state.el.props.install !== false) {
|
|
117
|
+
const sub = state.createState('Install', { title: pkg.name, pkg: pkg.name });
|
|
118
|
+
install = await Markdown.Install(sub);
|
|
119
|
+
}
|
|
120
|
+
return `# ${title}\n${desc ? `## ${desc}\n` : ''}${install}\n`;
|
|
121
|
+
},
|
|
122
|
+
Mod: async ({ props, context }) => {
|
|
123
|
+
const cfg = MOD_MAPPING[props.name];
|
|
124
|
+
return `[${cfg.displayName}](${context.link(cfg.folder, cfg)}#readme "${cfg.description}")`;
|
|
125
|
+
},
|
|
126
|
+
Library: async ({ props }) => {
|
|
127
|
+
const cfg = LIB_MAPPING[props.name];
|
|
128
|
+
return `[${cfg.title}](${cfg.href})`;
|
|
66
129
|
}
|
|
67
130
|
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ManifestRoot, PackageUtil, path, RootIndex } from '@travetto/manifest';
|
|
2
|
+
|
|
3
|
+
import { isJSXElement, JSXElement, createFragment, JSXFragmentType } from '@travetto/doc/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
import { EMPTY_ELEMENT, getComponentName, JSXElementByFn, c } from '../jsx';
|
|
6
|
+
import { DocumentShape, RenderProvider, RenderState } from '../types';
|
|
7
|
+
import { DocFileUtil } from '../util/file';
|
|
8
|
+
|
|
9
|
+
import { RenderContext } from './context';
|
|
10
|
+
import { Html } from './html';
|
|
11
|
+
import { Markdown } from './markdown';
|
|
12
|
+
|
|
13
|
+
const providers = { [Html.ext]: Html, [Markdown.ext]: Markdown };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Doc Renderer
|
|
17
|
+
*/
|
|
18
|
+
export class DocRenderer {
|
|
19
|
+
|
|
20
|
+
static async get(file: string, manifest: ManifestRoot): Promise<DocRenderer> {
|
|
21
|
+
const mod = RootIndex.getFromSource(file)?.import;
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
23
|
+
const res = await import(mod!) as DocumentShape;
|
|
24
|
+
|
|
25
|
+
const pkg = PackageUtil.readPackage(manifest.workspacePath);
|
|
26
|
+
const mainPath = path.resolve(manifest.workspacePath, manifest.mainFolder);
|
|
27
|
+
const repoBaseUrl = pkg.travetto?.docBaseUrl ?? mainPath;
|
|
28
|
+
return new DocRenderer(res,
|
|
29
|
+
new RenderContext(file, repoBaseUrl, path.resolve(pkg.travetto?.docRoot ?? manifest.workspacePath))
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#root: DocumentShape;
|
|
34
|
+
#rootNode: JSXElement | JSXElement[];
|
|
35
|
+
#support: RenderContext;
|
|
36
|
+
|
|
37
|
+
constructor(root: DocumentShape, support: RenderContext) {
|
|
38
|
+
this.#root = root;
|
|
39
|
+
this.#support = support;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async #render(
|
|
43
|
+
renderer: RenderProvider<RenderContext>,
|
|
44
|
+
node: JSXElement[] | JSXElement | string | bigint | object | number | boolean | null | undefined,
|
|
45
|
+
stack: JSXElement[] = []
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
|
|
48
|
+
if (node === null || node === undefined) {
|
|
49
|
+
return '';
|
|
50
|
+
} else if (Array.isArray(node)) {
|
|
51
|
+
const out: string[] = [];
|
|
52
|
+
for (const el of node) {
|
|
53
|
+
out.push(await this.#render(renderer, el, stack));
|
|
54
|
+
}
|
|
55
|
+
return out.join('');
|
|
56
|
+
} else if (isJSXElement(node)) {
|
|
57
|
+
let final: JSXElement = node;
|
|
58
|
+
// Render simple element if needed
|
|
59
|
+
if (typeof node.type === 'function') {
|
|
60
|
+
// @ts-expect-error
|
|
61
|
+
const out = node.type(node.props);
|
|
62
|
+
final = out !== EMPTY_ELEMENT ? out : final;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (final.type === createFragment || final.type === JSXFragmentType) {
|
|
66
|
+
return this.#render(renderer, final.props.children ?? []);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(final)) {
|
|
70
|
+
return this.#render(renderer, final, stack);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const name = getComponentName(final.type);
|
|
74
|
+
if (name in renderer) {
|
|
75
|
+
const recurse = () => this.#render(renderer, final.props.children ?? [], [...stack, final]);
|
|
76
|
+
// @ts-expect-error
|
|
77
|
+
const state: RenderState<JSXElement, RenderContext> = {
|
|
78
|
+
el: final, props: final.props, recurse, stack, context: this.#support
|
|
79
|
+
};
|
|
80
|
+
state.createState = (key, props) => this.createState(state, key, props);
|
|
81
|
+
// @ts-expect-error
|
|
82
|
+
return renderer[name](state);
|
|
83
|
+
} else {
|
|
84
|
+
console.log(final);
|
|
85
|
+
throw new Error(`Unknown element: ${final.type}`);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
switch (typeof node) {
|
|
89
|
+
case 'string': return node.replace(/ /g, ' ');
|
|
90
|
+
case 'number':
|
|
91
|
+
case 'bigint':
|
|
92
|
+
case 'boolean': return `${node}`;
|
|
93
|
+
default: {
|
|
94
|
+
const meta = (typeof node === 'function' ? RootIndex.getFunctionMetadata(node) : undefined);
|
|
95
|
+
if (meta && typeof node === 'function') {
|
|
96
|
+
const title = (await DocFileUtil.isDecorator(node.name, meta.source)) ? `@${node.name}` : node.name;
|
|
97
|
+
const el = this.#support.createElement('CodeLink', {
|
|
98
|
+
src: meta.source,
|
|
99
|
+
startRe: new RegExp(`(class|function)\\s+(${node.name})`),
|
|
100
|
+
title
|
|
101
|
+
});
|
|
102
|
+
// @ts-expect-error
|
|
103
|
+
const state: RenderState<JSXElementByFn<'CodeLink'>, RenderContext> = {
|
|
104
|
+
el, props: el.props, recurse: async () => '', context: this.#support, stack: []
|
|
105
|
+
};
|
|
106
|
+
// @ts-expect-error
|
|
107
|
+
state.createState = (key, props) => this.createState(state, key, props);
|
|
108
|
+
return await renderer.CodeLink(state);
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`Unknown object type: ${typeof node}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
createState<K extends keyof typeof c>(
|
|
117
|
+
state: RenderState<JSXElement, RenderContext>,
|
|
118
|
+
key: K,
|
|
119
|
+
props: JSXElementByFn<K>['props']
|
|
120
|
+
// @ts-expect-error
|
|
121
|
+
): RenderState<JSXElementByFn<K>, RenderContext> {
|
|
122
|
+
const el = this.#support.createElement(key, props);
|
|
123
|
+
return { ...state, el, props: el.props };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Render a context given a specific renderer
|
|
128
|
+
* @param renderer
|
|
129
|
+
*/
|
|
130
|
+
async render(fmt: keyof typeof providers): Promise<string> {
|
|
131
|
+
if (!providers[fmt]) {
|
|
132
|
+
throw new Error(`Unknown renderer with format: ${fmt}`);
|
|
133
|
+
}
|
|
134
|
+
if (!this.#rootNode) {
|
|
135
|
+
this.#rootNode = (Array.isArray(this.#root.text) || isJSXElement(this.#root.text)) ?
|
|
136
|
+
this.#root.text : await (this.#root.text());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const text = await this.#render(providers[fmt], this.#rootNode);
|
|
140
|
+
let cleaned = `${text.replace(/\n{3,100}/msg, '\n\n').trim()}\n`;
|
|
141
|
+
if (this.#root.wrap?.[fmt]) {
|
|
142
|
+
cleaned = this.#root.wrap[fmt](cleaned);
|
|
143
|
+
}
|
|
144
|
+
return providers[fmt].finalize(cleaned, this.#support);
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,64 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import { JSXElement, ValidHtmlTags } from '../jsx-runtime';
|
|
2
|
+
import { JSXElementByFn, c } from './jsx';
|
|
2
3
|
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
export type TextType = { _type: 'text', content: string };
|
|
6
|
-
|
|
7
|
-
export type Wrapper = Record<string, (c: string) => string>;
|
|
4
|
+
export type Wrapper = Record<string, (cnt: string) => string>;
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* Document file shape
|
|
11
8
|
*/
|
|
12
|
-
export interface DocumentShape
|
|
13
|
-
text: () =>
|
|
9
|
+
export interface DocumentShape {
|
|
10
|
+
text: JSXElement | JSXElement[] | (() => Promise<JSXElement | JSXElement[]>);
|
|
14
11
|
wrap?: Wrapper;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Github root for project
|
|
29
|
-
*/
|
|
30
|
-
baseUrl: string;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Github root for Travetto framework
|
|
34
|
-
*/
|
|
35
|
-
travettoBaseUrl: string;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Get table of contents
|
|
39
|
-
*/
|
|
40
|
-
toc(root: DocNode): DocNode;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Generate link location
|
|
44
|
-
*/
|
|
45
|
-
link(text: string, line?: number | { [key: string]: unknown, line?: number }): string;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Clean text
|
|
49
|
-
*/
|
|
50
|
-
cleanText(a?: string): string;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get a consistent anchor id
|
|
54
|
-
*/
|
|
55
|
-
getAnchorId(a: string): string;
|
|
56
|
-
}
|
|
14
|
+
export type RenderState<T extends JSXElement, C> = {
|
|
15
|
+
el: T;
|
|
16
|
+
props: T['props'];
|
|
17
|
+
recurse: () => Promise<string>;
|
|
18
|
+
stack: JSXElement[];
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
createState: <K extends keyof typeof c>(key: K, props: JSXElementByFn<K>['props']) => RenderState<JSXElementByFn<K>, C>;
|
|
21
|
+
context: C;
|
|
22
|
+
};
|
|
57
23
|
|
|
58
24
|
/**
|
|
59
|
-
* Renderer
|
|
25
|
+
* Renderer
|
|
60
26
|
*/
|
|
61
|
-
export type
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
27
|
+
export type RenderProvider<C> =
|
|
28
|
+
{
|
|
29
|
+
ext: string;
|
|
30
|
+
finalize: (text: string, ctx: C) => string;
|
|
31
|
+
} &
|
|
32
|
+
{ [K in ValidHtmlTags]: (state: RenderState<JSXElement<K>, C>) => Promise<string>; } &
|
|
33
|
+
// @ts-expect-error
|
|
34
|
+
{ [K in keyof typeof c]: (state: RenderState<JSXElementByFn<K>, C>) => Promise<string>; };
|
package/src/util/file.ts
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
2
|
|
|
3
|
-
import { path, RootIndex } from '@travetto/manifest';
|
|
3
|
+
import { ManifestModuleUtil, path, RootIndex } from '@travetto/manifest';
|
|
4
4
|
|
|
5
5
|
const ESLINT_PATTERN = /\s*\/\/ eslint.*$/;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Standard file utilities
|
|
9
9
|
*/
|
|
10
|
-
export class
|
|
10
|
+
export class DocFileUtil {
|
|
11
11
|
|
|
12
12
|
static #decCache: Record<string, boolean> = {};
|
|
13
13
|
static #extToLang: Record<string, string> = {
|
|
14
14
|
ts: 'typescript',
|
|
15
|
+
tsx: 'typescript',
|
|
15
16
|
js: 'javascript',
|
|
16
17
|
yml: 'yaml',
|
|
17
18
|
sh: 'bash',
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
static isFile(src: string): boolean {
|
|
22
|
+
return /^[@:A-Za-z0-9\/\\\-_.]+[.]([a-z]{2,10})$/.test(src);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
/**
|
|
21
26
|
* Resolve file
|
|
22
27
|
* @param file
|
|
23
28
|
* @returns
|
|
24
29
|
*/
|
|
25
|
-
static resolveFile(file: string): string {
|
|
30
|
+
static async resolveFile(file: string): Promise<string> {
|
|
26
31
|
let resolved = path.resolve(file);
|
|
27
|
-
if (!
|
|
28
|
-
if (
|
|
32
|
+
if (!(await fs.stat(resolved).catch(() => false))) {
|
|
33
|
+
if (ManifestModuleUtil.getFileType(file) === 'ts') {
|
|
29
34
|
resolved = RootIndex.getSourceFile(file);
|
|
30
35
|
}
|
|
31
|
-
if (!
|
|
36
|
+
if (!(await fs.stat(resolved).catch(() => false))) {
|
|
32
37
|
throw new Error(`Unknown file to resolve: ${file}`);
|
|
33
38
|
}
|
|
34
39
|
}
|
|
@@ -41,15 +46,15 @@ export class FileUtil {
|
|
|
41
46
|
* @param file
|
|
42
47
|
* @returns
|
|
43
48
|
*/
|
|
44
|
-
static read(file: string): { content: string, language: string, file: string } {
|
|
45
|
-
file = this.resolveFile(file);
|
|
49
|
+
static async read(file: string): Promise<{ content: string, language: string, file: string }> {
|
|
50
|
+
file = await this.resolveFile(file);
|
|
46
51
|
|
|
47
52
|
const ext = path.extname(file).replace(/^[.]/, '');
|
|
48
53
|
const language = this.#extToLang[ext] ?? ext;
|
|
49
54
|
|
|
50
55
|
let text: string | undefined;
|
|
51
56
|
if (language) {
|
|
52
|
-
text =
|
|
57
|
+
text = await fs.readFile(file, 'utf8');
|
|
53
58
|
|
|
54
59
|
text = text.split(/\n/)
|
|
55
60
|
.map(x => {
|
|
@@ -65,18 +70,28 @@ export class FileUtil {
|
|
|
65
70
|
return { content: text ?? '', language, file };
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
static async readCodeSnippet(file: string, startPattern: RegExp): Promise<{ file: string, startIdx: number, lines: string[], language: string }> {
|
|
74
|
+
const res = await this.read(file);
|
|
75
|
+
const lines = res.content.split(/\n/g);
|
|
76
|
+
const startIdx = lines.findIndex(l => startPattern.test(l));
|
|
77
|
+
if (startIdx < 0) {
|
|
78
|
+
throw new Error(`Pattern ${startPattern.source} not found in ${file}`);
|
|
79
|
+
}
|
|
80
|
+
return { file: res.file, startIdx, lines, language: res.language };
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
/**
|
|
69
84
|
* Determine if a file is a decorator
|
|
70
85
|
*/
|
|
71
|
-
static isDecorator(name: string, file: string): boolean {
|
|
72
|
-
file = this.resolveFile(file);
|
|
86
|
+
static async isDecorator(name: string, file: string): Promise<boolean> {
|
|
87
|
+
file = await this.resolveFile(file);
|
|
73
88
|
|
|
74
89
|
const key = `${name}:${file}`;
|
|
75
90
|
if (key in this.#decCache) {
|
|
76
91
|
return this.#decCache[key];
|
|
77
92
|
}
|
|
78
93
|
|
|
79
|
-
const text =
|
|
94
|
+
const text = (await fs.readFile(file, 'utf8'))
|
|
80
95
|
.split(/\n/g);
|
|
81
96
|
|
|
82
97
|
const start = text.findIndex(x => new RegExp(`function ${name}\\b`).test(x));
|
package/src/util/resolve.ts
CHANGED
|
@@ -1,89 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DocFileUtil } from './file';
|
|
2
|
+
|
|
3
|
+
export type ResolvedRef = { title: string, file: string, line: number };
|
|
4
|
+
export type ResolvedCode = { text: string, language: string, file?: string };
|
|
5
|
+
export type ResolvedSnippet = { text: string, language: string, file: string, line: number };
|
|
6
|
+
export type ResolvedSnippetLink = { file: string, line: number };
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Resolve utilities
|
|
5
10
|
*/
|
|
6
|
-
export class
|
|
11
|
+
export class DocResolveUtil {
|
|
7
12
|
|
|
8
|
-
static resolveRef
|
|
13
|
+
static async resolveRef(title: string, file: string): Promise<ResolvedRef> {
|
|
9
14
|
|
|
10
15
|
let line = 0;
|
|
11
|
-
const res =
|
|
16
|
+
const res = await DocFileUtil.read(file);
|
|
12
17
|
file = res.file;
|
|
13
18
|
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
line += 1;
|
|
22
|
-
}
|
|
23
|
-
if (FileUtil.isDecorator(title, file)) {
|
|
24
|
-
title = `@${title}`;
|
|
25
|
-
}
|
|
19
|
+
if (res.content) {
|
|
20
|
+
line = res.content.split(/\n/g)
|
|
21
|
+
.findIndex(x => new RegExp(`(class|function)[ ]+${title}`).test(x));
|
|
22
|
+
if (line < 0) {
|
|
23
|
+
line = 0;
|
|
24
|
+
} else {
|
|
25
|
+
line += 1;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
static resolveCode<T>(content: string | T, language: string, outline = false): { content: string | T, language: string, file?: string } {
|
|
32
|
-
let file: string | undefined;
|
|
33
|
-
if (typeof content === 'string') {
|
|
34
|
-
if (/^[@:A-Za-z0-9\/\\\-_.]+[.]([a-z]{2,4})$/.test(content)) {
|
|
35
|
-
const res = FileUtil.read(content);
|
|
36
|
-
language = res.language;
|
|
37
|
-
file = res.file;
|
|
38
|
-
content = res.content;
|
|
39
|
-
if (outline) {
|
|
40
|
-
content = FileUtil.buildOutline(content);
|
|
41
|
-
}
|
|
27
|
+
if (await DocFileUtil.isDecorator(title, file)) {
|
|
28
|
+
title = `@${title}`;
|
|
42
29
|
}
|
|
43
|
-
content = content.replace(/^\/\/# sourceMap.*$/gm, '');
|
|
44
30
|
}
|
|
45
|
-
return {
|
|
31
|
+
return { title, file, line };
|
|
46
32
|
}
|
|
47
33
|
|
|
48
|
-
static
|
|
34
|
+
static async resolveCode(content: string, language?: string, outline = false): Promise<ResolvedCode> {
|
|
49
35
|
let file: string | undefined;
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
if (DocFileUtil.isFile(content)) {
|
|
37
|
+
const res = await DocFileUtil.read(content);
|
|
38
|
+
language = res.language;
|
|
39
|
+
file = res.file;
|
|
40
|
+
content = res.content;
|
|
41
|
+
if (outline) {
|
|
42
|
+
content = DocFileUtil.buildOutline(content);
|
|
56
43
|
}
|
|
57
44
|
}
|
|
58
|
-
|
|
45
|
+
content = content.replace(/^\/\/# sourceMap.*$/gm, '');
|
|
46
|
+
return { text: content, language: language!, file };
|
|
59
47
|
}
|
|
60
48
|
|
|
61
|
-
static resolveSnippet(file: string, startPattern: RegExp, endPattern?: RegExp, outline = false):
|
|
62
|
-
const
|
|
63
|
-
const language = res.language;
|
|
64
|
-
file = res.file;
|
|
65
|
-
const content = res.content.split(/\n/g);
|
|
66
|
-
const startIdx = content.findIndex(l => startPattern.test(l));
|
|
67
|
-
|
|
68
|
-
if (startIdx < 0) {
|
|
69
|
-
throw new Error(`Pattern ${startPattern.source} not found in ${file}`);
|
|
70
|
-
}
|
|
49
|
+
static async resolveSnippet(file: string, startPattern: RegExp, endPattern?: RegExp, outline = false): Promise<ResolvedSnippet> {
|
|
50
|
+
const { lines, startIdx, language, file: resolvedFile } = await DocFileUtil.readCodeSnippet(file, startPattern);
|
|
71
51
|
|
|
72
|
-
const endIdx = endPattern ?
|
|
73
|
-
let text =
|
|
52
|
+
const endIdx = endPattern ? lines.findIndex((l, i) => i > startIdx && endPattern.test(l)) : lines.length;
|
|
53
|
+
let text = lines.slice(startIdx, endIdx + 1).join('\n');
|
|
74
54
|
|
|
75
55
|
if (outline) {
|
|
76
|
-
text =
|
|
56
|
+
text = DocFileUtil.buildOutline(text);
|
|
77
57
|
}
|
|
78
|
-
|
|
58
|
+
|
|
59
|
+
return { text, language, line: startIdx + 1, file: resolvedFile };
|
|
79
60
|
}
|
|
80
61
|
|
|
81
|
-
static
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
if (line < 0) {
|
|
85
|
-
throw new Error(`Pattern ${startPattern.source} not found in ${file}`);
|
|
86
|
-
}
|
|
87
|
-
return { file: res.file, line: line + 1 };
|
|
62
|
+
static async resolveCodeLink(file: string, startPattern: RegExp): Promise<ResolvedSnippetLink> {
|
|
63
|
+
const { startIdx, file: resolvedFile } = await DocFileUtil.readCodeSnippet(file, startPattern);
|
|
64
|
+
return { file: resolvedFile, line: startIdx + 1 };
|
|
88
65
|
}
|
|
89
66
|
}
|