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

Made plugin compatible with TypeDoc 0.22.x #447

Merged
merged 3 commits into from
Oct 24, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"html-escaper": "^3.0.0"
},
"peerDependencies": {
"typedoc": ">=0.21.0"
"typedoc": ">=0.22.0"
},
"devDependencies": {
"@types/html-escaper": "^3.0.0",
Expand All @@ -56,7 +56,7 @@
"rollup": "^2.53.3",
"rollup-plugin-typescript2": "^0.30.0",
"ts-jest": "^27.0.4",
"typedoc": "^0.21.4",
"typedoc": "^0.22.6",
"typescript": "^4.3.5"
}
}
14 changes: 3 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { Application } from 'typedoc/dist/lib/application';
import { Application } from 'typedoc';
import { MermaidPlugin } from './plugin';

export function load(PluginHost: Application): void {
const app = PluginHost.owner;
if (app.converter.hasComponent('mermaid')) {
return;
}

/**
* Add the plugin to the converter instance
*/
app.converter.addComponent('mermaid', new MermaidPlugin(app.converter));
export function load(app: Application): void {
new MermaidPlugin().addToApplication(app);
}
188 changes: 59 additions & 129 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,155 +1,85 @@
import * as html from 'html-escaper';
import { Converter } from 'typedoc/dist/lib/converter';
import { Component, ConverterComponent } from 'typedoc/dist/lib/converter/components';
import { Context } from 'typedoc/dist/lib/converter/context';
import { Comment, CommentTag } from 'typedoc/dist/lib/models/comments';
import { MarkdownEvent, PageEvent } from 'typedoc/dist/lib/output/events';
import { Converter, Context, PageEvent, Application, ReflectionKind } from 'typedoc';

/**
* Mermaid plugin component.
* 1. Load mermaid.js library.
* 2. Initialize mermaid.
*/
@Component({ name: 'mermaid' })
export class MermaidPlugin extends ConverterComponent {
/**
* 1. Load mermaid.js library.
* 2. Initialize mermaid.
* 3. Close body tag.
*/
private static customScriptsAndBodyClosingTag = `
<script
src="https://unpkg.com/mermaid/dist/mermaid.min.js"
></script>
<script>
mermaid.initialize({
startOnLoad: true,
});
</script>
</body>
`;

private static markdownStartMermaid = '\n```mermaid\n';
private static markdownEndMermaid = '\n```\n';

/**
* filter logic for Comment exist
*/
private static filterComment(comment: undefined | Comment): comment is Comment {
return comment !== undefined && !!comment;
}

/**
* filter logic for CommentTags exist
*/
private static filterCommentTags(tags: CommentTag[] | undefined): tags is CommentTag[] {
return tags !== undefined && !!tags;
}
const script =
'<script src="https://unpkg.com/mermaid/dist/mermaid.min.js"></script>' +
'<script>mermaid.initialize({startOnLoad:true});</script>';

/**
* return turn when tag's paramName is 'mermaid'
*/
private static isMermaidCommentTag(tag: CommentTag): boolean {
return tag.tagName === 'mermaid';
}
const mermaidBlockStart = '<div class="mermaid">';
const mermaidBlockEnd = '</div>';

/**
* get CommentTags for using `@mermaid` annotation from Context.
*/
private static mermaidTags(context: Context): CommentTag[] {
return Object.values(context.project.reflections) // get reflection from context
.map((reflection) => reflection.comment) // get Comment from Reflection
.filter(this.filterComment) // filter only comment exist
.map((comment) => comment.tags) // get CommentTags from Comment
.filter(this.filterCommentTags) // filter only CommentTags exist
.reduce((a, b) => a.concat(b), []) // merge all CommentTags
.filter(this.isMermaidCommentTag); // filter tag that paramName is 'mermaid'
}

/**
* Regex literal that matches body closing tag.
*/
private readonly BODY_CLOSING_TAG = /<\/body>/;
export class MermaidPlugin {
public addToApplication(app: Application): void {
app.converter.on(Converter.EVENT_RESOLVE_BEGIN, (context: Context) => {
this.onConverterResolveBegin(context);
});

/**
* The first line of text wraps h4.
* The other wraps by div classed mermaid.
*/
public convertCommentTagText(tagText: string): string {
const texts = tagText.split('\n');
// take first line
const title = texts.shift();
// the other
const mermaid = texts.join('\n');
return `#### ${title} \n\n <div class="mermaid">${mermaid}</div>`;
app.renderer.on({
[PageEvent.END]: (event: PageEvent) => {
this.onEndPage(event);
},
});
}

/**
* Insert custom script before closing body tag.
*/
public convertPageContents(contents: string): string {
if (this.BODY_CLOSING_TAG.test(contents)) {
return contents.replace(this.BODY_CLOSING_TAG, MermaidPlugin.customScriptsAndBodyClosingTag);
private onConverterResolveBegin(context: Context): void {
for (const reflection of context.project.getReflectionsByKind(ReflectionKind.All)) {
const { comment } = reflection;
if (comment) {
comment.text = this.handleMermaidCodeBlocks(comment.text);
for (const tag of comment.tags) {
if (tag.tagName === 'mermaid') {
tag.text = this.handleMermaidTag(tag.text);
} else {
tag.text = this.handleMermaidCodeBlocks(tag.text);
}
}
}
}
return contents;
}

/**
* listen to event on initialization
* Convert the text of `@mermaid` tags.
*
* This first line will be the title. It will be wrapped in an h4.
* All other lines are mermaid code and will be converted into a mermaid block.
*/
public initialize(): void {
this.listenTo(this.owner, {
[Converter.EVENT_RESOLVE_BEGIN]: this.onResolveBegin,
})
.listenTo(this.application.renderer, {
[PageEvent.END]: this.onPageEnd,
})
.listenTo(
this.application.renderer,
{
[MarkdownEvent.PARSE]: this.onParseMarkdown,
},
undefined,
100,
);
}
public handleMermaidTag(text: string): string {
const title = /^.*/.exec(text)?.[0] ?? '';
const code = text.slice(title.length);

return `#### ${title}\n\n${this.toMermaidBlock(code)}`;
}
/**
* Triggered when the converter begins converting a project.
* Replaces mermaid code blocks in Markdown text with mermaid blocks.
*/
public onResolveBegin(context: Context): void {
MermaidPlugin.mermaidTags(context).forEach((tag) => {
// convert
tag.text = this.convertCommentTagText(tag.text);
public handleMermaidCodeBlocks(text: string): string {
return text.replace(/^```mermaid[ \t\r]*\n([\s\S]*?)^```[ \t]*$/gm, (m, code) => {
return this.toMermaidBlock(code);
});
}

/**
* Triggered after a document has been rendered, just before it is written to disc.
* Remove duplicate lines to tidy up output
* Creates a mermaid block for the given mermaid code.
*/
public onPageEnd(page: PageEvent): void {
if (page.contents !== undefined) {
// convert
page.contents = this.convertPageContents(page.contents);
}
private toMermaidBlock(mermaidCode: string): string {
return mermaidBlockStart + html.escape(mermaidCode.trim()) + mermaidBlockEnd;
}

public onParseMarkdown(event: MarkdownEvent): void {
event.parsedText = this.replaceMarkdownMermaidCodeBlocks(event.parsedText);
private onEndPage(event: PageEvent): void {
if (event.contents !== undefined) {
event.contents = this.insertMermaidScript(event.contents);
}
}

public replaceMarkdownMermaidCodeBlocks(s: string): string {
let out = '';
let i = 0;
for (
let j = s.indexOf(MermaidPlugin.markdownStartMermaid, i);
j >= 0;
j = s.indexOf(MermaidPlugin.markdownStartMermaid, i)
) {
const start = j + MermaidPlugin.markdownStartMermaid.length;
const end = s.indexOf(MermaidPlugin.markdownEndMermaid, start);
out += `${s.slice(i, j + 1)}<div class="mermaid">${html.escape(s.slice(start, end))}</div>`;
i = end + MermaidPlugin.markdownEndMermaid.length - 1;
public insertMermaidScript(html: string): string {
if (!html.includes(mermaidBlockStart)) {
// this page doesn't need to load mermaid
return html;
}
return out + s.slice(i);

// find the closing </body> tag and insert our mermaid scripts
const bodyEndIndex = html.lastIndexOf('</body>');
return html.slice(0, bodyEndIndex) + script + html.slice(bodyEndIndex);
}
}
18 changes: 3 additions & 15 deletions test/__snapshots__/plugin.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MermaidPlugin convert CommentTag 1`] = `
"#### title
"#### title

<div class=\\"mermaid\\">graph</div>"
<div class=\\"mermaid\\">graph</div>"
`;

exports[`MermaidPlugin convert Markdown snippet returns same value if body closing tag not exist 1`] = `
Expand All @@ -17,16 +17,4 @@ more text"

exports[`MermaidPlugin convert PageContents returns same value if body closing tag not exist 1`] = `"hoge"`;

exports[`MermaidPlugin convert PageContents returns script tag for mermaid.js, initialize mermaid script, and body closing tag 1`] = `
"
<script
src=\\"https://unpkg.com/mermaid/dist/mermaid.min.js\\"
></script>
<script>
mermaid.initialize({
startOnLoad: true,
});
</script>
</body>
"
`;
exports[`MermaidPlugin convert PageContents returns script tag for mermaid.js, initialize mermaid script, and body closing tag 1`] = `"<div class=\\"mermaid\\"></div><script src=\\"https://unpkg.com/mermaid/dist/mermaid.min.js\\"></script><script>mermaid.initialize({startOnLoad:true});</script></body>"`;
59 changes: 0 additions & 59 deletions test/e2e.test.ts

This file was deleted.

18 changes: 6 additions & 12 deletions test/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import { Application } from 'typedoc/dist/lib/application';
import { MermaidPlugin } from '../src/plugin';

describe('MermaidPlugin', () => {
let plugin: MermaidPlugin;

beforeEach(() => {
const app = new Application();
plugin = new MermaidPlugin(app.converter);
});
const plugin = new MermaidPlugin();

it('convert CommentTag', () => {
const input = 'title\ngraph';
const result = plugin.convertCommentTagText(input);
const result = plugin.handleMermaidTag(input);
expect(result).toMatch('#### title');
expect(result).toMatch('<div class="mermaid">graph</div>');
expect(result).toMatchSnapshot();
});

it('convert PageContents returns script tag for mermaid.js, initialize mermaid script, and body closing tag', () => {
const input = '</body>';
const result = plugin.convertPageContents(input);
const input = '<div class="mermaid"></div></body>';
const result = plugin.insertMermaidScript(input);
expect(result).toMatch('</body>');
expect(result).toMatch('mermaid.initialize({');
expect(result).toMatch(/src="https:\/\/unpkg.com\/mermaid\/dist\/mermaid.min.js"/);
Expand All @@ -28,14 +22,14 @@ describe('MermaidPlugin', () => {

it('convert PageContents returns same value if body closing tag not exist', () => {
const input = 'hoge';
const result = plugin.convertPageContents(input);
const result = plugin.insertMermaidScript(input);
expect(result).toEqual('hoge');
expect(result).toMatchSnapshot();
});

it('convert Markdown snippet returns same value if body closing tag not exist', () => {
const input = '#### title\n\n```mermaid\ngraph LR\n <a> --> <b>\n```\n\nmore text';
const result = plugin.replaceMarkdownMermaidCodeBlocks(input);
const result = plugin.handleMermaidCodeBlocks(input);
expect(result).toMatch('#### title\n');
expect(result).toMatch('<div class="mermaid">graph LR\n &lt;a&gt; --&gt; &lt;b&gt;</div>');
expect(result).toMatch('\nmore text');
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ESNext",
"experimentalDecorators": true,
"moduleResolution": "node",
"allowJs": false,
"noImplicitAny": true,
"noImplicitReturns": true,
Expand Down
Loading