kopia lustrzana https://github.com/shoelace-style/shoelace
not perfect but faster (some things still broken)
rodzic
7f87887477
commit
def664ce75
|
@ -139,7 +139,6 @@
|
||||||
"semibold",
|
"semibold",
|
||||||
"sitedir",
|
"sitedir",
|
||||||
"slotchange",
|
"slotchange",
|
||||||
"smartquotes",
|
|
||||||
"spacebar",
|
"spacebar",
|
||||||
"stylesheet",
|
"stylesheet",
|
||||||
"Tabbable",
|
"Tabbable",
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
function normalizePathname(pathname) {
|
|
||||||
// Remove /index.html
|
|
||||||
if (pathname.endsWith('/index.html')) {
|
|
||||||
pathname = pathname.replace(/\/index\.html/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove trailing slashes
|
|
||||||
return pathname.replace(/\/$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a class name to links that are currently active.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc, options) {
|
|
||||||
options = {
|
|
||||||
className: 'active-link', // the class to add to active links
|
|
||||||
pathname: undefined, // the current pathname to compare
|
|
||||||
within: 'body', // element containing the target links
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
const within = doc.querySelector(options.within);
|
|
||||||
|
|
||||||
if (!within) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
within.querySelectorAll('a').forEach(link => {
|
|
||||||
if (normalizePathname(options.pathname) === normalizePathname(link.pathname)) {
|
|
||||||
link.classList.add(options.className);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
|
||||||
|
function normalizePathname(pathname) {
|
||||||
|
// Remove /index.html
|
||||||
|
if (pathname.endsWith('/index.html')) {
|
||||||
|
pathname = pathname.replace(/\/index\.html/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing slashes
|
||||||
|
return pathname.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a class name to links that are currently active.
|
||||||
|
*/
|
||||||
|
export function activeLinksPlugin(options) {
|
||||||
|
options = {
|
||||||
|
className: 'active-link', // the class to add to active links
|
||||||
|
pathname: undefined, // the current pathname to compare
|
||||||
|
within: 'body', // element containing the target links
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('active-links', function (content) {
|
||||||
|
const doc = parse(content);
|
||||||
|
const within = doc.querySelector(options.within);
|
||||||
|
|
||||||
|
if (!within) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the href attribute to 11ty's page URL
|
||||||
|
within.querySelectorAll('a[href]').forEach(a => {
|
||||||
|
// eslint-disable-next-line no-invalid-this
|
||||||
|
if (normalizePathname(a.getAttribute('href')) === normalizePathname(this.page.url)) {
|
||||||
|
a.classList.add(options.className);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
const { createSlug } = require('./strings.cjs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM.
|
|
||||||
* The same document will be returned with the appropriate DOM manipulations.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc, options) {
|
|
||||||
options = {
|
|
||||||
levels: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], // the headings to convert
|
|
||||||
className: 'anchor-heading', // the class name to add
|
|
||||||
within: 'body', // the element containing the target headings
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
const within = doc.querySelector(options.within);
|
|
||||||
|
|
||||||
if (!within) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
within.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
|
|
||||||
const hasAnchor = heading.querySelector('a');
|
|
||||||
const anchor = doc.createElement('a');
|
|
||||||
let id = heading.textContent ?? '';
|
|
||||||
let suffix = 0;
|
|
||||||
|
|
||||||
// Skip heading levels we don't care about
|
|
||||||
if (!options.levels?.includes(heading.tagName.toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert dots to underscores
|
|
||||||
id = id.replace(/\./g, '_');
|
|
||||||
|
|
||||||
// Turn it into a slug
|
|
||||||
id = createSlug(id);
|
|
||||||
|
|
||||||
// Make sure it starts with a letter
|
|
||||||
if (!/^[a-z]/i.test(id)) {
|
|
||||||
id = `id_${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the id is unique
|
|
||||||
const originalId = id;
|
|
||||||
while (doc.getElementById(id) !== null) {
|
|
||||||
id = `${originalId}-${++suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAnchor || !id) return;
|
|
||||||
|
|
||||||
heading.setAttribute('id', id);
|
|
||||||
anchor.setAttribute('href', `#${encodeURIComponent(id)}`);
|
|
||||||
anchor.setAttribute('aria-label', `Direct link to "${heading.textContent}"`);
|
|
||||||
|
|
||||||
if (options.className) {
|
|
||||||
heading.classList.add(options.className);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the anchor
|
|
||||||
heading.append(anchor);
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import slugify from 'slugify';
|
||||||
|
|
||||||
|
function createId(text) {
|
||||||
|
let slug = slugify(String(text), {
|
||||||
|
remove: /[^\w|\s]/g,
|
||||||
|
lower: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// ids must start with a letter
|
||||||
|
if (!/^[a-z]/i.test(slug)) {
|
||||||
|
slug = `quiet_${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eleventy plugin to add anchors to headings to content.
|
||||||
|
*/
|
||||||
|
export function anchorHeadingsPlugin(options = {}) {
|
||||||
|
options = {
|
||||||
|
container: 'body',
|
||||||
|
headingSelector: 'h2, h3, h4, h5, h6',
|
||||||
|
anchorLabel: 'Jump to heading',
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('anchor-headings', content => {
|
||||||
|
const doc = parse(content);
|
||||||
|
const container = doc.querySelector(options.container);
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for headings
|
||||||
|
container.querySelectorAll(options.headingSelector).forEach(heading => {
|
||||||
|
const hasAnchor = heading.querySelector('a');
|
||||||
|
const slug = createId(heading.textContent ?? '') ?? uuid().slice(-12);
|
||||||
|
let id = slug;
|
||||||
|
let suffix = 0;
|
||||||
|
|
||||||
|
// Make sure the slug is unique in the document
|
||||||
|
while (doc.getElementById(id) !== null) {
|
||||||
|
id = `${slug}-${++suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAnchor || !id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the anchor
|
||||||
|
const anchor = parse(`
|
||||||
|
<a href="#${encodeURIComponent(id)}" aria-label="Direct link to ${heading.textContent}"></a>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Update the heading
|
||||||
|
heading.setAttribute('id', id);
|
||||||
|
heading.classList.add('anchor-heading');
|
||||||
|
heading.appendChild(anchor);
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,14 +1,21 @@
|
||||||
const customElementsManifest = require('../../dist/custom-elements.json');
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const rootDir = dirname(dirname(__dirname));
|
||||||
|
|
||||||
|
const customElementsManifest = JSON.parse(fs.readFileSync(join(rootDir, 'dist/custom-elements.json'), 'utf-8'));
|
||||||
|
|
||||||
//
|
//
|
||||||
// Export it here so we can import it elsewhere and use the same version
|
// Export it here so we can import it elsewhere and use the same version
|
||||||
//
|
//
|
||||||
module.exports.customElementsManifest = customElementsManifest;
|
export { customElementsManifest };
|
||||||
|
|
||||||
//
|
//
|
||||||
// Gets all components from custom-elements.json and returns them in a more documentation-friendly format.
|
// Gets all components from custom-elements.json and returns them in a more documentation-friendly format.
|
||||||
//
|
//
|
||||||
module.exports.getAllComponents = function () {
|
export function getAllComponents() {
|
||||||
const allComponents = [];
|
const allComponents = [];
|
||||||
|
|
||||||
customElementsManifest.modules?.forEach(module => {
|
customElementsManifest.modules?.forEach(module => {
|
||||||
|
@ -68,4 +75,4 @@ module.exports.getAllComponents = function () {
|
||||||
if (a.name > b.name) return 1;
|
if (a.name > b.name) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
};
|
}
|
|
@ -1,138 +0,0 @@
|
||||||
let count = 1;
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns code fields with the :preview suffix into interactive code previews.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc, options) {
|
|
||||||
options = {
|
|
||||||
within: 'body', // the element containing the code fields to convert
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
const within = doc.querySelector(options.within);
|
|
||||||
if (!within) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
within.querySelectorAll('[class*=":preview"]').forEach(code => {
|
|
||||||
const pre = code.closest('pre');
|
|
||||||
if (!pre) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const adjacentPre = pre.nextElementSibling?.tagName.toLowerCase() === 'pre' ? pre.nextElementSibling : null;
|
|
||||||
const reactCode = adjacentPre?.querySelector('code[class$="react"]');
|
|
||||||
const sourceGroupId = `code-preview-source-group-${count}`;
|
|
||||||
const isExpanded = code.getAttribute('class').includes(':expanded');
|
|
||||||
const noCodePen = code.getAttribute('class').includes(':no-codepen');
|
|
||||||
|
|
||||||
count++;
|
|
||||||
|
|
||||||
const htmlButton = `
|
|
||||||
<button type="button"
|
|
||||||
title="Show HTML code"
|
|
||||||
class="code-preview__button code-preview__button--html"
|
|
||||||
>
|
|
||||||
HTML
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const reactButton = `
|
|
||||||
<button type="button" title="Show React code" class="code-preview__button code-preview__button--react">
|
|
||||||
React
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const codePenButton = `
|
|
||||||
<button type="button" class="code-preview__button code-preview__button--codepen" title="Edit on CodePen">
|
|
||||||
<svg
|
|
||||||
width="138"
|
|
||||||
height="26"
|
|
||||||
viewBox="0 0 138 26"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2.3"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const codePreview = `
|
|
||||||
<div class="code-preview ${isExpanded ? 'code-preview--expanded' : ''}">
|
|
||||||
<div class="code-preview__preview">
|
|
||||||
${code.textContent}
|
|
||||||
<div class="code-preview__resizer">
|
|
||||||
<sl-icon name="grip-vertical"></sl-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="code-preview__source-group" id="${sourceGroupId}">
|
|
||||||
<div class="code-preview__source code-preview__source--html" ${reactCode ? 'data-flavor="html"' : ''}>
|
|
||||||
<pre><code class="language-html">${escapeHtml(code.textContent)}</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${
|
|
||||||
reactCode
|
|
||||||
? `
|
|
||||||
<div class="code-preview__source code-preview__source--react" data-flavor="react">
|
|
||||||
<pre><code class="language-jsx">${escapeHtml(reactCode.textContent)}</code></pre>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="code-preview__buttons">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="code-preview__button code-preview__toggle"
|
|
||||||
aria-expanded="${isExpanded ? 'true' : 'false'}"
|
|
||||||
aria-controls="${sourceGroupId}"
|
|
||||||
>
|
|
||||||
Source
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
${reactCode ? ` ${htmlButton} ${reactButton} ` : ''}
|
|
||||||
|
|
||||||
${noCodePen ? '' : codePenButton}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
pre.insertAdjacentHTML('afterend', codePreview);
|
|
||||||
pre.remove();
|
|
||||||
|
|
||||||
if (adjacentPre) {
|
|
||||||
adjacentPre.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wrap code preview scripts in anonymous functions so they don't run in the global scope
|
|
||||||
doc.querySelectorAll('.code-preview__preview script').forEach(script => {
|
|
||||||
if (script.type === 'module') {
|
|
||||||
// Modules are already scoped
|
|
||||||
script.textContent = script.innerHTML;
|
|
||||||
} else {
|
|
||||||
// Wrap non-modules in an anonymous function so they don't run in the global scope
|
|
||||||
script.textContent = `(() => { ${script.innerHTML} })();`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
let count = 1;
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns code fields with the :preview suffix into interactive code previews.
|
||||||
|
*/
|
||||||
|
export function codePreviewsPlugin(options) {
|
||||||
|
options = {
|
||||||
|
within: 'body', // the element containing the code fields to convert
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('code-previews', content => {
|
||||||
|
const doc = parse(content);
|
||||||
|
const within = doc.querySelector(options.within);
|
||||||
|
|
||||||
|
if (!within) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
within.querySelectorAll('code[class]').forEach(code => {
|
||||||
|
if (!code.outerHTML.includes('nowrap')) {
|
||||||
|
console.log(code.outerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code.getAttribute('class').includes('preview')) {
|
||||||
|
//
|
||||||
|
// TODO - why aren't code blocks getting picked up?
|
||||||
|
//
|
||||||
|
}
|
||||||
|
const pre = code.closest('pre');
|
||||||
|
|
||||||
|
if (!pre || !code.getAttribute('class').includes(':preview')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const adjacentPre = pre.nextElementSibling?.tagName.toLowerCase() === 'pre' ? pre.nextElementSibling : null;
|
||||||
|
const reactCode = adjacentPre?.querySelector('code[class$="react"]');
|
||||||
|
const sourceGroupId = `code-preview-source-group-${count}`;
|
||||||
|
const isExpanded = code.getAttribute('class').includes(':expanded');
|
||||||
|
const noCodePen = code.getAttribute('class').includes(':no-codepen');
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
const htmlButton = `
|
||||||
|
<button type="button"
|
||||||
|
title="Show HTML code"
|
||||||
|
class="code-preview__button code-preview__button--html"
|
||||||
|
>
|
||||||
|
HTML
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const reactButton = `
|
||||||
|
<button type="button" title="Show React code" class="code-preview__button code-preview__button--react">
|
||||||
|
React
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const codePenButton = `
|
||||||
|
<button type="button" class="code-preview__button code-preview__button--codepen" title="Edit on CodePen">
|
||||||
|
<svg
|
||||||
|
width="138"
|
||||||
|
height="26"
|
||||||
|
viewBox="0 0 138 26"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const codePreview = `
|
||||||
|
<div class="code-preview ${isExpanded ? 'code-preview--expanded' : ''}">
|
||||||
|
<div class="code-preview__preview">
|
||||||
|
${code.textContent}
|
||||||
|
<div class="code-preview__resizer">
|
||||||
|
<sl-icon name="grip-vertical"></sl-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-preview__source-group" id="${sourceGroupId}">
|
||||||
|
<div class="code-preview__source code-preview__source--html" ${reactCode ? 'data-flavor="html"' : ''}>
|
||||||
|
<pre><code class="language-html">${escapeHtml(code.textContent)}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${
|
||||||
|
reactCode
|
||||||
|
? `
|
||||||
|
<div class="code-preview__source code-preview__source--react" data-flavor="react">
|
||||||
|
<pre><code class="language-jsx">${escapeHtml(reactCode.textContent)}</code></pre>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-preview__buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="code-preview__button code-preview__toggle"
|
||||||
|
aria-expanded="${isExpanded ? 'true' : 'false'}"
|
||||||
|
aria-controls="${sourceGroupId}"
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
${reactCode ? ` ${htmlButton} ${reactButton} ` : ''}
|
||||||
|
|
||||||
|
${noCodePen ? '' : codePenButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
pre.insertAdjacentHTML('afterend', codePreview);
|
||||||
|
pre.remove();
|
||||||
|
|
||||||
|
if (adjacentPre) {
|
||||||
|
// adjacentPre.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wrap code preview scripts in anonymous functions so they don't run in the global scope
|
||||||
|
doc.querySelectorAll('.code-preview__preview script').forEach(script => {
|
||||||
|
if (script.type === 'module') {
|
||||||
|
// Modules are already scoped
|
||||||
|
script.textContent = script.innerHTML;
|
||||||
|
} else {
|
||||||
|
// Wrap non-modules in an anonymous function so they don't run in the global scope
|
||||||
|
script.textContent = `(() => { ${script.innerHTML} })();`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
let codeBlockId = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
|
|
||||||
* document will be returned with the appropriate DOM manipulations.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc) {
|
|
||||||
doc.querySelectorAll('pre > code').forEach(code => {
|
|
||||||
const pre = code.closest('pre');
|
|
||||||
const button = doc.createElement('sl-copy-button');
|
|
||||||
|
|
||||||
if (!code.id) {
|
|
||||||
code.id = `code-block-${++codeBlockId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.classList.add('copy-code-button');
|
|
||||||
button.setAttribute('from', code.id);
|
|
||||||
|
|
||||||
pre.append(button);
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
|
||||||
|
let codeBlockId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
|
||||||
|
* document will be returned with the appropriate DOM manipulations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function copyCodeButtonsPlugin() {
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('copy-code-buttons', content => {
|
||||||
|
const doc = parse(content);
|
||||||
|
|
||||||
|
doc.querySelectorAll('pre > code').forEach(code => {
|
||||||
|
const pre = code.closest('pre');
|
||||||
|
const button = parse('<sl-copy-button></sl-copy-button>').firstChild;
|
||||||
|
|
||||||
|
if (!code.id) {
|
||||||
|
code.id = `code-block-${++codeBlockId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.classList.add('copy-code-button');
|
||||||
|
button.setAttribute('from', code.id);
|
||||||
|
|
||||||
|
pre.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
const { isExternalLink } = require('./strings.cjs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms external links to make them safer and optionally add a target. The provided doc should be a document
|
|
||||||
* object provided by JSDOM. The same document will be returned with the appropriate DOM manipulations.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc, options) {
|
|
||||||
options = {
|
|
||||||
className: 'external-link', // the class name to add to links
|
|
||||||
noopener: true, // sets rel="noopener"
|
|
||||||
noreferrer: true, // sets rel="noreferrer"
|
|
||||||
ignore: () => false, // callback function to filter links that should be ignored
|
|
||||||
within: 'body', // element that contains the target links
|
|
||||||
target: '', // sets the target attribute
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
const within = doc.querySelector(options.within);
|
|
||||||
|
|
||||||
if (within) {
|
|
||||||
within.querySelectorAll('a').forEach(link => {
|
|
||||||
if (isExternalLink(link) && !options.ignore(link)) {
|
|
||||||
link.classList.add(options.className);
|
|
||||||
|
|
||||||
const rel = [];
|
|
||||||
if (options.noopener) rel.push('noopener');
|
|
||||||
if (options.noreferrer) rel.push('noreferrer');
|
|
||||||
|
|
||||||
if (rel.length) {
|
|
||||||
link.setAttribute('rel', rel.join(' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.target) {
|
|
||||||
link.setAttribute('target', options.target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { isExternalLink } from './strings.js';
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms external links to make them safer and optionally add a target. The provided doc should be a document
|
||||||
|
* object provided by JSDOM. The same document will be returned with the appropriate DOM manipulations.
|
||||||
|
*/
|
||||||
|
export function externalLinksPlugin(options) {
|
||||||
|
options = {
|
||||||
|
className: 'external-link', // the class name to add to links
|
||||||
|
noopener: true, // sets rel="noopener"
|
||||||
|
noreferrer: true, // sets rel="noreferrer"
|
||||||
|
ignore: () => false, // callback function to filter links that should be ignored
|
||||||
|
within: 'body', // element that contains the target links
|
||||||
|
target: '', // sets the target attribute
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('external-links', content => {
|
||||||
|
const doc = parse(content);
|
||||||
|
const within = doc.querySelector(options.within);
|
||||||
|
|
||||||
|
if (within) {
|
||||||
|
within.querySelectorAll('a').forEach(link => {
|
||||||
|
if (isExternalLink(link) && !options.ignore(link)) {
|
||||||
|
link.classList.add(options.className);
|
||||||
|
|
||||||
|
const rel = [];
|
||||||
|
if (options.noopener) rel.push('noopener');
|
||||||
|
if (options.noreferrer) rel.push('noreferrer');
|
||||||
|
|
||||||
|
if (rel.length) {
|
||||||
|
link.setAttribute('rel', rel.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.target) {
|
||||||
|
link.setAttribute('target', options.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,63 +0,0 @@
|
||||||
const Prism = require('prismjs');
|
|
||||||
const PrismLoader = require('prismjs/components/index.js');
|
|
||||||
|
|
||||||
PrismLoader('diff');
|
|
||||||
PrismLoader.silent = true;
|
|
||||||
|
|
||||||
/** Highlights a code string. */
|
|
||||||
function highlight(code, language) {
|
|
||||||
const alias = language.replace(/^diff-/, '');
|
|
||||||
const isDiff = /^diff-/i.test(language);
|
|
||||||
|
|
||||||
// Auto-load the target language
|
|
||||||
if (!Prism.languages[alias]) {
|
|
||||||
PrismLoader(alias);
|
|
||||||
|
|
||||||
if (!Prism.languages[alias]) {
|
|
||||||
throw new Error(`Unsupported language for code highlighting: "${language}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register diff-* languages to use the diff grammar
|
|
||||||
if (isDiff) {
|
|
||||||
Prism.languages[language] = Prism.languages.diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Prism.highlight(code, Prism.languages[language], language);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights all code fields that have a language parameter. If the language has a colon in its name, the first chunk
|
|
||||||
* will be the language used and additional chunks will be applied as classes to the `<pre>`. For example, a code field
|
|
||||||
* tagged with "html:preview" will be rendered as `<pre class="language-html preview">`.
|
|
||||||
*
|
|
||||||
* The provided doc should be a document object provided by JSDOM. The same document will be returned with the
|
|
||||||
* appropriate DOM manipulations.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc) {
|
|
||||||
doc.querySelectorAll('pre > code[class]').forEach(code => {
|
|
||||||
// Look for class="language-*" and split colons into separate classes
|
|
||||||
code.classList.forEach(className => {
|
|
||||||
if (className.startsWith('language-')) {
|
|
||||||
//
|
|
||||||
// We use certain suffixes to indicate code previews, expanded states, etc. The class might look something like
|
|
||||||
// this:
|
|
||||||
//
|
|
||||||
// class="language-html:preview:expanded"
|
|
||||||
//
|
|
||||||
// The language will always come first, so we need to drop the "language-" prefix and everything after the first
|
|
||||||
// color to get the highlighter language.
|
|
||||||
//
|
|
||||||
const language = className.replace(/^language-/, '').split(':')[0];
|
|
||||||
|
|
||||||
try {
|
|
||||||
code.innerHTML = highlight(code.textContent ?? '', language);
|
|
||||||
} catch (err) {
|
|
||||||
// Language not found, skip it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
import Prism from 'prismjs';
|
||||||
|
import PrismLoader from 'prismjs/components/index.js';
|
||||||
|
|
||||||
|
PrismLoader('diff');
|
||||||
|
PrismLoader.silent = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights a string of code using the specified language.
|
||||||
|
*
|
||||||
|
* @param {string} code - The code to highlight.
|
||||||
|
* @param {string} language - The language the code is written in. For available languages, refer to this page:
|
||||||
|
* https://prismjs.com/#supported-languages
|
||||||
|
*/
|
||||||
|
export function highlightCode(code, language = 'plain') {
|
||||||
|
const alias = language.replace(/^diff-/, '');
|
||||||
|
const isDiff = /^diff-/i.test(language);
|
||||||
|
|
||||||
|
if (!Prism.languages[alias]) {
|
||||||
|
PrismLoader(alias);
|
||||||
|
if (!Prism.languages[alias]) {
|
||||||
|
throw new Error(`Unsupported language for code highlighting: "${language}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDiff) {
|
||||||
|
Prism.languages[language] = Prism.languages.diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Prism.highlight(code, Prism.languages[language], language);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eleventy plugin to highlight code blocks with the `language-*` attribute using Prism.js. Unlike most plugins, this
|
||||||
|
* works on the entire document — not just markdown content.
|
||||||
|
*/
|
||||||
|
export function highlightCodePlugin(options = {}) {
|
||||||
|
options = {
|
||||||
|
container: 'body',
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('highlight-code', content => {
|
||||||
|
const doc = parse(content, { blockTextElements: { code: true } });
|
||||||
|
const container = doc.querySelector(options.container);
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for <code class="language-*"> and highlight each one
|
||||||
|
container.querySelectorAll('code[class*="language-"]').forEach(code => {
|
||||||
|
const langClass = [...code.classList.values()].find(val => val.startsWith('language-'));
|
||||||
|
const lang = langClass ? langClass.replace(/^language-/, '').split(':')[0] : 'plain';
|
||||||
|
|
||||||
|
try {
|
||||||
|
code.innerHTML = highlightCode(code.textContent ?? '', lang);
|
||||||
|
} catch (err) {
|
||||||
|
if (!options.ignoreMissingLangs) {
|
||||||
|
throw new Error(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
const MarkdownIt = require('markdown-it');
|
import MarkdownIt from 'markdown-it';
|
||||||
const markdownItContainer = require('markdown-it-container');
|
import markdownItContainer from 'markdown-it-container';
|
||||||
const markdownItIns = require('markdown-it-ins');
|
import markdownItIns from 'markdown-it-ins';
|
||||||
const markdownItKbd = require('markdown-it-kbd');
|
import markdownItKbd from 'markdown-it-kbd';
|
||||||
const markdownItMark = require('markdown-it-mark');
|
import markdownItMark from 'markdown-it-mark';
|
||||||
const markdownItReplaceIt = require('markdown-it-replace-it');
|
import markdownItReplaceIt from 'markdown-it-replace-it';
|
||||||
|
|
||||||
const markdown = MarkdownIt({
|
const markdown = MarkdownIt({
|
||||||
html: true,
|
html: true,
|
||||||
|
@ -64,4 +64,4 @@ markdownItReplaceIt.replacements.push({
|
||||||
default: true
|
default: true
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = markdown;
|
export default markdown;
|
|
@ -1,26 +0,0 @@
|
||||||
const { format } = require('prettier');
|
|
||||||
|
|
||||||
/** Formats markup using prettier. */
|
|
||||||
module.exports = function (content, options) {
|
|
||||||
options = {
|
|
||||||
arrowParens: 'avoid',
|
|
||||||
bracketSpacing: true,
|
|
||||||
htmlWhitespaceSensitivity: 'css',
|
|
||||||
insertPragma: false,
|
|
||||||
bracketSameLine: false,
|
|
||||||
jsxSingleQuote: false,
|
|
||||||
parser: 'html',
|
|
||||||
printWidth: 120,
|
|
||||||
proseWrap: 'preserve',
|
|
||||||
quoteProps: 'as-needed',
|
|
||||||
requirePragma: false,
|
|
||||||
semi: true,
|
|
||||||
singleQuote: true,
|
|
||||||
tabWidth: 2,
|
|
||||||
trailingComma: 'none',
|
|
||||||
useTabs: false,
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
return format(content, options);
|
|
||||||
};
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { format } from 'prettier';
|
||||||
|
|
||||||
|
/** Formats markup using prettier. */
|
||||||
|
export function prettierPlugin(options) {
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('external-links', async content => {
|
||||||
|
options = {
|
||||||
|
arrowParens: 'avoid',
|
||||||
|
bracketSpacing: true,
|
||||||
|
htmlWhitespaceSensitivity: 'css',
|
||||||
|
insertPragma: false,
|
||||||
|
bracketSameLine: false,
|
||||||
|
jsxSingleQuote: false,
|
||||||
|
parser: 'html',
|
||||||
|
printWidth: 120,
|
||||||
|
proseWrap: 'preserve',
|
||||||
|
quoteProps: 'as-needed',
|
||||||
|
requirePragma: false,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
tabWidth: 2,
|
||||||
|
trailingComma: 'none',
|
||||||
|
useTabs: false,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
return await format(content, options);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
/**
|
|
||||||
* @typedef {object} Replacement
|
|
||||||
* @property {string | RegExp} pattern
|
|
||||||
* @property {string} replacement
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Array<Replacement>} Replacements
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Document} content
|
|
||||||
* @param {Replacements} replacements
|
|
||||||
*/
|
|
||||||
module.exports = function (content, replacements) {
|
|
||||||
replacements.forEach(replacement => {
|
|
||||||
content.body.innerHTML = content.body.innerHTML.replaceAll(replacement.pattern, replacement.replacement);
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* @typedef {object} Replacement
|
||||||
|
* @property {string | RegExp} pattern
|
||||||
|
* @property {string} replacement
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Array<Replacement>} Replacements
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Document} content
|
||||||
|
* @param {Replacements} replacements
|
||||||
|
*/
|
||||||
|
export function replacerPlugin(replacements) {
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('replacer', content => {
|
||||||
|
replacements.forEach(replacement => {
|
||||||
|
content = content.replaceAll(replacement.pattern, replacement.replacement);
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
/**
|
|
||||||
* Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM.
|
|
||||||
* The same document will be returned with the appropriate DOM manipulations.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc, options) {
|
|
||||||
const tables = [...doc.querySelectorAll('table')];
|
|
||||||
|
|
||||||
options = {
|
|
||||||
className: 'table-scroll', // the class name to add to the table's container
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
tables.forEach(table => {
|
|
||||||
const div = doc.createElement('div');
|
|
||||||
div.classList.add(options.className);
|
|
||||||
table.insertAdjacentElement('beforebegin', div);
|
|
||||||
div.append(table);
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM.
|
||||||
|
* The same document will be returned with the appropriate DOM manipulations.
|
||||||
|
*/
|
||||||
|
export function scrollingTablesPlugin(options) {
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('scrolling-tables', content => {
|
||||||
|
const doc = parse(content);
|
||||||
|
const tables = [...doc.querySelectorAll('table')];
|
||||||
|
|
||||||
|
options = {
|
||||||
|
className: 'table-scroll', // the class name to add to the table's container
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
tables.forEach(table => {
|
||||||
|
const div = parse('<div></div>').firstChild;
|
||||||
|
div.classList.add(options.className);
|
||||||
|
table.insertAdjacentHTML('beforebegin', div);
|
||||||
|
div.appendChild(table);
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
const slugify = require('slugify');
|
import slugify from 'slugify';
|
||||||
|
|
||||||
/** Creates a slug from an arbitrary string of text. */
|
/** Creates a slug from an arbitrary string of text. */
|
||||||
module.exports.createSlug = function (text) {
|
export function createSlug(text) {
|
||||||
return slugify(String(text), {
|
return slugify(String(text), {
|
||||||
remove: /[^\w|\s]/g,
|
remove: /[^\w|\s]/g,
|
||||||
lower: true
|
lower: true
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/** Determines whether or not a link is external. */
|
/** Determines whether or not a link is external. */
|
||||||
module.exports.isExternalLink = function (link) {
|
export function isExternalLink(link) {
|
||||||
// We use the "internal" hostname when initializing JSDOM so we know that those are local links
|
// We use the "internal" hostname when initializing JSDOM so we know that those are local links
|
||||||
if (!link.hostname || link.hostname === 'internal') return false;
|
if (!link.hostname) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
}
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* Generates an in-page table of contents based on headings.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc, options) {
|
|
||||||
options = {
|
|
||||||
levels: ['h2'], // headings to include (they must have an id)
|
|
||||||
container: 'nav', // the container to append links to
|
|
||||||
listItem: true, // if true, links will be wrapped in <li>
|
|
||||||
within: 'body', // the element containing the headings to summarize
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
const container = doc.querySelector(options.container);
|
|
||||||
const within = doc.querySelector(options.within);
|
|
||||||
const headingSelector = options.levels.map(h => `${h}[id]`).join(', ');
|
|
||||||
|
|
||||||
if (!container || !within) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
within.querySelectorAll(headingSelector).forEach(heading => {
|
|
||||||
const listItem = doc.createElement('li');
|
|
||||||
const link = doc.createElement('a');
|
|
||||||
const level = heading.tagName.slice(1);
|
|
||||||
|
|
||||||
link.href = `#${heading.id}`;
|
|
||||||
link.textContent = heading.textContent;
|
|
||||||
|
|
||||||
if (options.listItem) {
|
|
||||||
// List item + link
|
|
||||||
listItem.setAttribute('data-level', level);
|
|
||||||
listItem.append(link);
|
|
||||||
container.append(listItem);
|
|
||||||
} else {
|
|
||||||
// Link only
|
|
||||||
link.setAttribute('data-level', level);
|
|
||||||
container.append(link);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an in-page table of contents based on headings.
|
||||||
|
*/
|
||||||
|
export function tableOfContentsPlugin(options) {
|
||||||
|
options = {
|
||||||
|
levels: ['h2'], // headings to include (they must have an id)
|
||||||
|
container: 'nav', // the container to append links to
|
||||||
|
listItem: true, // if true, links will be wrapped in <li>
|
||||||
|
within: 'body', // the element containing the headings to summarize
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
return function (eleventyConfig) {
|
||||||
|
eleventyConfig.addTransform('table-of-contents', content => {
|
||||||
|
const doc = parse(content);
|
||||||
|
const container = doc.querySelector(options.container);
|
||||||
|
const within = doc.querySelector(options.within);
|
||||||
|
const headingSelector = options.levels.map(h => `${h}[id]`).join(', ');
|
||||||
|
|
||||||
|
if (!container || !within) {
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
within.querySelectorAll(headingSelector).forEach(heading => {
|
||||||
|
const listItem = parse('<li></li>').firstChild;
|
||||||
|
const link = parse('<a></a>').firstChild;
|
||||||
|
const level = heading.tagName.slice(1);
|
||||||
|
|
||||||
|
link.setAttribute('href', `#${heading.id}`);
|
||||||
|
link.textContent = heading.textContent;
|
||||||
|
|
||||||
|
if (options.listItem) {
|
||||||
|
// List item + link
|
||||||
|
listItem.setAttribute('data-level', level);
|
||||||
|
listItem.appendChild(link);
|
||||||
|
container.appendChild(listItem);
|
||||||
|
} else {
|
||||||
|
// Link only
|
||||||
|
link.setAttribute('data-level', level);
|
||||||
|
container.appendChild(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
const smartquotes = require('smartquotes');
|
|
||||||
|
|
||||||
smartquotes.replacements.push([/---/g, '\u2014']); // em dash
|
|
||||||
smartquotes.replacements.push([/--/g, '\u2013']); // en dash
|
|
||||||
smartquotes.replacements.push([/\.\.\./g, '\u2026']); // ellipsis
|
|
||||||
smartquotes.replacements.push([/\(c\)/gi, '\u00A9']); // copyright
|
|
||||||
smartquotes.replacements.push([/\(r\)/gi, '\u00AE']); // registered trademark
|
|
||||||
smartquotes.replacements.push([/\?!/g, '\u2048']); // ?!
|
|
||||||
smartquotes.replacements.push([/!!/g, '\u203C']); // !!
|
|
||||||
smartquotes.replacements.push([/\?\?/g, '\u2047']); // ??
|
|
||||||
smartquotes.replacements.push([/([0-9]\s?)-(\s?[0-9])/g, '$1\u2013$2']); // number ranges use en dash
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Improves typography by adding smart quotes and similar corrections within the specified element(s).
|
|
||||||
*
|
|
||||||
* The provided doc should be a document object provided by JSDOM. The same document will be returned with the
|
|
||||||
* appropriate DOM manipulations.
|
|
||||||
*/
|
|
||||||
module.exports = function (doc, selector = 'body') {
|
|
||||||
const elements = [...doc.querySelectorAll(selector)];
|
|
||||||
elements.forEach(el => smartquotes.element(el));
|
|
||||||
return doc;
|
|
||||||
};
|
|
|
@ -1,22 +1,21 @@
|
||||||
/* eslint-disable no-invalid-this */
|
/* eslint-disable no-invalid-this */
|
||||||
const fs = require('fs');
|
import { activeLinksPlugin } from './_utilities/active-links.js';
|
||||||
const path = require('path');
|
import { anchorHeadingsPlugin } from './_utilities/anchor-headings.js';
|
||||||
const lunr = require('lunr');
|
import { capitalCase } from 'change-case';
|
||||||
const { capitalCase } = require('change-case');
|
import { codePreviewsPlugin } from './_utilities/code-previews.js';
|
||||||
const { JSDOM } = require('jsdom');
|
import { copyCodeButtonsPlugin } from './_utilities/copy-code-buttons.js';
|
||||||
const { customElementsManifest, getAllComponents } = require('./_utilities/cem.cjs');
|
import { customElementsManifest, getAllComponents } from './_utilities/cem.js';
|
||||||
const shoelaceFlavoredMarkdown = require('./_utilities/markdown.cjs');
|
import { externalLinksPlugin } from './_utilities/external-links.js';
|
||||||
const activeLinks = require('./_utilities/active-links.cjs');
|
import { highlightCodePlugin } from './_utilities/highlight-code.js';
|
||||||
const anchorHeadings = require('./_utilities/anchor-headings.cjs');
|
import { parse } from 'node-html-parser';
|
||||||
const codePreviews = require('./_utilities/code-previews.cjs');
|
// import { prettierPlugin } from './_utilities/prettier.js';
|
||||||
const copyCodeButtons = require('./_utilities/copy-code-buttons.cjs');
|
import { replacerPlugin } from './_utilities/replacer.js';
|
||||||
const externalLinks = require('./_utilities/external-links.cjs');
|
import { scrollingTablesPlugin } from './_utilities/scrolling-tables.js';
|
||||||
const highlightCodeBlocks = require('./_utilities/highlight-code.cjs');
|
import { tableOfContentsPlugin } from './_utilities/table-of-contents.js';
|
||||||
const tableOfContents = require('./_utilities/table-of-contents.cjs');
|
import fs from 'fs';
|
||||||
const prettier = require('./_utilities/prettier.cjs');
|
import lunr from 'lunr';
|
||||||
const scrollingTables = require('./_utilities/scrolling-tables.cjs');
|
import path from 'path';
|
||||||
const typography = require('./_utilities/typography.cjs');
|
import shoelaceFlavoredMarkdown from './_utilities/markdown.js';
|
||||||
const replacer = require('./_utilities/replacer.cjs');
|
|
||||||
|
|
||||||
const assetsDir = 'assets';
|
const assetsDir = 'assets';
|
||||||
const cdndir = 'cdn';
|
const cdndir = 'cdn';
|
||||||
|
@ -24,7 +23,7 @@ const npmdir = 'dist';
|
||||||
const allComponents = getAllComponents();
|
const allComponents = getAllComponents();
|
||||||
let hasBuiltSearchIndex = false;
|
let hasBuiltSearchIndex = false;
|
||||||
|
|
||||||
module.exports = function (eleventyConfig) {
|
export default function (eleventyConfig) {
|
||||||
//
|
//
|
||||||
// Global data
|
// Global data
|
||||||
//
|
//
|
||||||
|
@ -113,47 +112,40 @@ module.exports = function (eleventyConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// Transforms
|
// Plugins
|
||||||
//
|
//
|
||||||
eleventyConfig.addTransform('html-transform', function (content) {
|
eleventyConfig.addPlugin(
|
||||||
// Parse the template and get a Document object
|
anchorHeadingsPlugin({
|
||||||
const doc = new JSDOM(content, {
|
container: '#content .content__body',
|
||||||
// We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily
|
selector: 'h2, h3, h4, h5'
|
||||||
// identify which ones are internal and which ones are external.
|
})
|
||||||
url: `https://internal/`
|
);
|
||||||
}).window.document;
|
eleventyConfig.addPlugin(activeLinksPlugin());
|
||||||
|
eleventyConfig.addPlugin(codePreviewsPlugin());
|
||||||
// DOM transforms
|
eleventyConfig.addPlugin(externalLinksPlugin());
|
||||||
activeLinks(doc, { pathname: this.page.url });
|
// eleventyConfig.addPlugin(highlightCodePlugin());
|
||||||
anchorHeadings(doc, {
|
eleventyConfig.addPlugin(
|
||||||
within: '#content .content__body',
|
tableOfContentsPlugin({
|
||||||
levels: ['h2', 'h3', 'h4', 'h5']
|
|
||||||
});
|
|
||||||
tableOfContents(doc, {
|
|
||||||
levels: ['h2', 'h3'],
|
levels: ['h2', 'h3'],
|
||||||
container: '#content .content__toc > ul',
|
container: '#content .content__toc > ul',
|
||||||
within: '#content .content__body'
|
within: '#content .content__body'
|
||||||
});
|
})
|
||||||
codePreviews(doc);
|
);
|
||||||
externalLinks(doc, { target: '_blank' });
|
eleventyConfig.addPlugin(scrollingTablesPlugin());
|
||||||
highlightCodeBlocks(doc);
|
eleventyConfig.addPlugin(copyCodeButtonsPlugin()); // must be after codePreviews + highlightCodeBlocks
|
||||||
scrollingTables(doc);
|
eleventyConfig.addPlugin(
|
||||||
copyCodeButtons(doc); // must be after codePreviews + highlightCodeBlocks
|
replacerPlugin([
|
||||||
typography(doc, '#content');
|
|
||||||
replacer(doc, [
|
|
||||||
{ pattern: '%VERSION%', replacement: customElementsManifest.package.version },
|
{ pattern: '%VERSION%', replacement: customElementsManifest.package.version },
|
||||||
{ pattern: '%CDNDIR%', replacement: cdndir },
|
{ pattern: '%CDNDIR%', replacement: cdndir },
|
||||||
{ pattern: '%NPMDIR%', replacement: npmdir }
|
{ pattern: '%NPMDIR%', replacement: npmdir }
|
||||||
]);
|
])
|
||||||
|
);
|
||||||
|
|
||||||
// Serialize the Document object to an HTML string and prepend the doctype
|
//
|
||||||
content = `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
// TODO - only run this at build
|
||||||
|
//
|
||||||
// String transforms
|
// eleventyConfig.addPlugin(prettierPlugin());
|
||||||
content = prettier(content);
|
//
|
||||||
|
|
||||||
return content;
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Build a search index
|
// Build a search index
|
||||||
|
@ -181,12 +173,9 @@ module.exports = function (eleventyConfig) {
|
||||||
.join('/', path.relative(eleventyConfig.dir.output, result.outputPath))
|
.join('/', path.relative(eleventyConfig.dir.output, result.outputPath))
|
||||||
.replace(/\\/g, '/') // convert backslashes to forward slashes
|
.replace(/\\/g, '/') // convert backslashes to forward slashes
|
||||||
.replace(/\/index.html$/, '/'); // convert trailing /index.html to /
|
.replace(/\/index.html$/, '/'); // convert trailing /index.html to /
|
||||||
const doc = new JSDOM(result.content, {
|
const doc = parse(result.content);
|
||||||
// We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily
|
|
||||||
// identify which ones are internal and which ones are external.
|
|
||||||
url: `https://internal/`
|
|
||||||
}).window.document;
|
|
||||||
const content = doc.querySelector('#content');
|
const content = doc.querySelector('#content');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
// Get title and headings
|
// Get title and headings
|
||||||
const title = (doc.querySelector('title')?.textContent || path.basename(result.outputPath)).trim();
|
const title = (doc.querySelector('title')?.textContent || path.basename(result.outputPath)).trim();
|
||||||
|
@ -242,4 +231,4 @@ module.exports = function (eleventyConfig) {
|
||||||
markdownTemplateEngine: 'njk', // use Nunjucks instead of Liquid for markdown files
|
markdownTemplateEngine: 'njk', // use Nunjucks instead of Liquid for markdown files
|
||||||
templateEngineOverride: ['njk'] // just Nunjucks and then markdown
|
templateEngineOverride: ['njk'] // just Nunjucks and then markdown
|
||||||
};
|
};
|
||||||
};
|
}
|
Plik diff jest za duży
Load Diff
|
@ -77,7 +77,7 @@
|
||||||
"qr-creator": "^1.0.0"
|
"qr-creator": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@11ty/eleventy": "^2.0.1",
|
"@11ty/eleventy": "3.0.0-alpha.5",
|
||||||
"@custom-elements-manifest/analyzer": "^0.8.4",
|
"@custom-elements-manifest/analyzer": "^0.8.4",
|
||||||
"@open-wc/testing": "^3.2.0",
|
"@open-wc/testing": "^3.2.0",
|
||||||
"@types/mocha": "^10.0.2",
|
"@types/mocha": "^10.0.2",
|
||||||
|
@ -124,6 +124,7 @@
|
||||||
"markdown-it-kbd": "^2.2.2",
|
"markdown-it-kbd": "^2.2.2",
|
||||||
"markdown-it-mark": "^3.0.1",
|
"markdown-it-mark": "^3.0.1",
|
||||||
"markdown-it-replace-it": "^1.0.0",
|
"markdown-it-replace-it": "^1.0.0",
|
||||||
|
"node-html-parser": "^6.1.12",
|
||||||
"npm-check-updates": "^16.14.6",
|
"npm-check-updates": "^16.14.6",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"pascal-case": "^3.1.2",
|
"pascal-case": "^3.1.2",
|
||||||
|
@ -133,12 +134,13 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"recursive-copy": "^2.0.14",
|
"recursive-copy": "^2.0.14",
|
||||||
"sinon": "^16.1.0",
|
"sinon": "^16.1.0",
|
||||||
"smartquotes": "^2.3.2",
|
"slugify": "^1.6.6",
|
||||||
"source-map": "^0.7.4",
|
"source-map": "^0.7.4",
|
||||||
"strip-css-comments": "^5.0.0",
|
"strip-css-comments": "^5.0.0",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"user-agent-data-types": "^0.3.1"
|
"user-agent-data-types": "^0.3.1",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,js}": [
|
"*.{ts,js}": [
|
||||||
|
|
Ładowanie…
Reference in New Issue