Skip to content

Commit 4523f6d

Browse files
committed
Create NotePlugin, add base Plugin, update MD syntax
1 parent f653118 commit 4523f6d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2275
-2321
lines changed

github.js

Lines changed: 0 additions & 41 deletions
This file was deleted.

index.html

Lines changed: 1864 additions & 2023 deletions
Large diffs are not rendered by default.

matf.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import markdownit from 'markdown-it';
55
import ejs from 'ejs';
66
import open from 'open';
77

8-
import { GithubPlugin } from './github.js';
9-
import { WcagPlugin } from './wcag.js';
8+
import { GithubPlugin } from './plugins/github.js';
9+
import { NotePlugin } from './plugins/note.js';
10+
import { WcagPlugin } from './plugins/wcag.js';
1011

1112
const root = process.cwd();
1213

@@ -33,7 +34,8 @@ const readFiles = async (folder, extension) => {
3334
// Execute: init plugins, read files, render HTML
3435
const execute = async () => {
3536
console.log(`Initializing custom plugins...`);
36-
const githubPlugin = GithubPlugin.init('https://github.com/w3c/matf');
37+
const githubPlugin = await GithubPlugin.init('https://github.com/w3c/matf');
38+
const notePlugin = await NotePlugin.init();
3739
const wcagPlugin = await WcagPlugin.init('wcag', 'https://www.w3.org/TR/WCAG22/');
3840
const wcag2ictPlugin = await WcagPlugin.init('wcag2ict', 'https://www.w3.org/TR/wcag2ict-22/');
3941

@@ -42,6 +44,7 @@ const execute = async () => {
4244
html: true
4345
})
4446
.use(githubPlugin)
47+
.use(notePlugin)
4548
.use(wcagPlugin)
4649
.use(wcag2ictPlugin);
4750

plugins/github.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Plugin } from './plugin.js';
2+
3+
/**
4+
* The GithubPlugin adds a new markdown tag to render GitHub issues.
5+
* For example, the `[issue:71]` markdown will render issue #71 as a note with a link.
6+
*/
7+
export class GithubPlugin extends Plugin {
8+
constructor(url) {
9+
super('issue');
10+
this.url = url;
11+
}
12+
13+
/**
14+
* Initializes the plugin with the specified GitHub repository URL.
15+
* @param {string} url - The GitHub repository URL (e.g., `https://github.com/w3c/matf`).
16+
* @returns {function} - An initialized `GithubPlugin` function.
17+
*/
18+
static async init(url) {
19+
return new GithubPlugin(url).plugin();
20+
}
21+
22+
/**
23+
* Defines the regex for matching `[issue:<number>]`.
24+
* @returns {RegExp} - The regex for matching the issue syntax.
25+
*/
26+
regex() {
27+
return /^\[issue:(\d+)\]/;
28+
}
29+
30+
/**
31+
* Populates the token with issue number and link.
32+
* @param {Array} match - The regex match result.
33+
* @param {object} token - The token to populate.
34+
*/
35+
process(match, token) {
36+
const number = match[1];
37+
38+
token.number = number;
39+
token.link = `${this.url}/issues/${number}`;
40+
}
41+
42+
/**
43+
* Renders the token as a Note with a link to the issue on Github.
44+
* @param {object} token - The token to render.
45+
* @returns {string} - The rendered HTML string.
46+
*/
47+
render(token) {
48+
return `
49+
<div class="note" title="Work In Progress">
50+
<a href="${token.link}" target="_blank">
51+
Read issue #${token.number} on GitHub
52+
</a>
53+
</div>
54+
`;
55+
}
56+
}

