Skip to content
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: 1 addition & 3 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@ jobs:
cache: npm
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: npm ci --ignore-scripts --no-audit --fund-no
run: npm ci --no-audit --no-fund
- name: Run tests
run: npm test
- name: Build Package
run: npm run build --if-present
- name: Publish to npm
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}

2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.9.0
25.3.0
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v0.1.7] - 2026-04-06

### Changed
- Update to node 25.3.0
- Update npm publishing
- Update various dependencies

### Fixed
- Update for changes to `Sanitizer`/`setHTML()`

## [v0.1.6] - 2025-04-08

### Added
Expand Down
5 changes: 2 additions & 3 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ignoreFile } from '@shgysk8zer0/eslint-config/ignoreFile.js';
import browser from '@shgysk8zer0/eslint-config/browser.js';
import { browser } from '@shgysk8zer0/eslint-config';

export default [ignoreFile, browser()];
export default browser();
18 changes: 11 additions & 7 deletions http.config.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import '@shgysk8zer0/polyfills';
import { Importmap } from '@shgysk8zer0/importmap';
const importmap = new Importmap();
await importmap.importLocalPackage();

export default {
open: true,
routes: {
'/': async (req) => {
if (req.destination === 'document') {
// Hack to make it not read-only
const importmap = await import('@shgysk8zer0/importmap').then(mod => ({ ...mod.importmap }));
// const importmap = await import('@shgysk8zer0/importmap').then(mod => ({ ...mod.importmap }));
const { readFile } = await import('node:fs/promises');
importmap.imports['@aegisjsproject/markdown'] = '/markdown.min.js';
importmap.imports['@aegisjsproject/markdown/'] = '/';
// importmap.imports['@aegisjsproject/markdown'] = '/markdown.min.js';
// importmap.imports['@aegisjsproject/markdown/'] = '/';
const json = JSON.stringify(importmap);
const hash = new Uint8Array(await crypto.subtle.digest('SHA-384', new TextEncoder().encode(json)));
const integrity = 'sha384-' + hash.toBase64();
const integrity = await importmap.getIntegrity();
const doc = await readFile('./test/index.html', { encoding: 'utf-8' })
.then(doc => doc.replace('{{ importmap }}', json).replace('{{ integrity }}', integrity));

const csp = `default-src 'none';
script-src 'self' https://unpkg.com/@shgysk8zer0/ https://unpkg.com/@aegisjsproject/ https://unpkg.com/@highlightjs/ '${integrity}';
script-src 'self' https://unpkg.com/@shgysk8zer0/ https://unpkg.com/@aegisjsproject/ https://unpkg.com/@highlightjs/ ${importmap.resolve('marked')} ${importmap.resolve('marked-highlight')} '${integrity}';
style-src 'self' blob: https://unpkg.com/@highlightjs/;
img-src 'self' https://img.shields.io/ https://github.com/AegisJSProject/markdown/workflows/ https://github.com/AegisJSProject/markdown/actions/workflows/;
img-src 'self' https://img.shields.io/ https://github.com/AegisJSProject/markdown/workflows/ https://github.com/AegisJSProject/markdown/actions/workflows/ https://i.imgur.com/;
connect-src 'self';
trusted-types empty#html empty#script aegis-sanitizer#html;
require-trusted-types-for 'script'`;
Expand Down
99 changes: 63 additions & 36 deletions markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ import { stringify } from '@aegisjsproject/core/stringify.js';
import hljs from 'highlight.js/core.min.js';
import plaintext from 'highlight.js/languages/plaintext.min.js';

const SANITIZER = {
elements: [
'a', 'blockquote', 'br', 'code', 'del', 'em', 'h1', 'h2', 'h3', 'h4',
'h5', 'h6', 'hr', 'img', 'li', 'ol', 'p', 'pre', 'strong', 'table',
'tbody', 'td', 'th', 'thead', 'tr', 'ul', 'span', 'div', 'details',
'summary', 'dialog', 'sup', 'sub', 'kbd', 'samp', 'var', 'mark', 'q',
'cite', 'abbr', 'figure', 'figcaption', 'time', 'address', 's', 'u',
'small', 'b', 'i', 'dfn', 'ins', 'slot',
],
attributes: [
'href', 'src', 'alt', 'width', 'height','download', 'title', 'class', 'id',
'loading', 'crossorigin', 'rel', 'decoding', 'target', 'popover',
'srcset', 'sizes', 'media', 'datetime', 'dir', 'lang', 'name', 'hidden',
'inert', 'itemscope', 'itemtype', 'itemprop', 'itemref', 'itemid',
],
comments: true,
dataAttributes: true,
};

export const hljsURL = new URL(`https://unpkg.com/@highlightjs/cdn-assets@${hljs.versionString}/`);

export const registerLanguage = (name, def) => hljs.registerLanguage(name, def);
Expand All @@ -21,6 +40,28 @@ registerLanguage('plaintext', plaintext);

const sluggify = str => str.trim().replaceAll(/[^A-Za-z0-9]+/g, '-').toLowerCase();

function cleanUp(template, {
addHeadingIDs = true,
crossOrigin = 'anonymous',
loading = 'lazy',
idPrefix = null,
} = {}) {
const frag = template.content;

if (addHeadingIDs) {
frag.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
heading.id = typeof idPrefix === 'string' ? `${idPrefix}-${sluggify(heading.textContent)}` : sluggify(heading.textContent);
});
}

frag.querySelectorAll('img:not([laoding])').forEach(img => {
img.crossOrigin = crossOrigin;
img.loading = loading;
});

return frag;
}

export function createStyleSheet(path, { media, base = hljsURL } = {}) {
const link = document.createElement('link');
link.relList.add('stylesheet');
Expand All @@ -45,11 +86,12 @@ export function parse(input, {
fallbackLang = 'plaintext',
addHeadingIDs = true,
idPrefix = null,
allowElements,
allowAttributes,
allowCustomElements,
allowUnknownMarkup,
allowComments,
sanitizer: {
elements = SANITIZER.elements,
attributes = SANITIZER.attributes,
dataAttributes = SANITIZER.dataAttributes,
comments = SANITIZER.comments,
} = SANITIZER,
} = {}) {
const marked = new Marked(
markedHighlight({
Expand All @@ -61,7 +103,7 @@ export function parse(input, {
})
);

const frag = document.createDocumentFragment();
const template = document.createElement('template');
let raw = input.replaceAll(/\\`/g, '`');

if (String.dedent instanceof Function && raw.startsWith('\n') && /\n\t*$/.test(raw)) {
Expand All @@ -72,20 +114,12 @@ export function parse(input, {
}

const parsed = marked.parse(raw, { gfm, breaks, silent });
const doc = Document.parseHTML(parsed, {
allowElements, allowAttributes, allowCustomElements, allowUnknownMarkup,
allowComments,
});

frag.append(...doc.body.childNodes);

if (addHeadingIDs) {
frag.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
heading.id = typeof idPrefix === 'string' ? `${idPrefix}-${sluggify(heading.textContent)}` : sluggify(heading.textContent);
});
}
template.setHTML(parsed, {
sanitizer: { elements, attributes, dataAttributes, comments },
});

return frag;
return cleanUp(template, { addHeadingIDs, idPrefix });
}

export function createMDParser({
Expand All @@ -97,11 +131,12 @@ export function createMDParser({
addHeadingIDs = true,
idPrefix = null,
languages,
allowElements,
allowAttributes,
allowCustomElements,
allowUnknownMarkup,
allowComments,
sanitizer: {
elements = SANITIZER.elements,
attributes = SANITIZER.attributes,
dataAttributes = SANITIZER.dataAttributes,
comments = SANITIZER.comments,
} = SANITIZER,
} = {}) {
if (typeof languages === 'object' && languages !== null) {
registerLanguages(languages);
Expand All @@ -118,7 +153,7 @@ export function createMDParser({
);

return (strings, ...args) => {
const frag = document.createDocumentFragment();
const template = document.createElement('template');
let raw = String.raw(strings, ...args.map(stringify)).replaceAll(/\\`/g, '`');

if (String.dedent instanceof Function && raw.startsWith('\n') && /\n\t*$/.test(raw)) {
Expand All @@ -129,20 +164,12 @@ export function createMDParser({
}

const parsed = marked.parse(raw, { gfm, breaks, silent });
const doc = Document.parseHTML(parsed, {
allowElements, allowAttributes, allowCustomElements, allowUnknownMarkup,
allowComments,
});

frag.append(...doc.body.childNodes);

if (addHeadingIDs) {
frag.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
heading.id = typeof idPrefix === 'string' ? `${idPrefix}-${sluggify(heading.textContent)}` : sluggify(heading.textContent);
});
}
template.setHTML(parsed, {
sanitizer: { elements: elements, attributes, comments, dataAttributes }
});

return frag;
return cleanUp(template, { addHeadingIDs, idPrefix });
};
}

Expand Down
Loading
Loading