@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.
@@ -1,67 +1,130 @@
1
- import { AllType, node as n } from '../nodes';
2
- import { DocNode, Renderer } from '../types';
3
- import { AllChildren, RenderContext } from './context';
1
+ import fs from 'fs/promises';
2
+ import { PackageUtil, RootIndex } from '@travetto/manifest';
4
3
 
5
- const titleCase = (a: string): string => a.replace(/^[a-z]/, v => v.toUpperCase());
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: Renderer = {
10
+ export const Markdown: RenderProvider<RenderContext> = {
8
11
  ext: 'md',
9
- render(c: AllChildren, context: RenderContext, root: AllType = c) {
10
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
11
- const recurse = (s: AllChildren | DocNode): string => this.render(s as AllChildren, context, root);
12
- switch (c._type) {
13
- case 'toc': return recurse(n.Group([n.SubSection(c.title), context.toc(root)]));
14
- case 'strong': return `**${recurse(c.content)}**`;
15
- case 'group': return c.nodes.map(cc => recurse(cc,)).join('');
16
- case 'comment': return `<!-- ${recurse(c.text)} -->`;
17
- case 'code':
18
- case 'install':
19
- case 'terminal':
20
- case 'config': return `
21
- **${titleCase(c._type)}: ${recurse(c.title)}**
22
- \`\`\`${c.language}
23
- ${context.cleanText(recurse(c.content))}
24
- \`\`\`\n`;
25
- case 'anchor': return `[${recurse(c.title)}](#${context.getAnchorId(recurse(c.fragment))}})`;
26
- case 'library':
27
- case 'file':
28
- case 'ref': return `[${recurse(c.title)}](${context.link(recurse(c.link), c)})`;
29
- case 'mod': return `[${recurse(c.title)}](${context.link(recurse(c.link), c)}#readme "${recurse(c.description)}")`;
30
- case 'image': return `![${recurse(c.title)}](${context.link(recurse(c.link), c)})`;
31
- case 'section': return `## ${recurse(c.title)}`;
32
- case 'subsection': return `### ${recurse(c.title)}`;
33
- case 'subsubsection': return `#### ${recurse(c.title)}`;
34
- case 'command':
35
- case 'method':
36
- case 'path':
37
- case 'class':
38
- case 'field':
39
- case 'input': return `\`${context.cleanText(recurse(c.content))}\``;
40
- case 'note': return `**Note**: ${context.cleanText(recurse(c.content))}`;
41
- case 'item': return `${c.ordered ? '1.' : '* '} ${recurse(c.node)}`;
42
- case 'list': {
43
- const out: string[] = [''];
44
- for (const el of c.items) {
45
- out.push(...recurse(el).split(/\n/g));
46
- }
47
- for (let i = 0; i < out.length; i++) {
48
- out[i] = ` ${out[i]}`;
49
- }
50
- return out.join('\n');
51
- }
52
- case 'table': {
53
- const header = ['', ...c.headers.map(h => recurse(h)), ''].join('|');
54
- const out: string[] = [
55
- header,
56
- header.replace(/[^|]/g, '-'),
57
- ...c.rows.map(row => ['', ...row.map(r => recurse(r)), ''].join('|'))
58
- ];
59
- return out.join('\n');
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(/&nbsp;/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 `![${props.title}](${context.link(props.href)})`;
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(/&nbsp;/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
- export interface DocNode { _type: string }
1
+ import { JSXElement, ValidHtmlTags } from '../jsx-runtime';
2
+ import { JSXElementByFn, c } from './jsx';
2
3
 
3
- export type Content = DocNode | string;
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<T extends DocNode = DocNode> {
13
- text: () => (T | Promise<T>);
9
+ export interface DocumentShape {
10
+ text: JSXElement | JSXElement[] | (() => Promise<JSXElement | JSXElement[]>);
14
11
  wrap?: Wrapper;
15
12
  }
16
13
 
17
- /**
18
- * Render context shape
19
- */
20
- export interface RenderContextShape {
21
-
22
- /**
23
- * Filename
24
- */
25
- file: string;
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 support
25
+ * Renderer
60
26
  */
61
- export type Renderer = {
62
- ext: string;
63
- render(node: DocNode, context: RenderContextShape, root?: DocNode): string;
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 { existsSync, readFileSync } from 'fs';
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 FileUtil {
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 (!existsSync(resolved)) {
28
- if (file.endsWith('.ts')) {
32
+ if (!(await fs.stat(resolved).catch(() => false))) {
33
+ if (ManifestModuleUtil.getFileType(file) === 'ts') {
29
34
  resolved = RootIndex.getSourceFile(file);
30
35
  }
31
- if (!existsSync(resolved)) {
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 = readFileSync(file, 'utf8');
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 = readFileSync(file, 'utf8')
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));
@@ -1,89 +1,66 @@
1
- import { FileUtil } from './file';
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 ResolveUtil {
11
+ export class DocResolveUtil {
7
12
 
8
- static resolveRef<T>(title: string | T, file: string): { title: string | T, file: string, line: number } {
13
+ static async resolveRef(title: string, file: string): Promise<ResolvedRef> {
9
14
 
10
15
  let line = 0;
11
- const res = FileUtil.read(file);
16
+ const res = await DocFileUtil.read(file);
12
17
  file = res.file;
13
18
 
14
- if (typeof title == 'string') {
15
- if (res.content) {
16
- line = res.content.split(/\n/g)
17
- .findIndex(x => new RegExp(`(class|function)[ ]+${title}`).test(x));
18
- if (line < 0) {
19
- line = 0;
20
- } else {
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
- return { title, file, line };
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 { content, language, file };
31
+ return { title, file, line };
46
32
  }
47
33
 
48
- static resolveConfig<T>(content: string | T, language: string): { content: string | T, language: string, file?: string } {
34
+ static async resolveCode(content: string, language?: string, outline = false): Promise<ResolvedCode> {
49
35
  let file: string | undefined;
50
- if (typeof content === 'string') {
51
- if (/^[@:A-Za-z0-9\/\\\-_.]+[.](json|ya?ml|properties)$/.test(content)) {
52
- const res = FileUtil.read(content);
53
- language = res.language;
54
- file = res.file;
55
- content = res.content;
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
- return { content, language, file };
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): { text: string, language: string, file: string, line: number } {
62
- const res = FileUtil.read(file);
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 ? content.findIndex((l, i) => i > startIdx && endPattern.test(l)) : content.length;
73
- let text = content.slice(startIdx, endIdx + 1).join('\n');
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 = FileUtil.buildOutline(text);
56
+ text = DocFileUtil.buildOutline(text);
77
57
  }
78
- return { text, language, line: startIdx + 1, file };
58
+
59
+ return { text, language, line: startIdx + 1, file: resolvedFile };
79
60
  }
80
61
 
81
- static resolveSnippetLink(file: string, startPattern: RegExp): { file: string, line: number } {
82
- const res = FileUtil.read(file);
83
- const line = res.content.split(/\n/g).findIndex(l => startPattern.test(l));
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
  }