plugins/note.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Plugin } from './plugin.js';
2+
import markdownit from 'markdown-it';
3+
4+
/**
5+
* The Note plugin adds a new markdown tag to render a Note.
6+
* For example, the `[note:markdown]` markdown will render a Note containing the given `markdown`.
7+
*/
8+
export class NotePlugin extends Plugin {
9+
constructor() {
10+
super('note');
11+
this.md = markdownit({
12+
html: true
13+
})
14+
}
15+
16+
/**
17+
* Initializes the NotePlugin without any additional setup.
18+
* @returns {Promise<{function}>} - An initialized `NotePlugin` function.
19+
*/
20+
static async init() {
21+
return new NotePlugin().plugin();
22+
}
23+
24+
/**
25+
* Defines the regex for matching `[note:<markdown>]`.
26+
* @returns {RegExp} - The regex for matching the note syntax.
27+
*/
28+
regex() {
29+
return /\[note:(.*)?\]/;
30+
}
31+
32+
/**
33+
* Processes the matched content and populates the token with raw markdown.
34+
* @param {Array} match - The regex match result.
35+
* @param {object} token - The token to populate.
36+
*/
37+
process(match, token) {
38+
token.content = match[1].trim(); // Extract the markdown content from the match
39+
}
40+
41+
/**
42+
* Renders the token into HTML by converting markdown to HTML for the content.
43+
* @param {object} token - The token to render.
44+
* @returns {string} - The rendered HTML string.
45+
*/
46+
render(token) {
47+
// Use markdown-it to render the content into HTML
48+
const html = this.md.render(token.content);
49+
return `
50+
<div class="note">
51+
${html}
52+
</div>
53+
`;
54+
}
55+
}

plugins/plugin.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Plugin: A base class with shared logic for custom markdown-it plugins.
3+
*/
4+
export class Plugin {
5+
constructor(tag) {
6+
this.tag = tag;
7+
}
8+
9+
/**
10+
* Initializes the plugin with any required setup.
11+
* Subclasses should override this method to return a fully initialized plugin.
12+
* @param {...*} args - Arguments required to initialize the plugin.
13+
* @returns {Promise<Plugin>} - A promise resolving to an initialized plugin instance.
14+
*/
15+
static async init(...args) {
16+
throw new Error(`Subclass ${this.name} must implement 'async static init(... args)'`);
17+
}
18+
19+
/**
20+
* Returns the markdown-it plugin function.
21+
* @returns {function} - A markdown-it plugin function.
22+
*/
23+
plugin() {
24+
return (md) => {
25+
// Register the parsing rule for the tag
26+
md.inline.ruler.before('emphasis', this.tag, (state, silent) => {
27+
const regex = this.regex();
28+
const match = state.src.slice(state.pos, state.posMax).match(regex);
29+
30+
if (!match) return false; // No match found
31+
if (silent) return true; // Validate without modifying state
32+
33+
const token = state.push(`${this.tag}_details`, '', 0);
34+
this.process(match, token);
35+
36+
state.pos += match[0].length; // Move the parser position forward
37+
return true;
38+
});
39+
40+
// Register the rendering rule for the tag
41+
md.renderer.rules[`${this.tag}_details`] = (tokens, idx) => {
42+
const token = tokens[idx];
43+
return this.render(token);
44+
};
45+
};
46+
}
47+
48+
/**
49+
* Returns the regex for matching the tag.
50+
* Subclasses should override this method.
51+
* @returns {RegExp} - The regex for matching the tag.
52+
*/
53+
regex() {
54+
throw new Error(`Subclass ${this.name} must implement 'regex()'`);
55+
}
56+
57+
/**
58+
* Processes the regex match and populates the token with data.
59+
* Subclasses should override this method.
60+
* @param {Array} match - The regex match result.
61+
* @param {object} token - The token to populate.
62+
*/
63+
process(match, token) {
64+
throw new Error(`Subclass ${this.name} must implement 'process(match, token)'`);
65+
}
66+
67+
/**
68+
* Renders the token to HTML.
69+
* Subclasses should override this method.
70+
* @param {object} token - The token to render.
71+
* @returns {string} - The rendered HTML string.
72+
*/
73+
render(token) {
74+
throw new Error(`Subclass ${this.name} must implement 'render(token)'`);
75+
}
76+
}

plugins/wcag.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Plugin } from './plugin.js';
2+
import * as cheerio from 'cheerio';
3+
import * as puppeteer from 'puppeteer';
4+
5+
/**
6+
* The WCAG plugin adds a new markdown tag which can render sections of WCAG.
7+
* For example, the `[wcag:status-messages]` markdown will render the `status-messages` section.
8+
*/
9+
export class WcagPlugin extends Plugin {
10+
constructor(tag, url, html) {
11+
super(tag);
12+
this.url = url;
13+
this.html = html;
14+
this.$ = cheerio.load(html);
15+
}
16+
17+
/**
18+
* Initializes the WcagPlugin by fetching and preparing the HTML content from the provided URL.
19+
* @param {string} tag - The tag used in markdown (e.g., `wcag` or `wcag2ict`).
20+
* @param {string} url - The URL to fetch content from (e.g., `https://www.w3.org/TR/WCAG22/`).
21+
* @returns {Promise<{function}>} - An initialized `WcagPlugin` function.
22+
*/
23+
static async init(tag, url) {
24+
const html = await WcagPlugin.fetchHtml(url);
25+
return new WcagPlugin(tag, url, html).plugin();
26+
}
27+
28+
/**
29+
* Fetches the rendered HTML from the given URL using Puppeteer.
30+
* @param {string} url - The URL to fetch.
31+
* @returns {Promise<string>} - The rendered HTML content.
32+
*/
33+
static async fetchHtml(url) {
34+
console.log(`Fetching rendered HTML for: ${url}`);
35+
const browser = await puppeteer.launch({ headless: true });
36+
const page = await browser.newPage();
37+
38+
try {
39+
await page.goto(url, { waitUntil: ['load', 'networkidle0'] });
40+
const html = await page.content();
41+
await browser.close();
42+
return html;
43+
} catch (error) {
44+
console.error(`Error fetching ${url}:`, error);
45+
await browser.close();
46+
return `<p>Error loading content from ${url}</p><p>${error}</p>`;
47+
}
48+
}
49+
50+
/**
51+
* Defines the regex for matching `[<tag>:<identifier>]`.
52+
* @returns {RegExp} - The regex for matching the note syntax.
53+
*/
54+
regex() {
55+
return new RegExp(`\\[${this.tag}:([a-zA-Z0-9-_\\.]+)\\]`);
56+
}
57+
58+
/**
59+
* Processes the matched content and populates the token with id, link, header and content.
60+
* @param {Array} match - The regex match result.
61+
* @param {object} token - The token to populate.
62+
*/
63+
process(match, token) {
64+
const id = match[1];
65+
const section = this.extract(id);
66+
67+
token.id = id;
68+
token.link = `${this.url}#${id}`;
69+
token.header = section.header;
70+
token.content = section.content;
71+
}
72+
73+
/**
74+
* Renders the token into HTML with an expand/collapse component.
75+
* @param {object} token - The token to render.
76+
* @returns {string} - The rendered HTML string.
77+
*/
78+
render(token) {
79+
return `
80+
<details class="wcag">
81+
<summary>
82+
<strong>${this.tag.toUpperCase()}:</strong> ${token.header}
83+
</summary>
84+
<blockquote cite="${token.link}">
85+
<div>${token.content}</div>
86+
</blockquote>
87+
<footer>
88+
<cite>
89+
<a href="${token.link}" target="_blank">${token.link}</a>
90+
</cite>
91+
</footer>
92+
</details>
93+
`;
94+
}
95+
96+
/**
97+
* Renders the token into HTML by converting markdown to HTML for the content.
98+
* @param {string} id - The identifier for the section.
99+
* @returns {object} - An object containing the `header` and `content` of the section.
100+
*/
101+
extract(id) {
102+
const $section = this.$(`#${id}`);
103+
if (!$section.length) {
104+
throw new Error(`Section not found: ${id}`);
105+
}
106+
107+
const header = $section.find('h1, h2, h3, h4, h5, h6').first().text();
108+
$section.find('.header-wrapper, .doclinks, .conformance-level').remove();
109+
$section.find('a').each((_, anchor) => {
110+
const href = this.$(anchor).attr('href');
111+
if (href) {
112+
this.$(anchor).attr('target', '_blank');
113+
if (!/^[\w]+:\/\//.test(href)) {
114+
const fullUrl = new URL(href, this.url).href;
115+
this.$(anchor).attr('href', fullUrl);
116+
}
117+
}
118+
});
119+
120+
return {
121+
header,
122+
content: $section.html(),
123+
};
124+
}
125+
}

0 commit comments

Comments
 (0)