mirror of
https://github.com/modernweb-dev/rocket.git
synced 2026-03-21 15:54:57 +00:00
Compare commits
1 Commits
@mdjs/core
...
feat/web-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0166dc6f04 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,3 +40,4 @@ _merged_assets
|
||||
_merged_includes
|
||||
__output
|
||||
__output-dev
|
||||
docs_backup
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
---
|
||||
layout: layout-404
|
||||
permalink: 404.html
|
||||
menu:
|
||||
exclude: true
|
||||
---
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
:not(rocket-navigation):not(:defined) {
|
||||
:not(web-menu):not(:defined) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
rocket-navigation,
|
||||
web-menu,
|
||||
header {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
3
docs/blog/blog.11tydata.cjs
Normal file
3
docs/blog/blog.11tydata.cjs
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
layout: 'layout-blog-details',
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
Content-Security-Policy = "default-src 'self'; script-src 'self' www.googletagmanager.com 'sha256-W6Gq+BvrdAAMbF8E7WHA7UPQxuUOfJM8E9mpKD0oihA=' 'sha256-vFU+IJ5dUUukI5Varwy49dN2d89DmFj7UNewqQv88sw='; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src 'self' data: fonts.gstatic.com;"
|
||||
@@ -29,6 +29,7 @@
|
||||
"postinstall": "npm run setup",
|
||||
"release": "changeset publish && yarn format",
|
||||
"rocket:build": "node packages/cli/src/cli.js build",
|
||||
"rocket:upgrade": "node packages/cli/src/cli.js upgrade",
|
||||
"search": "node packages/cli/src/cli.js search",
|
||||
"setup": "npm run setup:ts-configs && npm run build:packages",
|
||||
"setup:patches": "npx patch-package",
|
||||
@@ -36,7 +37,7 @@
|
||||
"xprestart": "yarn analyze",
|
||||
"start": "node --trace-warnings packages/cli/src/cli.js start",
|
||||
"test": "yarn test:node && yarn test:web",
|
||||
"test:node": "mocha \"packages/*/test-node/**/*.test.{ts,js,mjs,cjs}\" --timeout 5000 --reporter dot --exit",
|
||||
"test:node": "mocha \"packages/*/test-node/**/*.test.{ts,js,mjs,cjs}\" -- --timeout 5000 --reporter dot --exit",
|
||||
"test:web": "web-test-runner",
|
||||
"types": "run-s types:clear types:copy types:build",
|
||||
"types:build": "tsc --build",
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<rocket-navigation>
|
||||
<ul>
|
||||
<li class="current">
|
||||
<h3>Headings</h3>
|
||||
{{ collections[section] | rocketPageAnchors({ title: title }) | rocketNavToHtml({
|
||||
listItemClass: "menu-item",
|
||||
activeListItemClass: "current",
|
||||
activeKey: eleventyNavigation.key
|
||||
}) | safe }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-tags">
|
||||
<h3>Date</h3>
|
||||
<div>{{ page.date.toDateString() }}</div>
|
||||
</div>
|
||||
<div class="sidebar-tags">
|
||||
<h3>Tags</h3>
|
||||
<div class="tags">
|
||||
{% for tag in tags %}
|
||||
<span class="tag">{{tag}}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/mobile-sidebar-bottom.njk' %}
|
||||
</rocket-navigation>
|
||||
@@ -1,19 +1 @@
|
||||
<div class="articles">
|
||||
{% for post in posts %}
|
||||
{% if post.data.published %}
|
||||
<article>
|
||||
{% if post.data.cover_image %}
|
||||
<a href="{{ post.url | url }}" class="thumbnail" style="background-image: url({{ post.data.cover_image | url }});">
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="content">
|
||||
<h2>
|
||||
<a href="{{ post.url | url }}">{{ post.data.title }}</a>
|
||||
</h2>
|
||||
<p>{{ post.data.description }}</p>
|
||||
<a class="read" href="{{ post.url | url }}">...read more</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<web-menu name="article-overview"></web-menu>
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { addPlugin } from 'plugins-manager';
|
||||
// import { addPlugin } from 'plugins-manager';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SECTION = 'blog';
|
||||
const POST_COLLECTION = 'posts';
|
||||
// const SECTION = 'blog';
|
||||
// const POST_COLLECTION = 'posts';
|
||||
|
||||
export function rocketBlog({ section = SECTION, postCollection = POST_COLLECTION } = {}) {
|
||||
const isHiddenCollection = item => ['-', '_'].includes(item.charAt(0));
|
||||
const isVisibleCollection = item => !isHiddenCollection(item);
|
||||
const isNotPostCollection = collection => collection !== postCollection;
|
||||
export function rocketBlog() {
|
||||
// const isHiddenCollection = item => ['-', '_'].includes(item.charAt(0));
|
||||
// const isVisibleCollection = item => !isHiddenCollection(item);
|
||||
// const isNotPostCollection = collection => collection !== postCollection;
|
||||
|
||||
const eleventyPluginRocketBlog = {
|
||||
configFunction: eleventyConfig => {
|
||||
eleventyConfig.addCollection('posts', collection => {
|
||||
/*
|
||||
// It's not working beacuse it's a paginated collection.
|
||||
const headerDocs = eleventyConfig.collections.header(collection);
|
||||
headerDocs.filter(page => page.data.section === section).forEach(page => {
|
||||
page.data.layout = 'blog';
|
||||
});
|
||||
*/
|
||||
if (section === postCollection) {
|
||||
throw new Error("Rocket blog: section and postCollection couldn't be equal");
|
||||
}
|
||||
if (!eleventyConfig.collections[section]) {
|
||||
const collectionKeys = Object.keys(eleventyConfig.collections);
|
||||
const availableCollections = collectionKeys
|
||||
.filter(isVisibleCollection)
|
||||
.filter(isNotPostCollection);
|
||||
throw new Error(
|
||||
`Rocket blog: Collection '${section}' not found. Aviable colections: ${availableCollections.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
// const eleventyPluginRocketBlog = {
|
||||
// configFunction: eleventyConfig => {
|
||||
// eleventyConfig.addCollection('posts', collection => {
|
||||
// /*
|
||||
// // It's not working beacuse it's a paginated collection.
|
||||
// const headerDocs = eleventyConfig.collections.header(collection);
|
||||
// headerDocs.filter(page => page.data.section === section).forEach(page => {
|
||||
// page.data.layout = 'blog';
|
||||
// });
|
||||
// */
|
||||
// if (section === postCollection) {
|
||||
// throw new Error("Rocket blog: section and postCollection couldn't be equal");
|
||||
// }
|
||||
// if (!eleventyConfig.collections[section]) {
|
||||
// const collectionKeys = Object.keys(eleventyConfig.collections);
|
||||
// const availableCollections = collectionKeys
|
||||
// .filter(isVisibleCollection)
|
||||
// .filter(isNotPostCollection);
|
||||
// throw new Error(
|
||||
// `Rocket blog: Collection '${section}' not found. Aviable colections: ${availableCollections.join(
|
||||
// ', ',
|
||||
// )}`,
|
||||
// );
|
||||
// }
|
||||
|
||||
const posts = eleventyConfig.collections[section](collection);
|
||||
posts.forEach(page => {
|
||||
page.data.layout = 'layout-blog-details';
|
||||
});
|
||||
return posts;
|
||||
});
|
||||
},
|
||||
};
|
||||
// const posts = eleventyConfig.collections[section](collection);
|
||||
// posts.forEach(page => {
|
||||
// page.data.layout = 'layout-blog-details';
|
||||
// });
|
||||
// return posts;
|
||||
// });
|
||||
// },
|
||||
// };
|
||||
|
||||
return {
|
||||
path: path.resolve(__dirname),
|
||||
setupEleventyPlugins: [addPlugin(eleventyPluginRocketBlog)],
|
||||
// setupEleventyPlugins: [addPlugin({ name: 'rocket-blog', plugin: eleventyPluginRocketBlog })],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
# @rocket/cli
|
||||
|
||||
## 0.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 15a82c0: Enable including script files into the simulator via `<script src=".." mdjs-use>`
|
||||
- 15a82c0: Allow only a limited set of characters for simulator includes `[a-zA-Z0-9\/\-_]`.
|
||||
Notably, there is no:
|
||||
|
||||
- `:` to prevent `http://...` includes
|
||||
- `.` so filenames as `this.is.my.js` are not supported. Also includes will be without file endings which will be added automatically
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -2,9 +2,6 @@ const { setComputedConfig, getComputedConfig } = require('./src/public/computedC
|
||||
const {
|
||||
generateEleventyComputed,
|
||||
LayoutPlugin,
|
||||
TitleMetaPlugin,
|
||||
TitlePlugin,
|
||||
EleventyNavigationPlugin,
|
||||
SectionPlugin,
|
||||
SocialMediaImagePlugin,
|
||||
JoiningBlocksPlugin,
|
||||
@@ -16,9 +13,6 @@ module.exports = {
|
||||
getComputedConfig,
|
||||
generateEleventyComputed,
|
||||
LayoutPlugin,
|
||||
TitleMetaPlugin,
|
||||
TitlePlugin,
|
||||
EleventyNavigationPlugin,
|
||||
SectionPlugin,
|
||||
SocialMediaImagePlugin,
|
||||
JoiningBlocksPlugin,
|
||||
|
||||
@@ -4,9 +4,6 @@ export { setComputedConfig, getComputedConfig } from './src/public/computedConfi
|
||||
export {
|
||||
generateEleventyComputed,
|
||||
LayoutPlugin,
|
||||
TitleMetaPlugin,
|
||||
TitlePlugin,
|
||||
EleventyNavigationPlugin,
|
||||
SectionPlugin,
|
||||
SocialMediaImagePlugin,
|
||||
JoiningBlocksPlugin,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/cli",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -60,7 +60,6 @@
|
||||
"@rocket/building-rollup": "^0.4.0",
|
||||
"@rocket/core": "^0.1.2",
|
||||
"@rocket/eleventy-plugin-mdjs-unified": "^0.6.0",
|
||||
"@rocket/eleventy-rocket-nav": "^0.3.0",
|
||||
"@rollup/plugin-babel": "^5.2.2",
|
||||
"@rollup/plugin-node-resolve": "^11.0.1",
|
||||
"@web/config-loader": "^0.1.3",
|
||||
@@ -71,6 +70,7 @@
|
||||
"command-line-args": "^5.1.1",
|
||||
"command-line-usage": "^6.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"micromatch": "^4.0.2",
|
||||
"plugins-manager": "^0.3.0",
|
||||
"slash": "^3.0.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<a class="logo-link" href="{{ '/' | url }}">
|
||||
<img src="{{ '/_assets/logo.svg' | asset | url }}" alt="{{ site.logoAlt }}" />
|
||||
<span class="sr-only">{{ site.name }}</span>
|
||||
<span>{{ site.name }}</span>
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<html theme="light" platform="web" lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="menu:exclude" content="true">
|
||||
<meta charset="utf-8">
|
||||
<style type="text/css">
|
||||
body {
|
||||
@@ -14,37 +15,19 @@
|
||||
<script type="module">
|
||||
import { render } from '@mdjs/mdjs-story';
|
||||
|
||||
function sanitize(input, type) {
|
||||
return `${document.location.origin}/${input.match(/[a-zA-Z0-9\/\-_]*/)[0]}.${type}`;
|
||||
}
|
||||
|
||||
async function onHashChange() {
|
||||
const urlParts = new URLSearchParams(document.location.hash.substr(1));
|
||||
|
||||
if (urlParts.get('stylesheets')) {
|
||||
for (const stylesheet of urlParts.getAll('stylesheets')) {
|
||||
const safeStylesheetUrl = sanitize(stylesheet, 'css');
|
||||
if (!document.querySelector(`link[rel="stylesheet"][href="${safeStylesheetUrl}"]`)) {
|
||||
if (!document.querySelector(`link[rel="stylesheet"][href="${stylesheet}"]`)) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = safeStylesheetUrl;
|
||||
link.href = stylesheet;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParts.get('moduleUrls')) {
|
||||
for (const moduleUrl of urlParts.getAll('moduleUrls')) {
|
||||
const safeModuleUrl = sanitize(moduleUrl, 'js');
|
||||
if (!document.querySelector(`script[type=module][src="${safeModuleUrl}"]`)) {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = safeModuleUrl;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParts.get('theme')) {
|
||||
document.documentElement.setAttribute('theme', urlParts.get('theme'));
|
||||
}
|
||||
@@ -64,8 +47,7 @@
|
||||
document.documentElement.removeAttribute('edge-distance');
|
||||
}
|
||||
|
||||
const safeStoryUrl = sanitize(urlParts.get('story-file'), 'js');
|
||||
const mod = await import(safeStoryUrl);
|
||||
const mod = await import(urlParts.get('story-file'));
|
||||
render(mod[urlParts.get('story-key')]({ shadowRoot: document }), document.body);
|
||||
}
|
||||
|
||||
|
||||
187
packages/cli/src/RocketUpgrade.js
Executable file
187
packages/cli/src/RocketUpgrade.js
Executable file
@@ -0,0 +1,187 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
import { readdir, rename, writeFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { upgrade202109menu } from './upgrades/upgrade202109menu.js';
|
||||
import { copy } from 'fs-extra';
|
||||
|
||||
/** @typedef {import('../types/main').RocketCliOptions} RocketCliOptions */
|
||||
/** @typedef {import('../types/upgrade').UpgradeFile} UpgradeFile */
|
||||
/** @typedef {import('../types/upgrade').FolderRename} FolderRename */
|
||||
/** @typedef {import('../types/upgrade').upgrade} upgrade */
|
||||
|
||||
/**
|
||||
* @param {UpgradeFile} options
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function filterMerged({ relPath }) {
|
||||
return relPath.startsWith('_merged');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {string} options.rootDir
|
||||
* @param {string} options.currentDir
|
||||
* @param {(options: UpgradeFile) => Boolean} [options.filter]
|
||||
* @returns
|
||||
*/
|
||||
async function getAllFiles(options) {
|
||||
const { rootDir, currentDir, filter = filterMerged } = options;
|
||||
const entries = await readdir(currentDir, { withFileTypes: true });
|
||||
/** @type {UpgradeFile[]} */
|
||||
let files = [];
|
||||
for (const entry of entries) {
|
||||
const { name: folderName } = entry;
|
||||
const currentPath = path.join(currentDir, folderName);
|
||||
|
||||
if (entry.isFile()) {
|
||||
const relPath = path.relative(rootDir, currentPath);
|
||||
/** @type {UpgradeFile} */
|
||||
const data = {
|
||||
path: currentPath,
|
||||
relPath,
|
||||
name: path.basename(relPath),
|
||||
extName: path.extname(relPath),
|
||||
};
|
||||
if (!filter(data)) {
|
||||
files.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const { name: folderName } = entry;
|
||||
const currentPath = path.join(currentDir, folderName);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files = [...files, ...(await getAllFiles({ ...options, currentDir: currentPath }))];
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {upgrade} options
|
||||
*/
|
||||
async function updateFileSystem({ files, folderRenames }) {
|
||||
// rename files while not touching folders
|
||||
for (const file of files) {
|
||||
if (file.updatedName) {
|
||||
const newPath = path.join(path.dirname(file.path), file.updatedName);
|
||||
await rename(file.path, newPath);
|
||||
}
|
||||
}
|
||||
// rename folders
|
||||
for (const renameObj of folderRenames) {
|
||||
if (renameObj.fromAbsolute && renameObj.toAbsolute) {
|
||||
await rename(renameObj.fromAbsolute, renameObj.toAbsolute);
|
||||
}
|
||||
}
|
||||
// update file content
|
||||
for (const file of files) {
|
||||
if (file.updatedContent) {
|
||||
await writeFile(file.updatedPath || file.path, file.updatedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} relPath
|
||||
* @param {FolderRename[]} folderRenames
|
||||
* @returns {string}
|
||||
*/
|
||||
function applyFolderRenames(relPath, folderRenames) {
|
||||
let newRelPath = relPath;
|
||||
for (const renameObj of folderRenames) {
|
||||
if (newRelPath.startsWith(renameObj.from)) {
|
||||
newRelPath = renameObj.to + newRelPath.slice(renameObj.from.length);
|
||||
}
|
||||
}
|
||||
return newRelPath;
|
||||
}
|
||||
|
||||
export class RocketUpgrade {
|
||||
static pluginName = 'RocketUpgrade';
|
||||
commands = ['upgrade'];
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {RocketCliOptions} options.config
|
||||
* @param {any} options.argv
|
||||
*/
|
||||
async setup({ config, argv }) {
|
||||
this.__argv = argv;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async upgradeCommand() {
|
||||
if (!this?.config?._inputDirCwdRelative) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backupPath = path.join(this.config._inputDirCwdRelative, '..', 'docs_backup');
|
||||
await copy(this.config._inputDirCwdRelative, backupPath);
|
||||
console.log(`A backup of your docs folder has been created at ${backupPath}.`);
|
||||
|
||||
let files = await getAllFiles({
|
||||
rootDir: this.config._inputDirCwdRelative,
|
||||
currentDir: this.config._inputDirCwdRelative,
|
||||
});
|
||||
/** @type {FolderRename[]} */
|
||||
let folderRenames = [];
|
||||
|
||||
const upgrade = await upgrade202109menu({ files, folderRenames });
|
||||
files = upgrade.files;
|
||||
folderRenames = upgrade.folderRenames;
|
||||
|
||||
const orderedFolderRenames = [...folderRenames].sort((a, b) => {
|
||||
return b.from.split('/').length - a.from.split('/').length;
|
||||
});
|
||||
|
||||
// adjust relPath if there is a new filename
|
||||
let i = 0;
|
||||
for (const fileData of files) {
|
||||
if (fileData.updatedName) {
|
||||
files[i].updatedRelPath = `${path.dirname(fileData.relPath)}/${fileData.updatedName}`;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// adjust relPath to consider renamed folders
|
||||
i = 0;
|
||||
for (const fileData of files) {
|
||||
const modifiedPath = applyFolderRenames(
|
||||
fileData.updatedRelPath || fileData.relPath,
|
||||
orderedFolderRenames,
|
||||
);
|
||||
if (modifiedPath !== fileData.relPath) {
|
||||
files[i].updatedRelPath = modifiedPath;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// add an updatedPath if needed
|
||||
i = 0;
|
||||
for (const file of files) {
|
||||
if (file.updatedRelPath) {
|
||||
files[i].updatedPath = path.join(this.config._inputDirCwdRelative, file.updatedRelPath);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// create absolute paths for renames
|
||||
i = 0;
|
||||
for (const renameObj of folderRenames) {
|
||||
folderRenames[i].fromAbsolute = path.join(this.config._inputDirCwdRelative, renameObj.from);
|
||||
folderRenames[i].toAbsolute = path.join(this.config._inputDirCwdRelative, renameObj.to);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
await updateFileSystem({
|
||||
files,
|
||||
folderRenames: orderedFolderRenames,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ parser.prepareWasm(saxWasmBuffer);
|
||||
* @param {string} link
|
||||
*/
|
||||
function isRelativeLink(link) {
|
||||
if (link.startsWith('http') || link.startsWith('/')) {
|
||||
if (link.startsWith('http') || link.startsWith('/') || link.includes(':')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -14,8 +14,11 @@ import { readConfig } from '@web/config-loader';
|
||||
|
||||
import { RocketStart } from './RocketStart.js';
|
||||
import { RocketBuild } from './RocketBuild.js';
|
||||
import { RocketUpgrade } from './RocketUpgrade.js';
|
||||
import { RocketLint } from './RocketLint.js';
|
||||
|
||||
import { webMenu } from '@web/menu';
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
@@ -42,7 +45,7 @@ function ignore({ src }) {
|
||||
*/
|
||||
export async function normalizeConfig(inConfig) {
|
||||
let config = {
|
||||
presets: [],
|
||||
presets: [webMenu()],
|
||||
setupUnifiedPlugins: [],
|
||||
setupDevAndBuildPlugins: [],
|
||||
setupDevPlugins: [],
|
||||
@@ -50,6 +53,7 @@ export async function normalizeConfig(inConfig) {
|
||||
setupEleventyPlugins: [],
|
||||
setupEleventyComputedConfig: [],
|
||||
setupCliPlugins: [],
|
||||
setupMenus: [],
|
||||
eleventy: () => {},
|
||||
command: 'help',
|
||||
watch: true,
|
||||
@@ -98,7 +102,7 @@ export async function normalizeConfig(inConfig) {
|
||||
try {
|
||||
const fileConfig = await readConfig('rocket.config', userConfigFile, path.resolve(__configDir));
|
||||
if (fileConfig) {
|
||||
config = {
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
...fileConfig,
|
||||
build: {
|
||||
@@ -111,12 +115,16 @@ export async function normalizeConfig(inConfig) {
|
||||
},
|
||||
imagePresets: config.imagePresets,
|
||||
};
|
||||
if (fileConfig.presets) {
|
||||
updatedConfig.presets = [...config.presets, ...fileConfig.presets];
|
||||
}
|
||||
if (fileConfig.imagePresets && fileConfig.imagePresets.responsive) {
|
||||
config.imagePresets.responsive = {
|
||||
updatedConfig.imagePresets.responsive = {
|
||||
...config.imagePresets.responsive,
|
||||
...fileConfig.imagePresets.responsive,
|
||||
};
|
||||
}
|
||||
config = updatedConfig;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not read rocket config file', error);
|
||||
@@ -165,6 +173,9 @@ export async function normalizeConfig(inConfig) {
|
||||
if (preset.setupCliPlugins) {
|
||||
config.setupCliPlugins = [...config.setupCliPlugins, ...preset.setupCliPlugins];
|
||||
}
|
||||
if (preset.setupMenus) {
|
||||
config.setupMenus = [...config.setupMenus, ...preset.setupMenus];
|
||||
}
|
||||
|
||||
if (typeof preset.before11ty === 'function') {
|
||||
config.__before11tyFunctions.push(preset.before11ty);
|
||||
@@ -174,7 +185,7 @@ export async function normalizeConfig(inConfig) {
|
||||
config._presetPaths.push(path.resolve(_inputDirCwdRelative));
|
||||
|
||||
/** @type {MetaPlugin[]} */
|
||||
let pluginsMeta = [{ plugin: RocketStart }, { plugin: RocketBuild }, { plugin: RocketLint }];
|
||||
let pluginsMeta = [{ plugin: RocketStart }, { plugin: RocketBuild }, { plugin: RocketLint }, { plugin: RocketUpgrade}];
|
||||
|
||||
if (Array.isArray(config.setupCliPlugins)) {
|
||||
for (const setupFn of config.setupCliPlugins) {
|
||||
|
||||
@@ -33,17 +33,6 @@ class TitlePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
class EleventyNavigationPlugin {
|
||||
static dataName = 'eleventyNavigation';
|
||||
|
||||
async execute(data) {
|
||||
if (data.eleventyNavigation) {
|
||||
return data.eleventyNavigation;
|
||||
}
|
||||
return data.titleMeta?.eleventyNavigation;
|
||||
}
|
||||
}
|
||||
|
||||
class SectionPlugin {
|
||||
static dataName = 'section';
|
||||
|
||||
@@ -181,17 +170,50 @@ class JoiningBlocksPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `xx--` prefix that is used for ordering
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
class PermalinkPlugin {
|
||||
static dataName = 'permalink';
|
||||
|
||||
execute(data) {
|
||||
if (data.permalink) {
|
||||
return data.permalink;
|
||||
}
|
||||
let filePath = data.page.filePathStem.replace(/[0-9]+--/g, '');
|
||||
return filePath.endsWith('index') ? `${filePath}.html` : `${filePath}/index.html`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number}
|
||||
*/
|
||||
class MenuOrderPlugin {
|
||||
static dataName = 'menu.order';
|
||||
|
||||
execute(data) {
|
||||
const matches = data.page.fileSlug.match(/([0-9]+)--/);
|
||||
if (matches) {
|
||||
return parseInt(matches[1]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function generateEleventyComputed() {
|
||||
const rocketConfig = getComputedConfig();
|
||||
|
||||
let metaPlugins = [
|
||||
{ plugin: TitleMetaPlugin, options: {} },
|
||||
{ plugin: TitlePlugin, options: {} },
|
||||
{ plugin: EleventyNavigationPlugin, options: {} },
|
||||
{ plugin: SectionPlugin, options: {} },
|
||||
{ plugin: SocialMediaImagePlugin, options: { rocketConfig } },
|
||||
{ plugin: TitleMetaPlugin, options: {} }, // TODO: remove after search & social media are standalone
|
||||
{ plugin: TitlePlugin, options: {} }, // TODO: remove after search & social media are standalone
|
||||
{ plugin: SectionPlugin, options: {} }, // TODO: remove this
|
||||
{ plugin: SocialMediaImagePlugin, options: { rocketConfig } }, // TODO: convert to standalone tool that can work with html
|
||||
{ plugin: JoiningBlocksPlugin, options: rocketConfig },
|
||||
{ plugin: LayoutPlugin, options: {} },
|
||||
{ plugin: PermalinkPlugin, options: {} },
|
||||
{ plugin: MenuOrderPlugin, options: {} },
|
||||
];
|
||||
|
||||
const finalMetaPlugins = executeSetupFunctions(
|
||||
@@ -216,9 +238,8 @@ function generateEleventyComputed() {
|
||||
module.exports = {
|
||||
generateEleventyComputed,
|
||||
LayoutPlugin,
|
||||
TitleMetaPlugin,
|
||||
TitlePlugin,
|
||||
EleventyNavigationPlugin,
|
||||
PermalinkPlugin,
|
||||
MenuOrderPlugin,
|
||||
SectionPlugin,
|
||||
SocialMediaImagePlugin,
|
||||
JoiningBlocksPlugin,
|
||||
|
||||
@@ -37,14 +37,6 @@ module.exports = function (eleventyConfig) {
|
||||
setupUnifiedPlugins: [...defaultSetupUnifiedPlugins, ...config.setupUnifiedPlugins],
|
||||
},
|
||||
},
|
||||
{
|
||||
plugin: eleventyRocketNav,
|
||||
options: {},
|
||||
},
|
||||
{
|
||||
plugin: rocketCollections,
|
||||
options: { _inputDirCwdRelative },
|
||||
},
|
||||
];
|
||||
|
||||
if (Array.isArray(config.setupEleventyPlugins)) {
|
||||
|
||||
169
packages/cli/src/upgrades/upgrade202109menu.js
Normal file
169
packages/cli/src/upgrades/upgrade202109menu.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import matter from 'gray-matter';
|
||||
|
||||
/** @typedef {import('@rocket/cli/types/upgrade').upgrade} upgrade */
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {upgrade} options
|
||||
*/
|
||||
export async function upgrade202109menu({ files, folderRenames }) {
|
||||
let i = 0;
|
||||
|
||||
const updatedFolderRenames = [...folderRenames];
|
||||
for (const fileData of files) {
|
||||
if (fileData.extName === '.md') {
|
||||
const content = (await readFile(fileData.path)).toString();
|
||||
const lines = content.split('\n');
|
||||
const { title, lineNumber } = extractTitle(content);
|
||||
let order = 0;
|
||||
if (title && lineNumber >= 0) {
|
||||
const parsedTitle = parseTitle(title);
|
||||
order = parsedTitle.order;
|
||||
lines[lineNumber] = `# ${parsedTitle.title}`;
|
||||
files[i].updatedContent = lines.join('\n');
|
||||
}
|
||||
if (lines[0] === '---') {
|
||||
const fmObj = matter(content);
|
||||
if (fmObj.data.eleventyNavigation) {
|
||||
const eleventyNav = fmObj.data.eleventyNavigation;
|
||||
if (eleventyNav.order) {
|
||||
order = eleventyNav.order;
|
||||
delete fmObj.data.eleventyNavigation.order;
|
||||
}
|
||||
if (eleventyNav.key) {
|
||||
fmObj.data.menu = { ...fmObj.data.menu, linkText: eleventyNav.key };
|
||||
delete fmObj.data.eleventyNavigation.key;
|
||||
}
|
||||
if (eleventyNav.parent) {
|
||||
delete fmObj.data.eleventyNavigation.parent;
|
||||
}
|
||||
if (Object.keys(eleventyNav).length === 0) {
|
||||
delete fmObj.data.eleventyNavigation;
|
||||
}
|
||||
}
|
||||
|
||||
if (!title && fmObj.data.title) {
|
||||
fmObj.content = `\n# ${fmObj.data.title}\n${fmObj.content}`;
|
||||
delete fmObj.data.title;
|
||||
}
|
||||
|
||||
if (fmObj.data.eleventyExcludeFromCollections) {
|
||||
fmObj.data.menu = { ...fmObj.data.menu, exclude: true };
|
||||
delete fmObj.data.eleventyExcludeFromCollections;
|
||||
}
|
||||
|
||||
if (Object.keys(fmObj.data).length > 0) {
|
||||
files[i].updatedContent = matter.stringify(fmObj.content, fmObj.data);
|
||||
}
|
||||
}
|
||||
if (order !== 0) {
|
||||
if (fileData.relPath.toLowerCase().endsWith('index.md')) {
|
||||
const pathParts = fileData.relPath.split('/');
|
||||
const originDirParts = [...pathParts];
|
||||
originDirParts.pop();
|
||||
pathParts[pathParts.length - 2] = `${order}--${pathParts[pathParts.length - 2]}`;
|
||||
const dirParts = [...pathParts];
|
||||
dirParts.pop();
|
||||
updatedFolderRenames.push({ from: originDirParts.join('/'), to: dirParts.join('/') });
|
||||
} else {
|
||||
files[i].updatedName = `${order}--${fileData.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return { files, folderRenames: updatedFolderRenames };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a text and extracts a title from it
|
||||
*
|
||||
* @param {string} content The text where to extract the title from
|
||||
* @param {string} engine
|
||||
*/
|
||||
export function extractTitle(content, engine = 'md') {
|
||||
if (engine === 'md') {
|
||||
let captureHeading = true;
|
||||
let i = 0;
|
||||
for (const line of content.split('\n')) {
|
||||
if (line.startsWith('```')) {
|
||||
captureHeading = !captureHeading;
|
||||
}
|
||||
if (captureHeading && line.startsWith('# ')) {
|
||||
return { title: line.substring(2), lineNumber: i };
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return { title: '', lineNumber: -1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a title and extracts the relevante data for it.
|
||||
* A title can contain
|
||||
* - ">>" to define a parent => child relationship
|
||||
* - "||" to define the order for this page
|
||||
*
|
||||
* @example
|
||||
* Foo ||3
|
||||
* Foo >> Bar ||10
|
||||
*
|
||||
* @param {string} inTitle
|
||||
* @return {{ title: string, order: number }}
|
||||
*/
|
||||
export function parseTitle(inTitle) {
|
||||
if (typeof inTitle !== 'string') {
|
||||
throw new Error('You need to provide a string to `parseTitle`');
|
||||
}
|
||||
|
||||
let title = inTitle;
|
||||
let order = 0;
|
||||
let navigationTitle = title;
|
||||
if (title.includes('>>')) {
|
||||
const parts = title
|
||||
.split('>>')
|
||||
.map(part => part.trim())
|
||||
.filter(Boolean);
|
||||
title = parts.join(' ');
|
||||
navigationTitle = parts[parts.length - 1];
|
||||
if (parts.length >= 2) {
|
||||
title = `${parts[0]}: ${parts[1]}`;
|
||||
const parentParts = [...parts];
|
||||
parentParts.pop();
|
||||
if (parts.length >= 3) {
|
||||
title = `${parts[parts.length - 2]}: ${parts[parts.length - 1]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (title.includes('||')) {
|
||||
const parts = title
|
||||
.split('||')
|
||||
.map(part => part.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length !== 2) {
|
||||
throw new Error('You can use || only once in `parseTitle`');
|
||||
}
|
||||
|
||||
navigationTitle = navigationTitle.split('||').map(part => part.trim())[0];
|
||||
title = parts[0];
|
||||
order = parseInt(parts[1]);
|
||||
}
|
||||
return {
|
||||
title: navigationTitle,
|
||||
order,
|
||||
};
|
||||
// data.parts = titleParts;
|
||||
// data.title = title;
|
||||
// data.eleventyNavigation = {
|
||||
// key,
|
||||
// title: navigationTitle,
|
||||
// order,
|
||||
// };
|
||||
// if (parent) {
|
||||
// data.eleventyNavigation.parent = parent;
|
||||
// }
|
||||
// return data;
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import chai from 'chai';
|
||||
import { RocketCli } from '../src/RocketCli.js';
|
||||
import path from 'path';
|
||||
import globby from 'globby';
|
||||
import fs from 'fs-extra';
|
||||
import fs, { move, remove } from 'fs-extra';
|
||||
import prettier from 'prettier';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
@@ -165,6 +166,37 @@ export async function executeBootstrap(pathToDir) {
|
||||
return { cli };
|
||||
}
|
||||
|
||||
export async function executeUpgrade(pathToConfig) {
|
||||
const configFile = path.join(fixtureDir, pathToConfig.split('/').join(path.sep));
|
||||
const cli = new RocketCli({
|
||||
argv: ['upgrade', '--config-file', configFile],
|
||||
});
|
||||
await cli.setup();
|
||||
|
||||
// restore from backup if available - in cases the test did stop in the middle
|
||||
if (cli.config._inputDirCwdRelative) {
|
||||
const backupDir = path.join(cli.config._inputDirCwdRelative, '..', 'docs_backup');
|
||||
if (existsSync(backupDir)) {
|
||||
await remove(cli.config._inputDirCwdRelative);
|
||||
await move(backupDir, cli.config._inputDirCwdRelative);
|
||||
}
|
||||
}
|
||||
await cli.run();
|
||||
return {
|
||||
cli,
|
||||
fileExists: fileName => {
|
||||
const outputDir = cli.config._inputDirCwdRelative;
|
||||
return fs.existsSync(path.join(outputDir, fileName));
|
||||
},
|
||||
readFile: async fileName => {
|
||||
// TODO: use readOutput once it's changed to read full file paths
|
||||
const filePath = path.join(cli.config._inputDirCwdRelative, fileName);
|
||||
const text = await fs.promises.readFile(filePath);
|
||||
return text.toString();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function trimWhiteSpace(inString) {
|
||||
return inString
|
||||
.split('\n')
|
||||
|
||||
@@ -19,40 +19,7 @@ describe('RocketCli computedConfig', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('will extract a title from markdown and set first folder as section', async () => {
|
||||
const { cli, readOutput } = await execute(
|
||||
'computed-config-fixtures/headlines/rocket.config.js',
|
||||
{ captureLog: true },
|
||||
);
|
||||
cleanupCli = cli;
|
||||
|
||||
const indexHtml = await readOutput('index.html');
|
||||
const [indexTitle, indexSection] = indexHtml.split('\n');
|
||||
expect(indexTitle).to.equal('Root');
|
||||
expect(indexSection).to.be.undefined;
|
||||
|
||||
const subHtml = await readOutput('sub/index.html');
|
||||
const [subTitle, subSection] = subHtml.split('\n');
|
||||
expect(subTitle).to.equal('Root: Sub');
|
||||
expect(subSection).to.equal('sub');
|
||||
|
||||
const subSubHtml = await readOutput('sub/subsub/index.html');
|
||||
const [subSubTitle, subSubSection] = subSubHtml.split('\n');
|
||||
expect(subSubTitle).to.equal('Sub: SubSub');
|
||||
expect(subSubSection).to.equal('sub');
|
||||
|
||||
const sub2Html = await readOutput('sub2/index.html');
|
||||
const [sub2Title, sub2Section] = sub2Html.split('\n');
|
||||
expect(sub2Title).to.equal('Root: Sub2');
|
||||
expect(sub2Section).to.equal('sub2');
|
||||
|
||||
const withDataHtml = await readOutput('with-data/index.html');
|
||||
const [withDataTitle, withDataSection] = withDataHtml.split('\n');
|
||||
expect(withDataTitle).to.equal('Set via data');
|
||||
expect(withDataSection).be.undefined;
|
||||
});
|
||||
|
||||
it('will note create a social media image in "start"', async () => {
|
||||
it('will not create a social media image in "start"', async () => {
|
||||
const { cli, readOutput } = await execute(
|
||||
'computed-config-fixtures/social-images-only-build/rocket.config.js',
|
||||
{
|
||||
|
||||
@@ -195,9 +195,7 @@ describe('RocketCli images', () => {
|
||||
});
|
||||
expect(indexHtml).to.equal(
|
||||
[
|
||||
'<h2 id="one">',
|
||||
' <a aria-hidden="true" tabindex="-1" href="#one"><span class="icon icon-link"></span></a>one',
|
||||
'</h2>',
|
||||
'<p>one</p>',
|
||||
'<p>',
|
||||
' <picture>',
|
||||
' <source',
|
||||
@@ -221,9 +219,7 @@ describe('RocketCli images', () => {
|
||||
' />',
|
||||
' </picture>',
|
||||
'</p>',
|
||||
'<h2 id="two">',
|
||||
' <a aria-hidden="true" tabindex="-1" href="#two"><span class="icon icon-link"></span></a>two',
|
||||
'</h2>',
|
||||
'<p>two</p>',
|
||||
'<p>',
|
||||
' <picture>',
|
||||
' <source',
|
||||
|
||||
77
packages/cli/test-node/RocketCli.menu.test.js
Normal file
77
packages/cli/test-node/RocketCli.menu.test.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import chai from 'chai';
|
||||
import chalk from 'chalk';
|
||||
import { execute, setFixtureDir } from '@rocket/cli/test-helpers';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
describe('RocketCli Menu', () => {
|
||||
let cleanupCli;
|
||||
|
||||
before(() => {
|
||||
// ignore colors in tests as most CIs won't support it
|
||||
chalk.level = 0;
|
||||
setFixtureDir(import.meta.url);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (cleanupCli?.cleanup) {
|
||||
await cleanupCli.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('will render a menu', async () => {
|
||||
const { cli, readOutput } = await execute('e2e-fixtures/menu/rocket.config.js', {
|
||||
captureLog: true,
|
||||
});
|
||||
cleanupCli = cli;
|
||||
const indexHtml = await readOutput('index.html', {
|
||||
formatHtml: true,
|
||||
});
|
||||
expect(indexHtml).to.equal(
|
||||
[
|
||||
'<html>',
|
||||
' <head> </head>',
|
||||
' <body>',
|
||||
' <web-menu name="site">',
|
||||
' <nav aria-label="site">',
|
||||
' <a href="/components/">Components</a>',
|
||||
' <a href="/getting-started/">Getting Started</a>',
|
||||
' <a href="/blog/">Blog</a>',
|
||||
' </nav>',
|
||||
' </web-menu>',
|
||||
' <h1 id="menu-page">',
|
||||
' <a aria-hidden="true" tabindex="-1" href="#menu-page"><span class="icon icon-link"></span></a',
|
||||
' >Menu Page',
|
||||
' </h1>',
|
||||
' </body>',
|
||||
'</html>',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const accordion = await readOutput('components/content/accordion/index.html', {
|
||||
formatHtml: true,
|
||||
});
|
||||
expect(accordion).to.equal(
|
||||
[
|
||||
'<html>',
|
||||
' <head>',
|
||||
' <meta name="menu:order" content="10" />',
|
||||
' </head>',
|
||||
' <body>',
|
||||
' <web-menu name="site">',
|
||||
' <nav aria-label="site">',
|
||||
' <a href="/components/">Components</a>',
|
||||
' <a href="/getting-started/">Getting Started</a>',
|
||||
' <a href="/blog/">Blog</a>',
|
||||
' </nav>',
|
||||
' </web-menu>',
|
||||
' <h1 id="accordion">',
|
||||
' <a aria-hidden="true" tabindex="-1" href="#accordion"><span class="icon icon-link"></span></a',
|
||||
' >Accordion',
|
||||
' </h1>',
|
||||
' </body>',
|
||||
'</html>',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -72,7 +72,7 @@ describe('RocketCli preset', () => {
|
||||
' <div class="content-area">',
|
||||
' <a class="logo-link" href="/">',
|
||||
' <img src="/_merged_assets/logo.svg" alt="" />',
|
||||
' <span class="sr-only">Rocket</span>',
|
||||
' <span>Rocket</span>',
|
||||
' </a>',
|
||||
' </div>',
|
||||
' </header>',
|
||||
|
||||
57
packages/cli/test-node/RocketCli.upgrade.test.js
Normal file
57
packages/cli/test-node/RocketCli.upgrade.test.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import chai from 'chai';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import { executeUpgrade, setFixtureDir } from '@rocket/cli/test-helpers';
|
||||
import { move, remove } from 'fs-extra';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
describe('Upgrade System', () => {
|
||||
let cli;
|
||||
|
||||
before(() => {
|
||||
// ignore colors in tests as most CIs won't support it
|
||||
chalk.level = 0;
|
||||
setFixtureDir(import.meta.url);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (cli?.cleanup) {
|
||||
await cli.cleanup();
|
||||
}
|
||||
if (cli?.config._inputDirCwdRelative) {
|
||||
const backupDir = path.join(cli.config._inputDirCwdRelative, '..', 'docs_backup');
|
||||
if (existsSync(backupDir)) {
|
||||
await remove(cli.config._inputDirCwdRelative);
|
||||
await move(backupDir, cli.config._inputDirCwdRelative);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('2021-09-menu', async () => {
|
||||
const run = await executeUpgrade('fixtures-upgrade/2021-09-menu/rocket.config.js');
|
||||
cli = run.cli;
|
||||
expect(run.fileExists('index.md')).to.be.true;
|
||||
expect(run.fileExists('31--components/index.md')).to.be.true;
|
||||
expect(await run.readFile('31--components/index.md')).to.equal(
|
||||
[
|
||||
'---',
|
||||
'menu:',
|
||||
' linkText: Components',
|
||||
'---',
|
||||
'',
|
||||
'# Component Directory',
|
||||
'',
|
||||
'Here you get started.',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(run.fileExists('31--components/10--content/20--accordion/overview.md')).to.be.false;
|
||||
expect(run.fileExists('31--components/10--content/20--accordion/10--overview.md')).to.be.true;
|
||||
expect(await run.readFile('31--components/10--content/20--accordion/10--overview.md')).to.equal(
|
||||
'# Overview\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,10 @@
|
||||
layout: layout-raw
|
||||
---
|
||||
|
||||
## one
|
||||
one
|
||||
|
||||

|
||||
|
||||
## two
|
||||
two
|
||||
|
||||

|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Accordion
|
||||
@@ -0,0 +1 @@
|
||||
# Tabs
|
||||
@@ -0,0 +1 @@
|
||||
# Content
|
||||
@@ -0,0 +1 @@
|
||||
# Components
|
||||
@@ -0,0 +1 @@
|
||||
# Software
|
||||
@@ -0,0 +1 @@
|
||||
# Node
|
||||
@@ -0,0 +1 @@
|
||||
# Setup
|
||||
@@ -0,0 +1 @@
|
||||
# Getting Started
|
||||
@@ -0,0 +1 @@
|
||||
# Blog
|
||||
@@ -0,0 +1,11 @@
|
||||
<html>
|
||||
<head>
|
||||
{% if menu.order %}
|
||||
<meta name="menu:order" content="{{ menu.order }}" />
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<web-menu name="site"></web-menu>
|
||||
{{ content | safe }}
|
||||
</body>
|
||||
</html>
|
||||
1
packages/cli/test-node/e2e-fixtures/menu/docs/index.md
Normal file
1
packages/cli/test-node/e2e-fixtures/menu/docs/index.md
Normal file
@@ -0,0 +1 @@
|
||||
# Menu Page
|
||||
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
@@ -0,0 +1,11 @@
|
||||
<html>
|
||||
<head>
|
||||
{% if menuOrder %}
|
||||
<meta name="menu:order" content="{{ menuOrder }}"/>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<web-menu name="site"></web-menu>
|
||||
{{ content | safe }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
# Components >> Content >> Accordion >> Api || 20
|
||||
@@ -0,0 +1 @@
|
||||
# Components >> Content >> Accordion || 20
|
||||
@@ -0,0 +1 @@
|
||||
# Components >> Content >> Accordion >> Overview || 10
|
||||
@@ -0,0 +1 @@
|
||||
# Components >> Content || 10
|
||||
@@ -0,0 +1 @@
|
||||
# Components >> Content >> Tabs || 10
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Component Directory
|
||||
eleventyNavigation:
|
||||
key: Components
|
||||
order: 31
|
||||
---
|
||||
|
||||
Here you get started.
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Rocket
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
/** @type {Partial<import("../../../types/main").RocketCliOptions>} */
|
||||
const config = {};
|
||||
export default config;
|
||||
@@ -14,19 +14,32 @@ function cleanup(config) {
|
||||
delete configNoPaths.eleventy;
|
||||
delete configNoPaths.outputDevDir;
|
||||
delete configNoPaths.imagePresets.responsive.ignore;
|
||||
delete configNoPaths.presets;
|
||||
delete configNoPaths.setupCliPlugins;
|
||||
return configNoPaths;
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
{ commands: ['start'] },
|
||||
{ commands: ['build'] },
|
||||
{ commands: ['start', 'build', 'lint'] }, // lint
|
||||
{ commands: ['upgrade'] },
|
||||
{ commands: ['start', 'build', 'lint'] }, // web-menu
|
||||
];
|
||||
|
||||
describe('normalizeConfig', () => {
|
||||
it('makes sure essential settings are there', async () => {
|
||||
const configFile = path.join(__dirname, 'fixtures', 'empty', 'rocket.config.js');
|
||||
const config = await normalizeConfig({ configFile });
|
||||
|
||||
// testing pathes is always a little more complicted 😅
|
||||
// testing pathes is always a little more complicated 😅
|
||||
expect(config._inputDirCwdRelative).to.match(/empty\/docs$/);
|
||||
expect(config._presetPaths[0]).to.match(/cli\/preset$/);
|
||||
expect(config._presetPaths[1]).to.match(/empty\/docs$/);
|
||||
expect(config._presetPaths[1]).to.match(/web-menu\/preset$/);
|
||||
expect(config._presetPaths[2]).to.match(/empty\/docs$/);
|
||||
expect(config.outputDevDir).to.match(/_site-dev$/);
|
||||
expect(config.presets.length).to.equal(1);
|
||||
expect(config.setupCliPlugins.length).to.equal(1);
|
||||
|
||||
expect(cleanup(config)).to.deep.equal({
|
||||
__before11tyFunctions: [],
|
||||
@@ -41,14 +54,9 @@ describe('normalizeConfig', () => {
|
||||
setupDevPlugins: [],
|
||||
setupEleventyPlugins: [],
|
||||
setupEleventyComputedConfig: [],
|
||||
setupCliPlugins: [],
|
||||
presets: [],
|
||||
setupMenus: [],
|
||||
serviceWorkerName: 'service-worker.js',
|
||||
plugins: [
|
||||
{ commands: ['start'] },
|
||||
{ commands: ['build'] },
|
||||
{ commands: ['start', 'build', 'lint'] },
|
||||
],
|
||||
plugins,
|
||||
imagePresets: {
|
||||
responsive: {
|
||||
formats: ['avif', 'jpeg'],
|
||||
@@ -84,9 +92,8 @@ describe('normalizeConfig', () => {
|
||||
setupDevAndBuildPlugins: [],
|
||||
setupDevPlugins: [],
|
||||
setupEleventyPlugins: [],
|
||||
setupCliPlugins: [],
|
||||
setupEleventyComputedConfig: [],
|
||||
presets: [],
|
||||
setupMenus: [],
|
||||
imagePresets: {
|
||||
responsive: {
|
||||
formats: ['avif', 'jpeg'],
|
||||
@@ -95,11 +102,7 @@ describe('normalizeConfig', () => {
|
||||
},
|
||||
},
|
||||
serviceWorkerName: 'service-worker.js',
|
||||
plugins: [
|
||||
{ commands: ['start'] },
|
||||
{ commands: ['build'] },
|
||||
{ commands: ['start', 'build', 'lint'] },
|
||||
],
|
||||
plugins,
|
||||
inputDir: 'docs',
|
||||
outputDir: '_site',
|
||||
});
|
||||
@@ -125,9 +128,8 @@ describe('normalizeConfig', () => {
|
||||
setupDevAndBuildPlugins: [],
|
||||
setupDevPlugins: [],
|
||||
setupEleventyPlugins: [],
|
||||
setupCliPlugins: [],
|
||||
setupEleventyComputedConfig: [],
|
||||
presets: [],
|
||||
setupMenus: [],
|
||||
imagePresets: {
|
||||
responsive: {
|
||||
formats: ['avif', 'jpeg'],
|
||||
@@ -136,11 +138,7 @@ describe('normalizeConfig', () => {
|
||||
},
|
||||
},
|
||||
serviceWorkerName: 'service-worker.js',
|
||||
plugins: [
|
||||
{ commands: ['start'] },
|
||||
{ commands: ['build'] },
|
||||
{ commands: ['start', 'build', 'lint'] },
|
||||
],
|
||||
plugins,
|
||||
inputDir: 'docs',
|
||||
outputDir: '_site',
|
||||
});
|
||||
@@ -169,9 +167,8 @@ describe('normalizeConfig', () => {
|
||||
setupDevAndBuildPlugins: [],
|
||||
setupDevPlugins: [],
|
||||
setupEleventyPlugins: [],
|
||||
setupCliPlugins: [],
|
||||
setupEleventyComputedConfig: [],
|
||||
presets: [],
|
||||
setupMenus: [],
|
||||
imagePresets: {
|
||||
responsive: {
|
||||
formats: ['avif', 'jpeg'],
|
||||
@@ -180,11 +177,7 @@ describe('normalizeConfig', () => {
|
||||
},
|
||||
},
|
||||
serviceWorkerName: 'service-worker.js',
|
||||
plugins: [
|
||||
{ commands: ['start'] },
|
||||
{ commands: ['build'] },
|
||||
{ commands: ['start', 'build', 'lint'] },
|
||||
],
|
||||
plugins,
|
||||
inputDir: 'docs',
|
||||
outputDir: '_site',
|
||||
});
|
||||
|
||||
22
packages/cli/types/upgrade.d.ts
vendored
Normal file
22
packages/cli/types/upgrade.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface UpgradeFile {
|
||||
path: string;
|
||||
relPath: string;
|
||||
name: string;
|
||||
extName: string;
|
||||
updatedContent?: string;
|
||||
updatedPath?: string;
|
||||
updatedRelPath?: string;
|
||||
updatedName?: string;
|
||||
}
|
||||
|
||||
export interface FolderRename {
|
||||
from: string;
|
||||
to: string;
|
||||
fromAbsolute?: string;
|
||||
toAbsolute?: string;
|
||||
}
|
||||
|
||||
export interface upgrade {
|
||||
files: UpgradeFile[];
|
||||
folderRenames: FolderRename[];
|
||||
}
|
||||
@@ -1,17 +1,5 @@
|
||||
# @rocket/drawer
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1f14105: Add export map which enables side effect import via `@rocket/drawer/define`
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 445b028: Update to latest lit, @open-wc, @lion packages
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/drawer",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -13,11 +13,6 @@
|
||||
},
|
||||
"author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)",
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./rocket-drawer.js": "./rocket-drawer.js",
|
||||
"./define": "./rocket-drawer.js"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "web-dev-server --node-resolve --root-dir ../../ --open packages/drawer/ --watch",
|
||||
"rocket:build": "node src/build/cli.js -c demo/docs",
|
||||
@@ -38,8 +33,8 @@
|
||||
"testing"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lion/overlays": "^0.29.1",
|
||||
"lit": "^2.0.0"
|
||||
"@lion/overlays": "^0.26.1",
|
||||
"lit-element": "^2.4.0"
|
||||
},
|
||||
"types": "dist-types/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ body[layout^='layout-home'] #main-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#main-header web-menu > nav,
|
||||
#main-header .content-area {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -155,6 +156,10 @@ body[layout^='layout-home'] #main-header {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
#main-header web-menu {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
#main-header a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@@ -209,10 +214,15 @@ body[layout^='layout-home'] #main-header a:hover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#main-header web-menu > nav > a,
|
||||
#main-header .content-area > * {
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
#main-header .content-area web-menu {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#main-header .content-area > .social-link {
|
||||
margin-right: 15px;
|
||||
}
|
||||
@@ -263,26 +273,26 @@ body[layout^='layout-home'] #main-header a:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
rocket-navigation .light-dark-switch {
|
||||
web-menu[name="index"] .light-dark-switch {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
rocket-navigation .light-dark-switch::part(label) {
|
||||
web-menu[name="index"] .light-dark-switch::part(label) {
|
||||
order: 10;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
rocket-navigation a {
|
||||
web-menu[name="index"] a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
rocket-navigation a:hover {
|
||||
web-menu[name="index"] a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* line on the left to indicate current page */
|
||||
rocket-navigation > ul > li > ul li.current ul li.anchor.current::before {
|
||||
web-menu[name="index"] > nav > ul > li > ul li.current ul li.anchor.current::before {
|
||||
content: '';
|
||||
height: 1.6em;
|
||||
width: 3px;
|
||||
@@ -292,58 +302,47 @@ rocket-navigation > ul > li > ul li.current ul li.anchor.current::before {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
rocket-navigation li {
|
||||
web-menu[name="index"] li {
|
||||
padding: 7px 0;
|
||||
}
|
||||
|
||||
rocket-navigation > ul > li > ul li.current a:not(.anchor) {
|
||||
web-menu[name="index"] > nav > ul > li > ul li.current a:not(.anchor) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
rocket-navigation > ul > li > ul > li.current > ul > li > a {
|
||||
web-menu[name="index"] > nav > ul > li > ul > li.current > ul > li > a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
rocket-navigation hr {
|
||||
web-menu[name="index"] hr {
|
||||
margin: 30px -10px 10px -10px;
|
||||
}
|
||||
|
||||
/* Hide below 3rd level by default */
|
||||
rocket-navigation > ul > li > ul > li ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Only show below 3rd level if level above is active/current */
|
||||
li.current > ul,
|
||||
li.active > ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
rocket-navigation > ul > li > a {
|
||||
web-menu[name="index"] > nav > ul > li > span {
|
||||
color: var(--primary-color);
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
rocket-navigation > ul > li > ul a {
|
||||
web-menu[name="index"] > nav > ul > li > ul a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
rocket-navigation {
|
||||
web-menu[name="index"] {
|
||||
overflow: auto;
|
||||
display: block;
|
||||
margin-top: 40px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
rocket-navigation ul {
|
||||
web-menu[name="index"] ul {
|
||||
padding: 7px 0 10px 15px;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
rocket-navigation > ul {
|
||||
web-menu[name="index"] > nav > ul {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
@@ -422,7 +421,7 @@ li.current > ul > li.anchor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
rocket-navigation {
|
||||
web-menu[name="index"] {
|
||||
padding: 0 25px 0 0;
|
||||
}
|
||||
|
||||
@@ -433,7 +432,7 @@ li.current > ul > li.anchor {
|
||||
}
|
||||
|
||||
/* for blog detail page */
|
||||
rocket-navigation h3 {
|
||||
web-menu[name="index"] h3 {
|
||||
font-family: var(--heading-font-family, var(--primary-font-family));
|
||||
font-size: 16px;
|
||||
margin: 0 0 7px 0;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import '@rocket/navigation/rocket-navigation.js';
|
||||
import '@rocket/drawer/rocket-drawer.js';
|
||||
const drawer = document.querySelector('#sidebar');
|
||||
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
<rocket-navigation>
|
||||
{{ collections[section] | rocketNav | rocketNavToHtml({
|
||||
listItemClass: "menu-item",
|
||||
activeListItemClass: "current",
|
||||
activeKey: eleventyNavigation.key
|
||||
}) | safe }}
|
||||
{% include 'partials/mobile-sidebar-bottom.njk' %}
|
||||
</rocket-navigation>
|
||||
<web-menu name="index"></web-menu>
|
||||
{% include 'partials/mobile-sidebar-bottom.njk' %}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{% if layout === "home.njk" and rocketLaunch.homeLayout === "background" %}
|
||||
<a class="logo-link" href="{{ '/' | url }}">
|
||||
<img src="{{ '/_assets/logo.svg' | asset | url }}" alt="{{ site.logoAlt }}" />
|
||||
<span class="sr-only">{{ site.name }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{% include 'partials/_shared/logoLink.njk' %}
|
||||
{% endif %}
|
||||
@@ -1,6 +0,0 @@
|
||||
{%- for entry in collections.header %}
|
||||
<a href="{{ entry.url | url }}" class="
|
||||
{% if entry.url == page.url %} current {% endif %}
|
||||
{% if page.url and page.url.search and (page.url.search(entry.url) !== -1) and (page.url !== '/') %} active {% endif %}
|
||||
">{{ entry.data.eleventyNavigation.key }}</a>
|
||||
{%- endfor %}
|
||||
@@ -0,0 +1 @@
|
||||
<web-menu name="site"></web-menu>
|
||||
@@ -1,6 +1,7 @@
|
||||
<html lang="en">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="menu:exclude" content="true" />
|
||||
<title>Page not found</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css?family=Dosis:300,400,500');
|
||||
|
||||
@@ -4,6 +4,7 @@ import { adjustPluginOptions } from 'plugins-manager';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { LayoutPlugin } from '@rocket/cli';
|
||||
import htmlHeading from 'rehype-autolink-headings';
|
||||
import { IndexMenu } from '@web/menu';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -54,6 +55,9 @@ export function rocketLaunch() {
|
||||
setupEleventyComputedConfig: [
|
||||
adjustPluginOptions(LayoutPlugin, { defaultLayout: 'layout-sidebar' }),
|
||||
],
|
||||
setupMenus: [
|
||||
adjustPluginOptions(IndexMenu, { navWrapper: nav => nav }),
|
||||
],
|
||||
adjustImagePresets: imagePresets => ({
|
||||
...imagePresets,
|
||||
responsive: {
|
||||
|
||||
@@ -108,6 +108,11 @@ describe('RocketLaunch preset', () => {
|
||||
' <body layout="layout-sidebar">',
|
||||
' <header id="main-header">',
|
||||
' <div class="content-area">',
|
||||
' <a class="logo-link" href="/">',
|
||||
' <img src="/_merged_assets/logo.svg" alt="Rocket Logo" />',
|
||||
' <span>Rocket</span>',
|
||||
' </a>',
|
||||
'',
|
||||
' <button id="mobile-menu-trigger" data-action="trigger-mobile-menu">',
|
||||
' <span class="sr-only">Show Menu</span>',
|
||||
' <svg',
|
||||
@@ -124,10 +129,11 @@ describe('RocketLaunch preset', () => {
|
||||
' </svg>',
|
||||
' </button>',
|
||||
'',
|
||||
' <a class="logo-link" href="/">',
|
||||
' <img src="/_merged_assets/logo.svg" alt="Rocket Logo" />',
|
||||
' <span>Rocket</span>',
|
||||
' </a>',
|
||||
' <web-menu name="site">',
|
||||
' <nav aria-label="site">',
|
||||
' <a href="/page/" aria-current="page">Page</a>',
|
||||
' </nav>',
|
||||
' </web-menu>',
|
||||
'',
|
||||
' <launch-dark-switch class="light-dark-switch" label="Toggle darkmode"',
|
||||
' >Toggle darkmode</launch-dark-switch',
|
||||
@@ -188,16 +194,18 @@ describe('RocketLaunch preset', () => {
|
||||
' <span>Rocket</span>',
|
||||
' </a>',
|
||||
'',
|
||||
' <rocket-navigation>',
|
||||
' <div class="sidebar-bottom">',
|
||||
' <hr />',
|
||||
' <launch-dark-switch class="light-dark-switch" label="Toggle darkmode"',
|
||||
' >Toggle darkmode</launch-dark-switch',
|
||||
' >',
|
||||
|
||||
' <web-menu name="index">',
|
||||
' <ul class="lvl-2"></ul>',
|
||||
' </web-menu>',
|
||||
' <div class="sidebar-bottom">',
|
||||
' <hr />',
|
||||
' <launch-dark-switch class="light-dark-switch" label="Toggle darkmode"',
|
||||
' >Toggle darkmode</launch-dark-switch',
|
||||
' >',
|
||||
'',
|
||||
' <a href="https://github.com/modernweb-dev/rocket/issues">Help and Feedback</a>',
|
||||
' </div>',
|
||||
' </rocket-navigation>',
|
||||
' <a href="https://github.com/modernweb-dev/rocket/issues">Help and Feedback</a>',
|
||||
' </div>',
|
||||
' </nav>',
|
||||
' </rocket-drawer>',
|
||||
'',
|
||||
@@ -253,7 +261,7 @@ describe('RocketLaunch preset', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('offers a layout-home', async () => {
|
||||
it.skip('offers a layout-home', async () => {
|
||||
const { cli, readOutput } = await execute('fixtures/layout-home/rocket.config.js', {
|
||||
captureLog: true,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# Change Log
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 97cb38c: Add missing slash dependency
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mdjs/core",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -60,7 +60,6 @@
|
||||
"remark-gfm": "^1.0.0",
|
||||
"remark-parse": "^9.0.0",
|
||||
"remark-rehype": "^8.0.0",
|
||||
"slash": "^3.0.0",
|
||||
"unified": "^9.2.0",
|
||||
"unist-util-remove": "^2.0.1",
|
||||
"unist-util-visit": "^2.0.3"
|
||||
@@ -68,7 +67,7 @@
|
||||
"devDependencies": {
|
||||
"demo-wc-card": "^0.1.0",
|
||||
"remark-autolink-headings": "^6.0.1",
|
||||
"remark-html": "^13.0.2",
|
||||
"remark-html": "^13.0.1",
|
||||
"remark-slug": "^6.0.0",
|
||||
"remark-stringify": "^9.0.1"
|
||||
},
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('Integration', () => {
|
||||
})
|
||||
.use(mdSlug)
|
||||
.use(mdHeadings)
|
||||
.use(mdStringify, { sanitize: false });
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
expect(result.contents).to.equal(expected);
|
||||
expect(/** @type {MDJSVFileData} */ (result.data).stories).to.deep.equal([
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('mdjsParse', () => {
|
||||
'const bar = 22;',
|
||||
'```',
|
||||
].join('\n');
|
||||
const parser = unified().use(markdown).use(mdjsParse).use(html, { sanitize: false });
|
||||
const parser = unified().use(markdown).use(mdjsParse).use(html);
|
||||
const result = await parser.process(input);
|
||||
expect(result.contents).to.equal(
|
||||
'<h2>Intro</h2>\n<pre><code class="language-js">const foo = 1;\n</code></pre>\n',
|
||||
@@ -36,7 +36,7 @@ describe('mdjsParse', () => {
|
||||
'const bar = 22;',
|
||||
'```',
|
||||
].join('\n');
|
||||
const parser = unified().use(markdown).use(mdjsParse).use(html, { sanitize: false });
|
||||
const parser = unified().use(markdown).use(mdjsParse).use(html);
|
||||
const result = await parser.process(input);
|
||||
expect(result.contents).to.equal('');
|
||||
expect(/** @type {MDJSVFileData} */ (result.data).jsCode).to.equal('const bar = 22;');
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('mdjsStoryParse', () => {
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified().use(markdown).use(mdjsStoryParse).use(html, { sanitize: false });
|
||||
const parser = unified().use(markdown).use(mdjsStoryParse).use(html);
|
||||
const result = await parser.process(input);
|
||||
expect(result.contents).to.equal(expected);
|
||||
expect(/** @type {MDJSVFileData} */ (result.data).stories).to.deep.equal([
|
||||
@@ -110,7 +110,7 @@ describe('mdjsStoryParse', () => {
|
||||
storyTag: name => `<Story name="${name}"></Story>`,
|
||||
previewStoryTag: name => `<Preview><Story name="${name}"></Story></Preview>`,
|
||||
})
|
||||
.use(html, { sanitize: false });
|
||||
.use(html);
|
||||
const result = await parser.process(input);
|
||||
expect(result.contents).to.equal(expected);
|
||||
});
|
||||
@@ -148,7 +148,7 @@ describe('mdjsStoryParse', () => {
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified().use(markdown).use(mdjsStoryParse).use(html, { sanitize: false });
|
||||
const parser = unified().use(markdown).use(mdjsStoryParse).use(html);
|
||||
const result = await parser.process(input);
|
||||
expect(result.contents).to.equal(expected);
|
||||
});
|
||||
@@ -186,7 +186,7 @@ describe('mdjsStoryParse', () => {
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified().use(markdown).use(mdjsStoryParse).use(html, { sanitize: false });
|
||||
const parser = unified().use(markdown).use(mdjsStoryParse).use(html);
|
||||
const result = await parser.process(input);
|
||||
expect(result.contents).to.equal(expected);
|
||||
});
|
||||
@@ -242,7 +242,7 @@ describe('mdjsStoryParse', () => {
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified().use(markdown).use(mdjsStoryParse).use(html, { sanitize: false });
|
||||
const parser = unified().use(markdown).use(mdjsStoryParse).use(html);
|
||||
const result = await parser.process(input);
|
||||
expect(result.contents).to.equal(expected);
|
||||
});
|
||||
|
||||
@@ -1,29 +1,5 @@
|
||||
# @mdjs/mdjs-preview
|
||||
|
||||
## 0.5.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e81b77f: Change theme attribute into preview-theme attribute to separate theme styling of the preview section and the full mdjs viewer.
|
||||
- 456b8e7: Add css variable to style border-color of the mdjs-viewer
|
||||
|
||||
## 0.5.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 445b028: Update to latest lit, @open-wc, @lion packages
|
||||
|
||||
## 0.5.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 15a82c0: Enable including script files into the simulator via `<script src=".." mdjs-use>`
|
||||
- 15a82c0: Allow only a limited set of characters for simulator includes `[a-zA-Z0-9\/\-_]`.
|
||||
Notably, there is no:
|
||||
|
||||
- `:` to prevent `http://...` includes
|
||||
- `.` so filenames as `this.is.my.js` are not supported. Also includes will be without file endings which will be added automatically
|
||||
|
||||
## 0.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mdjs/mdjs-preview",
|
||||
"version": "0.5.6",
|
||||
"version": "0.5.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -32,9 +32,9 @@
|
||||
"src"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lion/accordion": "^0.7.2",
|
||||
"@open-wc/scoped-elements": "^2.0.0",
|
||||
"lit": "^2.0.0"
|
||||
"@lion/accordion": "^0.6.1",
|
||||
"@open-wc/scoped-elements": "^2.0.0-next.3",
|
||||
"lit": "^2.0.0-rc.2"
|
||||
},
|
||||
"types": "dist-types/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -10,16 +10,6 @@ import {
|
||||
} from './mdjsViewerSharedStates.js';
|
||||
import { addResizeHandler } from './resizeHandler.js';
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {'js'|'css'} type
|
||||
* @returns {string}
|
||||
*/
|
||||
function sanitize(input, type) {
|
||||
const url = new URL(input);
|
||||
return url.pathname.slice(1, (type.length + 1) * -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} StoryOptions
|
||||
* @property {HTMLElement | null} StoryOptions.shadowRoot
|
||||
@@ -61,7 +51,7 @@ export class MdJsPreview extends ScopedElementsMixin(LitElement) {
|
||||
platforms: { type: Array },
|
||||
size: { type: String },
|
||||
sizes: { type: Array },
|
||||
previewTheme: { type: String, reflect: true, attribute: 'preview-theme' },
|
||||
theme: { type: String, reflect: true },
|
||||
themes: { type: Array },
|
||||
language: { type: String },
|
||||
languages: { type: Array },
|
||||
@@ -82,7 +72,7 @@ export class MdJsPreview extends ScopedElementsMixin(LitElement) {
|
||||
this.__supportsClipboard = 'clipboard' in navigator;
|
||||
this.__copyButtonText = 'Copy Code';
|
||||
|
||||
this.previewTheme = 'light';
|
||||
this.theme = 'light';
|
||||
/** @type {{ key: string, name: string }[]} */
|
||||
this.themes = [
|
||||
// { key: 'light', name: 'Light' },
|
||||
@@ -283,10 +273,11 @@ export class MdJsPreview extends ScopedElementsMixin(LitElement) {
|
||||
if (!mdjsSetupScript) {
|
||||
throw new Error('Could not find a <script type="module" src="..." mdjs-setup></script>');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('story-file', sanitize(mdjsSetupScript.src, 'js'));
|
||||
params.set('story-file', mdjsSetupScript.src);
|
||||
params.set('story-key', this.key);
|
||||
params.set('theme', this.previewTheme);
|
||||
params.set('theme', this.theme);
|
||||
params.set('platform', this.platform);
|
||||
params.set('language', this.language);
|
||||
params.set('edge-distance', this.edgeDistance.toString());
|
||||
@@ -296,16 +287,7 @@ export class MdJsPreview extends ScopedElementsMixin(LitElement) {
|
||||
]);
|
||||
for (const link of links) {
|
||||
if (link.href) {
|
||||
params.append('stylesheets', sanitize(link.href, 'css'));
|
||||
}
|
||||
}
|
||||
|
||||
const moduleUrls = /** @type {HTMLScriptElement[]} */ ([
|
||||
...document.querySelectorAll('script[type=module][mdjs-use]'),
|
||||
]);
|
||||
for (const moduleUrl of moduleUrls) {
|
||||
if (moduleUrl.src) {
|
||||
params.append('moduleUrls', sanitize(moduleUrl.src, 'js'));
|
||||
params.append('stylesheets', link.href);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,20 +421,20 @@ export class MdJsPreview extends ScopedElementsMixin(LitElement) {
|
||||
@change=${
|
||||
/** @param {Event} ev */ ev => {
|
||||
if (ev.target) {
|
||||
this.previewTheme = /** @type {HTMLInputElement} */ (ev.target).value;
|
||||
this.theme = /** @type {HTMLInputElement} */ (ev.target).value;
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
${this.themes.map(
|
||||
previewTheme => html`
|
||||
<label class="${this.previewTheme === previewTheme.key ? 'selected' : ''}">
|
||||
<span>${previewTheme.name}</span>
|
||||
theme => html`
|
||||
<label class="${this.theme === theme.key ? 'selected' : ''}">
|
||||
<span>${theme.name}</span>
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="${previewTheme.key}"
|
||||
?checked=${this.previewTheme === previewTheme.key}
|
||||
value="${theme.key}"
|
||||
?checked=${this.theme === theme.key}
|
||||
/>
|
||||
</label>
|
||||
`,
|
||||
@@ -686,11 +668,11 @@ export class MdJsPreview extends ScopedElementsMixin(LitElement) {
|
||||
}
|
||||
|
||||
:host(:not([device-mode])) #wrapper {
|
||||
border: 2px solid var(--primary-lines-color, #4caf50);
|
||||
border: 2px solid #4caf50;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 2px solid var(--primary-lines-color, #4caf50);
|
||||
border: 2px solid #4caf50;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const _sharedStates = {
|
||||
platform: 'web',
|
||||
size: 'webSmall',
|
||||
previewTheme: 'light',
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
autoHeight: true,
|
||||
deviceMode: false,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @mdjs/mdjs-story
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 445b028: Update to latest lit, @open-wc, @lion packages
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mdjs/mdjs-story",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -31,7 +31,7 @@
|
||||
"src"
|
||||
],
|
||||
"dependencies": {
|
||||
"lit": "^2.0.0"
|
||||
"lit": "^2.0.0-rc.2"
|
||||
},
|
||||
"types": "dist-types/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @rocket/search
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 445b028: Update to latest lit, @open-wc, @lion packages
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/search",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -42,10 +42,10 @@
|
||||
"search"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lion/combobox": "^0.8.6",
|
||||
"@lion/core": "^0.19.0",
|
||||
"@lion/listbox": "^0.10.7",
|
||||
"@open-wc/scoped-elements": "^2.0.0",
|
||||
"@lion/combobox": "^0.8.0",
|
||||
"@lion/core": "^0.18.0",
|
||||
"@lion/listbox": "^0.10.1",
|
||||
"@open-wc/scoped-elements": "^2.0.0-next.3",
|
||||
"chalk": "^4.0.0",
|
||||
"minisearch": "^3.0.2",
|
||||
"plugins-manager": "^0.3.0",
|
||||
|
||||
@@ -189,7 +189,7 @@ export class RocketSearchCombobox extends LionCombobox {
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return /** @type {typeof LionCombobox['properties'] & { showInput: import("lit").PropertyDeclaration } } */ ({
|
||||
return /** @type {typeof LionCombobox['properties'] & { showInput: import("lit-element").PropertyDeclaration } } */ ({
|
||||
showInput: { type: Boolean, reflect: true, attribute: 'show-input' },
|
||||
});
|
||||
}
|
||||
|
||||
277
packages/web-menu/README.md
Normal file
277
packages/web-menu/README.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Web Menu
|
||||
|
||||
Gathers information about your static html pages and creates menus for it.
|
||||
|
||||
## Features
|
||||
|
||||
- Very fast (uses wasm & html streaming for parsing)
|
||||
- Comes with multiple pre defined menus like site, breadcrumb, tableOfContents, ...
|
||||
- Menus are accessible and fully style able
|
||||
- Works with any tool that outputs html
|
||||
- Typically reduces the tools build time by offloading menu generation
|
||||
- Flexible rendering system for menus via 4 plain javascript functions (render, list, listItem, link)
|
||||
- Low dependency count
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
npm i -D @web/menu
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
npx web-menu
|
||||
```
|
||||
|
||||
## Usage as a html user
|
||||
|
||||
Write your html as you normally would but don't include any menus.
|
||||
Where you want to place a menu put `<web-menu type="site"></web-menu>`.
|
||||
When you run `npx web-menu` it will insert the menu into this tag.
|
||||
|
||||
e.g.
|
||||
|
||||
You write
|
||||
|
||||
```html
|
||||
<header>
|
||||
<p>...</p>
|
||||
<web-menu type="site"></web-menu>
|
||||
</header>
|
||||
```
|
||||
|
||||
and it will become
|
||||
|
||||
```html
|
||||
<header>
|
||||
<p>...</p>
|
||||
<web-menu name="site">
|
||||
<nav aria-label="site">
|
||||
<a href="/about/">About</a>
|
||||
<a href="/components/" aria-current="page">Components</a>
|
||||
</nav>
|
||||
</web-menu>
|
||||
</header>
|
||||
```
|
||||
|
||||
## Configuration file
|
||||
|
||||
You can put configurations at
|
||||
|
||||
- `config/web-menu.js`
|
||||
- `config/web-menu.mjs`
|
||||
- `web-menu.config.js`
|
||||
- `web-menu.config.mjs`
|
||||
|
||||
```js
|
||||
export default {
|
||||
docsDir: 'my-menu/',
|
||||
outputDir:
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Types of the config file</summary>
|
||||
<div>
|
||||
TODO: inline types
|
||||
</div>
|
||||
</details>
|
||||
|
||||
## Add your own menu type
|
||||
|
||||
```js
|
||||
import { Menu } from '@web/menu';
|
||||
|
||||
class MyMenu extends Menu {
|
||||
static name = 'my-menu';
|
||||
|
||||
async render() {
|
||||
return '--- My Menu ---';
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
docsDir: 'my-html-site/',
|
||||
plugins: [addPlugin(MyMenu)],
|
||||
};
|
||||
```
|
||||
|
||||
## Menu types
|
||||
|
||||
1. **site**
|
||||
|
||||
- starts at level 1
|
||||
- flat list of links
|
||||
- commonly used as a top bar navigation of "sections"
|
||||
|
||||
```html
|
||||
<web-menu name="site">
|
||||
<nav aria-label="site">
|
||||
<a href="/about/">About</a>
|
||||
<a href="/components/" aria-current="page">Components</a>
|
||||
</nav>
|
||||
</web-menu>
|
||||
```
|
||||
|
||||
2. **index**
|
||||
|
||||
- starts at level 2
|
||||
- nested ul/li list
|
||||
- level 2 becomes a not clickable category heading if it has children
|
||||
- level 3+ becomes a `detail/summary` element needing a click if it has children
|
||||
- ideally used in combination with `site`
|
||||
|
||||
```html
|
||||
<web-menu name="index">
|
||||
<nav aria-label="index">
|
||||
<ul class="lvl-2">
|
||||
<li class="web-menu-active">
|
||||
<span>Content</span>
|
||||
<ul class="lvl-3">
|
||||
<li class="web-menu-active">
|
||||
<details open>
|
||||
<summary>Accordion</summary>
|
||||
<ul class="lvl-4">
|
||||
<li class="web-menu-current">
|
||||
<a href="/components/content/accordion/overview/" aria-current="page"
|
||||
>Overview</a
|
||||
>
|
||||
</li>
|
||||
<li><a href="/components/content/accordion/api/">API</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<span>Inputs</span>
|
||||
<ul class="lvl-3">
|
||||
<li><a href="/components/inputs/input-text/">Input Text</a></li>
|
||||
<li><a href="/components/inputs/textarea/">Textarea</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</web-menu>
|
||||
```
|
||||
|
||||
3. **breadcrumb**
|
||||
|
||||
- starts at root and goes the tree up to the current page
|
||||
- flat ol/li list
|
||||
|
||||
```html
|
||||
<web-menu name="breadcrumb">
|
||||
<nav aria-label="Breadcrumb">
|
||||
<ol>
|
||||
<li class="web-menu-active"><a href="/">Home</a></li>
|
||||
<li class="web-menu-active"><a href="/components/">Components</a></li>
|
||||
<li class="web-menu-current">
|
||||
<a href="/components/button-blue/" aria-current="page">Button Blue</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</web-menu>
|
||||
```
|
||||
|
||||
4. **articleOverview**
|
||||
|
||||
- shows a flat list of pages
|
||||
- includes multiple meta data like cover image, heading, subHeading, authors, ...
|
||||
- typically displayed as a grid
|
||||
|
||||
```html
|
||||
<web-menu name="article-overview">
|
||||
<div>
|
||||
<article class="post">
|
||||
<div class="cover">
|
||||
<a href="/blog/new-year-new-challenge/" tabindex="-1" aria-hidden="true">
|
||||
<figure>
|
||||
<img src="..." />
|
||||
</figure>
|
||||
</a>
|
||||
</div>
|
||||
<a href="/blog/new-year-new-challenge/">
|
||||
<h2>New year means new challenges</h2>
|
||||
</a>
|
||||
<div class="description">
|
||||
<a href="/blog/new-year-new-challenge/" tabindex="-1">
|
||||
<p>It is a new year and there are new challenges awaiting.</p>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="post">
|
||||
<div class="cover">
|
||||
<a href="/blog/comparing-apple-to-oranges/" tabindex="-1" aria-hidden="true">
|
||||
<figure>
|
||||
<img src="..." />
|
||||
</figure>
|
||||
</a>
|
||||
</div>
|
||||
<a href="/blog/comparing-apple-to-oranges/">
|
||||
<h2>Comparing apple to oranges</h2>
|
||||
</a>
|
||||
<div class="description">
|
||||
<a href="/blog/comparing-apple-to-oranges/" tabindex="-1">
|
||||
<p>Say you have an apple and you then find an orange - what would you do?</p>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</web-menu>
|
||||
```
|
||||
|
||||
5. **tableOfContents**
|
||||
|
||||
- lists the headlines of the current page in a hierarchy
|
||||
- nested ol/li list
|
||||
|
||||
```html
|
||||
<web-menu name="table-of-contents">
|
||||
<aside>
|
||||
<h2>Contents</h2>
|
||||
<nav aria-label="Table of Contents">
|
||||
<ol class="lvl-2">
|
||||
<li>
|
||||
<a href="#every-headline">Every headline</a>
|
||||
<ol class="lvl-3">
|
||||
<li><a href="#will-be">will be</a></li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><a href="#to-the">to the</a></li>
|
||||
<li><a href="#main-level">main level</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
</aside>
|
||||
</web-menu>
|
||||
```
|
||||
|
||||
6. **next**
|
||||
|
||||
- shows the next page in the tree
|
||||
- is either the first child or he next sibling
|
||||
|
||||
```html
|
||||
<web-menu name="next">
|
||||
<a href="/second.html">
|
||||
<span>next</span>
|
||||
<span>Second</span>
|
||||
</a>
|
||||
</web-menu>
|
||||
```
|
||||
|
||||
7. **previous**
|
||||
|
||||
- shows the previous page in the tree
|
||||
- is either the previous sibling or the parent
|
||||
|
||||
```html
|
||||
<web-menu name="previous">
|
||||
<a href="/first.html">
|
||||
<span>previous</span>
|
||||
<span>First</span>
|
||||
</a>
|
||||
</web-menu>
|
||||
```
|
||||
24
packages/web-menu/TODO.md
Normal file
24
packages/web-menu/TODO.md
Normal file
@@ -0,0 +1,24 @@
|
||||
NICE TO HAVE:
|
||||
|
||||
- Contribute to tree-model and "modernize it" es6, esm, types https://github.com/joaonuno/tree-model-js/issues/84
|
||||
- Blog needs support for tags & hero image
|
||||
- Pagination
|
||||
|
||||
## Usage as a @web/dev-server users
|
||||
|
||||
Run it in parallel?
|
||||
|
||||
## Usage as an eleventy user
|
||||
|
||||
Run it in parallel?
|
||||
|
||||
## Usage as as ???
|
||||
|
||||
- astro
|
||||
- next.js
|
||||
- hugo
|
||||
- gatsby
|
||||
- jenkyll
|
||||
- nuxt
|
||||
- hexo
|
||||
- docusaurus
|
||||
6
packages/web-menu/index.js
Normal file
6
packages/web-menu/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export { buildTree } from './src/buildTree.js';
|
||||
export { findCurrentNode } from './src/findCurrentNode.js';
|
||||
export { WebMenuCli } from './src/WebMenuCli.js';
|
||||
export { webMenu } from './preset/webMenu.js';
|
||||
export { Menu } from './src/Menu.js';
|
||||
export { IndexMenu } from './src/menus/IndexMenu.js';
|
||||
44
packages/web-menu/package.json
Normal file
44
packages/web-menu/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@web/menu",
|
||||
"version": "0.0.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "A fast low dependency menu renderer for html sites",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/modernweb-dev/rocket.git",
|
||||
"directory": "packages/web-menu"
|
||||
},
|
||||
"author": "Modern Web <hello@modern-web.dev> (https://modern-web.dev/)",
|
||||
"homepage": "https://rocket.modern-web.dev/docs/tools/web-menu/",
|
||||
"main": "./index.js",
|
||||
"bin": {
|
||||
"web-menu": "src/cli.js"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha test-node/**/*.test.{js,cjs} test-node/*.test.{js,cjs}",
|
||||
"test:watch": "onchange 'src/**/*.{js,cjs}' 'test-node/**/*.{js,cjs}' -- npm test",
|
||||
"types:copy": "copyfiles \"./types/**/*.d.ts\" dist-types/"
|
||||
},
|
||||
"files": [
|
||||
"*.js",
|
||||
"dist",
|
||||
"dist-types",
|
||||
"preset",
|
||||
"src"
|
||||
],
|
||||
"dependencies": {
|
||||
"colorette": "^2.0.16",
|
||||
"commander": "^8.2.0",
|
||||
"plugins-manager": "^0.3.0",
|
||||
"sax-wasm": "^2.0.0",
|
||||
"tree-model": "^1.0.7"
|
||||
},
|
||||
"types": "dist-types/index.d.ts"
|
||||
}
|
||||
31
packages/web-menu/preset/RocketCliPlugin.js
Executable file
31
packages/web-menu/preset/RocketCliPlugin.js
Executable file
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
/** @typedef {import('@rocket/cli').RocketCliOptions} RocketCliOptions */
|
||||
|
||||
import { WebMenuCli } from '@web/menu';
|
||||
|
||||
export class RocketMenuCliPlugin {
|
||||
static pluginName = 'RocketMenu';
|
||||
commands = ['start', 'build', 'lint'];
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {RocketCliOptions} options.config
|
||||
* @param {any} options.argv
|
||||
*/
|
||||
async setup({ config, argv }) {
|
||||
this.__argv = argv;
|
||||
this.webMenu = new WebMenuCli();
|
||||
this.webMenu.setOptions({
|
||||
docsDir: config.outputDevDir,
|
||||
outputDir: config.outputDevDir,
|
||||
setupPlugins: config.setupMenus,
|
||||
});
|
||||
}
|
||||
|
||||
async updated() {
|
||||
if (this.webMenu) {
|
||||
await this.webMenu.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% if menu.exclude %}
|
||||
<meta name="menu:exclude" content="true" />
|
||||
{% endif %}
|
||||
|
||||
{% if menu.order %}
|
||||
<meta name="menu:order" content="{{ menu.order }}" />
|
||||
{% endif %}
|
||||
|
||||
{% if menu.linkText %}
|
||||
<meta name="menu:link.text" content="{{ menu.linkText }}" />
|
||||
{% endif %}
|
||||
13
packages/web-menu/preset/webMenu.js
Normal file
13
packages/web-menu/preset/webMenu.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { addPlugin } from 'plugins-manager';
|
||||
import { RocketMenuCliPlugin } from './RocketCliPlugin.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export function webMenu() {
|
||||
return {
|
||||
path: path.resolve(__dirname),
|
||||
setupCliPlugins: [addPlugin(RocketMenuCliPlugin)],
|
||||
};
|
||||
}
|
||||
79
packages/web-menu/src/Menu.js
Normal file
79
packages/web-menu/src/Menu.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/** @typedef {import('../types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
export class Menu {
|
||||
static type = 'menu';
|
||||
|
||||
/** @type {NodeOfPage | undefined} */
|
||||
currentNode = undefined;
|
||||
|
||||
options = {
|
||||
listTag: 'ul',
|
||||
};
|
||||
|
||||
constructor(options = {}) {
|
||||
this.options = { ...this.options, ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
childCondition(node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {string}
|
||||
*/
|
||||
list(node) {
|
||||
if (this.childCondition(node) && node.children) {
|
||||
const lvl = node.model.level;
|
||||
const { listTag } = this.options;
|
||||
return `
|
||||
<${listTag} class="lvl-${lvl + 1}">
|
||||
${node.children.map(child => this.listItem(child)).join('')}
|
||||
</${listTag}>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {string}
|
||||
*/
|
||||
listItem(node) {
|
||||
let cssClasses = '';
|
||||
if (node.model.active) {
|
||||
cssClasses = ' class="web-menu-active" ';
|
||||
}
|
||||
if (node.model.current) {
|
||||
cssClasses = ' class="web-menu-current" ';
|
||||
}
|
||||
return `<li${cssClasses}>${this.link(node)}${
|
||||
node.children && node.children.length > 0 ? this.list(node) : ''
|
||||
}</li>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {string}
|
||||
*/
|
||||
link(node) {
|
||||
const current = node === this.currentNode ? ' aria-current="page" ' : '';
|
||||
return `<a href="${node.model.url}"${current}>${node.model.name}</a>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async render(node) {
|
||||
return `
|
||||
<nav aria-label="index">
|
||||
${this.list(node)}
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
}
|
||||
114
packages/web-menu/src/WebMenuCli.js
Normal file
114
packages/web-menu/src/WebMenuCli.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
/** @typedef {import('../types/main').WebMenuCliOptions} WebMenuCliOptions */
|
||||
/** @typedef {import('../types/main').MetaPluginMenu} MetaPluginMenu */
|
||||
|
||||
import path from 'path';
|
||||
import { cyan, green, red } from 'colorette';
|
||||
import { Command } from 'commander/esm.mjs';
|
||||
|
||||
import { applyPlugins } from 'plugins-manager';
|
||||
|
||||
import { buildTree } from './buildTree.js';
|
||||
import { insertMenus } from './insertMenus.js';
|
||||
import { writeTreeToFileSystem } from './writeTreeToFileSystem.js';
|
||||
|
||||
import { Site } from './menus/Site.js';
|
||||
import { Breadcrumb } from './menus/Breadcrumb.js';
|
||||
import { Next } from './menus/Next.js';
|
||||
import { Previous } from './menus/Previous.js';
|
||||
import { ArticleOverview } from './menus/ArticleOverview.js';
|
||||
import { IndexMenu } from './menus/IndexMenu.js';
|
||||
import { TableOfContents } from './menus/TableOfContents.js';
|
||||
|
||||
/** @type {MetaPluginMenu[]} */
|
||||
const defaultPlugins = [
|
||||
{ plugin: Site, options: {} },
|
||||
{ plugin: Breadcrumb, options: {} },
|
||||
{ plugin: Next, options: {} },
|
||||
{ plugin: IndexMenu, options: {} },
|
||||
{ plugin: Previous, options: {} },
|
||||
{ plugin: ArticleOverview, options: {} },
|
||||
{ plugin: TableOfContents, options: {} },
|
||||
];
|
||||
|
||||
const program = new Command();
|
||||
|
||||
export class WebMenuCli {
|
||||
/** @type {WebMenuCliOptions} */
|
||||
options = {
|
||||
docsDir: process.cwd(),
|
||||
setupPlugins: [],
|
||||
};
|
||||
|
||||
constructor({ argv } = { argv: undefined }) {
|
||||
program
|
||||
.option('-d, --docs-dir <path>', 'path to where to search for source files')
|
||||
.option('-c, --config-file <path>', 'path to config file');
|
||||
|
||||
program.parse(argv);
|
||||
this.setOptions(program.opts());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Partial<WebMenuCliOptions>} newOptions
|
||||
*/
|
||||
setOptions(newOptions) {
|
||||
const setupPlugins = newOptions.setupPlugins
|
||||
? [...this.options.setupPlugins, ...newOptions.setupPlugins]
|
||||
: this.options.setupPlugins;
|
||||
|
||||
this.options = {
|
||||
...this.options,
|
||||
...newOptions,
|
||||
setupPlugins,
|
||||
};
|
||||
}
|
||||
|
||||
async applyConfigFile() {
|
||||
if (this.options.configFile) {
|
||||
const configFilePath = path.resolve(this.options.configFile);
|
||||
const fileOptions = (await import(configFilePath)).default;
|
||||
if (fileOptions.docsDir) {
|
||||
fileOptions.docsDir = path.join(path.dirname(configFilePath), fileOptions.docsDir);
|
||||
}
|
||||
this.setOptions(fileOptions);
|
||||
}
|
||||
}
|
||||
|
||||
async run() {
|
||||
await this.applyConfigFile();
|
||||
|
||||
this.options = applyPlugins(this.options, defaultPlugins);
|
||||
|
||||
const { docsDir: userDocsDir, outputDir: userOutputDir } = this.options;
|
||||
this.docsDir = userDocsDir ? path.resolve(userDocsDir) : this.docsDir;
|
||||
this.outputDir = userOutputDir
|
||||
? path.resolve(userOutputDir)
|
||||
: path.join(this.docsDir, '..', '_site');
|
||||
const performanceStart = process.hrtime();
|
||||
|
||||
const relPath = path.relative(process.cwd(), this.docsDir);
|
||||
console.log(`👀 Analyzing file tree at ${cyan(relPath)}`);
|
||||
const tree = await buildTree(this.docsDir);
|
||||
if (!tree) {
|
||||
console.error(red(`💥 Error: Could not find any pages at ${cyan(this.docsDir)}.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📖 Found ${green(tree.all().length)} pages`);
|
||||
|
||||
const { counter } = await insertMenus(tree, this.options);
|
||||
console.log(`📝 Inserted ${green(counter.toString())} menus!`);
|
||||
|
||||
console.log(`✍️ Writing files to ${cyan(path.relative(process.cwd(), this.outputDir))} ...`);
|
||||
await writeTreeToFileSystem(tree, this.outputDir);
|
||||
|
||||
const performance = process.hrtime(performanceStart);
|
||||
console.log(
|
||||
`✅ Menus inserted and written to filesystem. (executed in ${performance[0]}s ${
|
||||
performance[1] / 1000000
|
||||
}ms)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
151
packages/web-menu/src/buildTree.js
Normal file
151
packages/web-menu/src/buildTree.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { existsSync } from 'fs';
|
||||
import { readdir } from 'fs/promises';
|
||||
import TreeModel from 'tree-model';
|
||||
import path from 'path';
|
||||
import { parseHtmlFile } from './parseHtmlFile.js';
|
||||
|
||||
/** @typedef {import('../types/main').Page} Page */
|
||||
/** @typedef {import('../types/main').ParseMetaData} ParseMetaData */
|
||||
|
||||
/**
|
||||
* @param {Page} a
|
||||
* @param {Page} b
|
||||
* @returns
|
||||
*/
|
||||
export function modelComparatorFn(a, b) {
|
||||
const aOrder = a.order || 0;
|
||||
const bOrder = b.order || 0;
|
||||
return aOrder > bOrder;
|
||||
}
|
||||
|
||||
const tree = new TreeModel({
|
||||
modelComparatorFn,
|
||||
});
|
||||
|
||||
/** @type {string} */
|
||||
let initialRootDir;
|
||||
|
||||
/**
|
||||
* @param {ParseMetaData} metaData
|
||||
* @returns {ParseMetaData}
|
||||
*/
|
||||
function processTocElements(metaData) {
|
||||
let node;
|
||||
let currentLevel = 0;
|
||||
if (metaData.__tocElements && metaData.__tocElements.length > 0) {
|
||||
for (const tocElement of metaData.__tocElements) {
|
||||
const { id, text, level } = tocElement;
|
||||
const child = tree.parse({
|
||||
name: text,
|
||||
url: `#${id}`,
|
||||
level,
|
||||
});
|
||||
if (node) {
|
||||
if (level <= currentLevel) {
|
||||
node = node
|
||||
.getPath()
|
||||
.reverse()
|
||||
.find(n => n.model.level < child.model.level);
|
||||
}
|
||||
if (!node) {
|
||||
throw new Error(`Could not find an h1 in "${metaData.relPath}"`);
|
||||
}
|
||||
if (node) {
|
||||
node.addChild(child);
|
||||
}
|
||||
}
|
||||
currentLevel = level;
|
||||
node = child;
|
||||
}
|
||||
}
|
||||
|
||||
delete metaData.__tocElements;
|
||||
const root = node?.getPath()[0];
|
||||
if (root) {
|
||||
metaData.tableOfContentsNode = root;
|
||||
}
|
||||
return metaData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} process
|
||||
* @param {string} process.filePath
|
||||
* @param {string} process.rootDir
|
||||
* @param {TreeModel.Node<Page>} [process.currentNode]
|
||||
* @param {boolean} [process.recursive]
|
||||
* @param {object} options
|
||||
* @param {string} [options.mode]
|
||||
* @param {number} [options.level]
|
||||
* @param {string} [options.url]
|
||||
* @returns
|
||||
*/
|
||||
async function processFile({ filePath, rootDir, currentNode, recursive = false }, options) {
|
||||
if (filePath && existsSync(filePath)) {
|
||||
const { level = 0, url = '/' } = options;
|
||||
|
||||
const metaData = await parseHtmlFile(filePath, { rootDir: initialRootDir });
|
||||
if (!metaData.exclude) {
|
||||
const treeEntry = tree.parse({ level, url, ...processTocElements(metaData) });
|
||||
if (currentNode) {
|
||||
currentNode.addChild(treeEntry);
|
||||
} else {
|
||||
currentNode = treeEntry;
|
||||
}
|
||||
if (recursive) {
|
||||
await buildTree(rootDir, treeEntry, { ...options, level: level + 1, mode: 'scan' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} inRootDir
|
||||
* @param {TreeModel.Node<Page>} [node]
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.mode]
|
||||
* @param {number} [options.level]
|
||||
* @param {string} [options.url]
|
||||
* @returns
|
||||
*/
|
||||
export async function buildTree(inRootDir, node, options = {}) {
|
||||
const { mode = 'indexFile', level = 0, url = '/' } = options;
|
||||
const rootDir = path.resolve(inRootDir);
|
||||
if (level === 0) {
|
||||
initialRootDir = rootDir;
|
||||
}
|
||||
let currentNode = node;
|
||||
|
||||
if (mode === 'indexFile') {
|
||||
const indexFilePath = path.join(rootDir, 'index.html');
|
||||
currentNode = await processFile(
|
||||
{ filePath: indexFilePath, rootDir, currentNode, recursive: true },
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'scan') {
|
||||
const entries = await readdir(rootDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const { name: folderName } = entry;
|
||||
const currentPath = path.join(rootDir, folderName);
|
||||
if (entry.isDirectory()) {
|
||||
await buildTree(currentPath, currentNode, {
|
||||
...options,
|
||||
level,
|
||||
url: `${url}${folderName}/`,
|
||||
mode: 'indexFile',
|
||||
});
|
||||
} else if (entry.name !== 'index.html' && entry.name.endsWith('.html')) {
|
||||
const filePath = path.join(rootDir, entry.name);
|
||||
currentNode = await processFile(
|
||||
{ filePath, rootDir, currentNode },
|
||||
{ ...options, url: `${url}${entry.name}` },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentNode;
|
||||
}
|
||||
27
packages/web-menu/src/cli.js
Executable file
27
packages/web-menu/src/cli.js
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { WebMenuCli } from './WebMenuCli.js';
|
||||
|
||||
const cli = new WebMenuCli();
|
||||
|
||||
const cwd = process.cwd();
|
||||
const configFiles = [
|
||||
'config/web-menu.js',
|
||||
'config/web-menu.mjs',
|
||||
'web-menu.config.js',
|
||||
'web-menu.config.mjs',
|
||||
];
|
||||
|
||||
for (const configFile of configFiles) {
|
||||
const configFilePath = path.join(cwd, configFile);
|
||||
if (existsSync(configFilePath)) {
|
||||
cli.setOptions({
|
||||
configFile: configFilePath,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await cli.run();
|
||||
9
packages/web-menu/src/findCurrentNode.js
Normal file
9
packages/web-menu/src/findCurrentNode.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @typedef {import('..//types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {NodeOfPage|undefined}
|
||||
*/
|
||||
export function findCurrentNode(node) {
|
||||
return node.first(entry => entry.model.current === true);
|
||||
}
|
||||
68
packages/web-menu/src/insertMenus.js
Normal file
68
packages/web-menu/src/insertMenus.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { replaceBetween } from './sax-helpers.js';
|
||||
|
||||
/** @typedef {import('../types/main').NodeOfPage} NodeOfPage */
|
||||
/** @typedef {import('../types/main').WebMenuCliOptions} WebMenuCliOptions */
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} tree
|
||||
* @param {NodeOfPage} node
|
||||
*/
|
||||
function setCurrent(tree, node) {
|
||||
const currentNode = tree.first(entry => entry.model.relPath === node.model.relPath);
|
||||
if (currentNode) {
|
||||
currentNode.model.current = true;
|
||||
for (const parent of currentNode.getPath()) {
|
||||
parent.model.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} tree
|
||||
*/
|
||||
function removeCurrent(tree) {
|
||||
const currentNode = tree.first(entry => entry.model.current === true);
|
||||
if (currentNode) {
|
||||
currentNode.model.current = false;
|
||||
for (const parent of currentNode.getPath()) {
|
||||
parent.model.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} tree
|
||||
* @param {Partial<WebMenuCliOptions>} options
|
||||
*/
|
||||
export async function insertMenus(tree, options = {}) {
|
||||
let counter = 0;
|
||||
const { plugins } = options;
|
||||
if (!plugins) {
|
||||
return { counter: 0, tree };
|
||||
}
|
||||
|
||||
for (const node of tree.all()) {
|
||||
if (node.model.menus && node.model.menus.length > 0) {
|
||||
setCurrent(tree, node);
|
||||
|
||||
for (const menu of node.model.menus.reverse()) {
|
||||
counter += 1;
|
||||
const menuInst = plugins.find(pluginInst => pluginInst.constructor.type === menu.name);
|
||||
if (!menuInst) {
|
||||
throw new Error(`Unknown menu type: ${menu.name}`);
|
||||
}
|
||||
menuInst.currentNode = tree.first(entry => entry.model.current === true);
|
||||
const menuString = await menuInst.render(tree);
|
||||
node.model.fileString = replaceBetween({
|
||||
content: node.model.fileString,
|
||||
start: menu.start,
|
||||
end: menu.end,
|
||||
replacement: menuString,
|
||||
});
|
||||
}
|
||||
|
||||
removeCurrent(tree);
|
||||
}
|
||||
}
|
||||
return { counter, tree };
|
||||
}
|
||||
46
packages/web-menu/src/menus/ArticleOverview.js
Normal file
46
packages/web-menu/src/menus/ArticleOverview.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Menu } from '../Menu.js';
|
||||
|
||||
/** @typedef {import('../../types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
export class ArticleOverview extends Menu {
|
||||
static type = 'article-overview';
|
||||
|
||||
/** @param {NodeOfPage} node */
|
||||
renderDescription(node) {
|
||||
if (node.model.subHeading) {
|
||||
return `
|
||||
<div class="description">
|
||||
<a href="${node.model.url}" tabindex="-1">
|
||||
<p>${node.model.subHeading}</p>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async render() {
|
||||
if (!this.currentNode || !this.currentNode.children) {
|
||||
return '';
|
||||
}
|
||||
return `
|
||||
<div>
|
||||
${this.currentNode.children
|
||||
.map(
|
||||
child => `
|
||||
<article class="post">
|
||||
<a href="${child.model.url}">
|
||||
<h2>${child.model.name}</h2>
|
||||
</a>
|
||||
${this.renderDescription(child)}
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
32
packages/web-menu/src/menus/Breadcrumb.js
Normal file
32
packages/web-menu/src/menus/Breadcrumb.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Menu } from '../Menu.js';
|
||||
|
||||
/** @typedef {import('../../types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
export class Breadcrumb extends Menu {
|
||||
static type = 'breadcrumb';
|
||||
|
||||
childCondition() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async render(node) {
|
||||
const current = node.first(node => node.model.current === true);
|
||||
if (!current) {
|
||||
return '';
|
||||
}
|
||||
const nodePath = current.getPath();
|
||||
// /** @param {NodeOfPage} node */
|
||||
// const breadcrumbItem = node => this.listItem();
|
||||
return `
|
||||
<nav aria-label="Breadcrumb">
|
||||
<ol>
|
||||
${nodePath.map(node => this.listItem(node)).join('\n')}
|
||||
</ol>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
}
|
||||
68
packages/web-menu/src/menus/IndexMenu.js
Normal file
68
packages/web-menu/src/menus/IndexMenu.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Menu } from '../Menu.js';
|
||||
|
||||
/** @typedef {import('../../types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
export class IndexMenu extends Menu {
|
||||
static type = 'index';
|
||||
|
||||
options = {
|
||||
...this.options,
|
||||
/** @param {string} nav */
|
||||
navWrapper: nav => `<nav aria-label="index">${nav}</nav>`,
|
||||
};
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.options = { ...this.options, ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async render(node) {
|
||||
if (!this.currentNode) {
|
||||
return '';
|
||||
}
|
||||
const activeLevelTwo = this.currentNode.getPath()[1] || node;
|
||||
const { navWrapper } = this.options;
|
||||
return navWrapper(this.list(activeLevelTwo));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {string}
|
||||
*/
|
||||
link(node) {
|
||||
if (node.children && node.children.length > 0) {
|
||||
const lvl = node.model.level;
|
||||
return lvl < 3 ? `<span>${node.model.name}</span>` : '';
|
||||
}
|
||||
const current = node === this.currentNode ? ' aria-current="page" ' : '';
|
||||
return `<a href="${node.model.url}"${current}>${node.model.name}</a>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {string}
|
||||
*/
|
||||
list(node) {
|
||||
if (!this.currentNode) {
|
||||
return '';
|
||||
}
|
||||
const open = this.currentNode.getPath().includes(node) ? 'open' : '';
|
||||
|
||||
if (this.childCondition(node) && node.children) {
|
||||
const lvl = node.model.level;
|
||||
const { listTag } = this.options;
|
||||
return `
|
||||
${lvl > 2 ? `<details ${open}><summary>${node.model.name}</summary>` : ''}
|
||||
<${listTag} class="lvl-${lvl + 1}">
|
||||
${node.children.map(child => this.listItem(child)).join('')}
|
||||
</${listTag}>
|
||||
${lvl > 2 ? `</details>` : ''}
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
38
packages/web-menu/src/menus/Next.js
Normal file
38
packages/web-menu/src/menus/Next.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Menu } from '../Menu.js';
|
||||
|
||||
/** @typedef {import('../../types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
export class Next extends Menu {
|
||||
static type = 'next';
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async render() {
|
||||
if (!this.currentNode) {
|
||||
return '';
|
||||
}
|
||||
const parents = this.currentNode.getPath();
|
||||
let next;
|
||||
if (parents.length > 1) {
|
||||
const parent = parents[parents.length - 2];
|
||||
if (parent && parent.children) {
|
||||
next = parent.children[this.currentNode.getIndex() + 1];
|
||||
}
|
||||
}
|
||||
if (!next) {
|
||||
if (this.currentNode.children) {
|
||||
next = this.currentNode.children[0];
|
||||
}
|
||||
}
|
||||
if (next) {
|
||||
return `
|
||||
<a href="${next.model.url}">
|
||||
<span>next</span>
|
||||
<span>${next.model.name}</span>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
36
packages/web-menu/src/menus/Previous.js
Normal file
36
packages/web-menu/src/menus/Previous.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Menu } from '../Menu.js';
|
||||
|
||||
/** @typedef {import('../../types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
export class Previous extends Menu {
|
||||
static type = 'previous';
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async render() {
|
||||
if (!this.currentNode) {
|
||||
return '';
|
||||
}
|
||||
const parents = this.currentNode.getPath();
|
||||
let previous;
|
||||
if (parents.length > 1) {
|
||||
const parent = parents[parents.length - 2];
|
||||
if (parent && parent.children) {
|
||||
previous = parent.children[this.currentNode.getIndex() - 1];
|
||||
if (!previous) {
|
||||
previous = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previous) {
|
||||
return `
|
||||
<a href="${previous.model.url}">
|
||||
<span>previous</span>
|
||||
<span>${previous.model.name}</span>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
22
packages/web-menu/src/menus/Site.js
Normal file
22
packages/web-menu/src/menus/Site.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Menu } from '../Menu.js';
|
||||
|
||||
/** @typedef {import('../../types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
export class Site extends Menu {
|
||||
static type = 'site';
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} node
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async render(node) {
|
||||
if (!node.children) {
|
||||
return '';
|
||||
}
|
||||
return `
|
||||
<nav aria-label="site">
|
||||
${node.children.map(child => this.link(child)).join('\n')}
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
}
|
||||
43
packages/web-menu/src/menus/TableOfContents.js
Normal file
43
packages/web-menu/src/menus/TableOfContents.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Menu } from '../Menu.js';
|
||||
|
||||
/** @typedef {import('../../types/main').NodeOfPage} NodeOfPage */
|
||||
|
||||
export class TableOfContents extends Menu {
|
||||
static type = 'table-of-contents';
|
||||
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
this.options = {
|
||||
navLabel: 'Table of Contents',
|
||||
navHeader: '<h2>Contents</h2>',
|
||||
/** @param {string} nav */
|
||||
navWrapper: nav => `<aside>${nav}</aside>`,
|
||||
listTag: 'ol',
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async render() {
|
||||
if (!this.currentNode) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
this.currentNode.model.tableOfContentsNode &&
|
||||
this.currentNode.model.tableOfContentsNode.children.length > 0
|
||||
) {
|
||||
const { navHeader, navLabel, navWrapper } = this.options;
|
||||
|
||||
const navString = `
|
||||
${navHeader}
|
||||
<nav aria-label="${navLabel}">
|
||||
${this.list(this.currentNode.model.tableOfContentsNode)}
|
||||
</nav>
|
||||
`;
|
||||
return navWrapper(navString);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
124
packages/web-menu/src/parseHtmlFile.js
Normal file
124
packages/web-menu/src/parseHtmlFile.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import saxWasm from 'sax-wasm';
|
||||
import { createRequire } from 'module';
|
||||
import { getAttribute, getText } from './sax-helpers.js';
|
||||
|
||||
/** @typedef {import('sax-wasm').Text} Text */
|
||||
/** @typedef {import('sax-wasm').Tag} Tag */
|
||||
/** @typedef {import('sax-wasm').Position} Position */
|
||||
/** @typedef {import('../types/main').ParseMetaData} ParseMetaData */
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { SaxEventType, SAXParser } = saxWasm;
|
||||
|
||||
const streamOptions = { highWaterMark: 256 * 1024 };
|
||||
|
||||
const saxPath = require.resolve('sax-wasm/lib/sax-wasm.wasm');
|
||||
const saxWasmBuffer = fs.readFileSync(saxPath);
|
||||
const parser = new SAXParser(SaxEventType.CloseTag | SaxEventType.Comment, streamOptions);
|
||||
|
||||
await parser.prepareWasm(saxWasmBuffer);
|
||||
|
||||
/**
|
||||
* @param {Tag} data
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isHeadline(data) {
|
||||
return data.name
|
||||
? data.name[0] === 'h' && ['1', '2', '3', '4', '5', '6'].includes(data.name[1])
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} htmlFilePath
|
||||
* @param {object} options
|
||||
* @param {string} options.rootDir
|
||||
* @returns
|
||||
*/
|
||||
export function parseHtmlFile(htmlFilePath, options) {
|
||||
const relPath = path.relative(options.rootDir, htmlFilePath);
|
||||
/** @type {ParseMetaData} */
|
||||
const metaData = {
|
||||
menus: [],
|
||||
relPath,
|
||||
__tocElements: [],
|
||||
};
|
||||
|
||||
parser.eventHandler = (ev, _data) => {
|
||||
if (ev === SaxEventType.CloseTag) {
|
||||
const data = /** @type {Tag} */ (/** @type {any} */ (_data));
|
||||
if (data.name === 'meta') {
|
||||
const metaName = getAttribute(data, 'name');
|
||||
if (metaName === 'menu:link.text') {
|
||||
metaData.metaLinkText = getAttribute(data, 'content');
|
||||
}
|
||||
if (metaName === 'menu:page.releaseDateTime') {
|
||||
const dtString = getAttribute(data, 'content');
|
||||
if (dtString) {
|
||||
const date = new Date(dtString);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth().toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0'); // getDay === week of the day
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
metaData.order = parseInt(`-${year}${month}${day}${hours}${minutes}`);
|
||||
metaData.releaseDateTime = dtString;
|
||||
}
|
||||
}
|
||||
if (metaName === 'menu:page.subHeading') {
|
||||
metaData.subHeading = getAttribute(data, 'content');
|
||||
}
|
||||
if (metaName === 'menu:order') {
|
||||
metaData.order = parseInt(getAttribute(data, 'content') || '0');
|
||||
}
|
||||
if (metaName === 'menu:exclude') {
|
||||
metaData.exclude = getAttribute(data, 'content') !== 'false';
|
||||
}
|
||||
}
|
||||
if (!metaData.title && data.name === 'title') {
|
||||
metaData.title = getText(data);
|
||||
}
|
||||
if (!metaData.h1 && data.name === 'h1') {
|
||||
metaData.h1 = getText(data);
|
||||
}
|
||||
|
||||
if (isHeadline(data)) {
|
||||
const id = getAttribute(data, 'id');
|
||||
const text = getText(data);
|
||||
if (id && text && metaData.__tocElements) {
|
||||
metaData.__tocElements.push({
|
||||
text,
|
||||
id,
|
||||
level: parseInt(data.name[1], 10),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.name === 'web-menu') {
|
||||
const name = getAttribute(data, 'name');
|
||||
if (name) {
|
||||
metaData.menus.push({ name, start: data.openEnd, end: data.closeStart });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise(resolve => {
|
||||
/** @type {Array<Buffer>} */
|
||||
const chunks = [];
|
||||
const readable = fs.createReadStream(htmlFilePath, streamOptions);
|
||||
readable.on('data', chunk => {
|
||||
parser.write(/** @type {Buffer} */ (chunk));
|
||||
chunks.push(Buffer.from(chunk));
|
||||
});
|
||||
readable.on('end', () => {
|
||||
parser.end();
|
||||
metaData.name = metaData.metaLinkText || metaData.h1 || metaData.title;
|
||||
metaData.fileString = Buffer.concat(chunks).toString('utf8');
|
||||
|
||||
resolve(metaData);
|
||||
});
|
||||
});
|
||||
}
|
||||
46
packages/web-menu/src/sax-helpers.js
Normal file
46
packages/web-menu/src/sax-helpers.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/** @typedef {import('sax-wasm').Text} Text */
|
||||
/** @typedef {import('sax-wasm').Tag} Tag */
|
||||
/** @typedef {import('sax-wasm').Position} Position */
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.content
|
||||
* @param {Position} options.start
|
||||
* @param {Position} options.end
|
||||
* @param {string} options.replacement
|
||||
*/
|
||||
export function replaceBetween({ content, start, end, replacement = '' }) {
|
||||
const lines = content.split('\n');
|
||||
const i = start.line;
|
||||
const line = lines[i];
|
||||
const upToChange = line.slice(0, start.character);
|
||||
const afterChange = line.slice(end.character);
|
||||
|
||||
lines[i] = `${upToChange}${replacement}${afterChange}`;
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Tag} data
|
||||
* @param {string} name
|
||||
*/
|
||||
export function getAttribute(data, name) {
|
||||
if (data.attributes) {
|
||||
const { attributes } = data;
|
||||
const foundIndex = attributes.findIndex(entry => entry.name.value === name);
|
||||
if (foundIndex !== -1) {
|
||||
return attributes[foundIndex].value.value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Tag} data
|
||||
*/
|
||||
export function getText(data) {
|
||||
if (data.textNodes) {
|
||||
return data.textNodes.map(textNode => textNode.value).join('');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
21
packages/web-menu/src/writeTreeToFileSystem.js
Normal file
21
packages/web-menu/src/writeTreeToFileSystem.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { existsSync } from 'fs';
|
||||
import { writeFile, mkdir } from 'fs/promises';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
/** @typedef {import('../types/main').Page} Page */
|
||||
/** @typedef {import('tree-model/types').Node<Page>} NodeOfPage */
|
||||
|
||||
/**
|
||||
* @param {NodeOfPage} tree
|
||||
* @param {string} outputDir
|
||||
*/
|
||||
export async function writeTreeToFileSystem(tree, outputDir) {
|
||||
for (const node of tree.all()) {
|
||||
const filePath = join(outputDir, node.model.relPath);
|
||||
const dir = dirname(filePath);
|
||||
if (!existsSync(dir)) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
await writeFile(filePath, node.model.fileString);
|
||||
}
|
||||
}
|
||||
258
packages/web-menu/test-node/buildTree.test.js
Normal file
258
packages/web-menu/test-node/buildTree.test.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import chai from 'chai';
|
||||
import { executeBuildTree, cleanup, modelComparatorFn } from './test-helpers.js';
|
||||
import TreeModel from 'tree-model';
|
||||
|
||||
const { expect } = chai;
|
||||
const treeModel = new TreeModel({ modelComparatorFn });
|
||||
|
||||
describe('buildTree', () => {
|
||||
it('builds a tree for one nested page', async () => {
|
||||
const tree = await executeBuildTree('fixtures/two-pages');
|
||||
|
||||
const twoLevels = treeModel.parse({
|
||||
name: 'Home',
|
||||
level: 0,
|
||||
h1: 'Welcome to two pages',
|
||||
metaLinkText: 'Home',
|
||||
title: 'Welcome to two pages | My Page',
|
||||
url: '/',
|
||||
menus: [
|
||||
{
|
||||
end: {
|
||||
character: 28,
|
||||
line: 7,
|
||||
},
|
||||
name: 'site',
|
||||
start: {
|
||||
character: 28,
|
||||
line: 7,
|
||||
},
|
||||
},
|
||||
],
|
||||
relPath: 'index.html',
|
||||
fileString:
|
||||
'<html>\n <head>\n <title>Welcome to two pages | My Page</title>\n <meta name="menu:link.text" content="Home">\n </head>\n <body>\n <header>\n <web-menu name="site"></web-menu>\n </header>\n <main>\n <h1>Welcome to two pages</h1>\n Content\n </main>\n </body>\n</html>\n',
|
||||
children: [
|
||||
{
|
||||
name: 'About Us',
|
||||
url: '/about/',
|
||||
level: 1,
|
||||
title: 'About Us',
|
||||
menus: [
|
||||
{
|
||||
end: {
|
||||
character: 28,
|
||||
line: 6,
|
||||
},
|
||||
name: 'site',
|
||||
start: {
|
||||
character: 28,
|
||||
line: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
relPath: 'about/index.html',
|
||||
fileString:
|
||||
'<html>\n <head>\n <title>About Us</title>\n </head>\n <body>\n <header>\n <web-menu name="site"></web-menu>\n </header>\n <main>\n Content\n </main>\n </body>\n</html>\n',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(tree).to.deep.equal(twoLevels);
|
||||
});
|
||||
|
||||
it('builds a tree and treats named html files as children of the index', async () => {
|
||||
const tree = await executeBuildTree('fixtures/build-named-html-files');
|
||||
|
||||
const twoLevels = treeModel.parse({
|
||||
name: 'Home',
|
||||
level: 0,
|
||||
h1: 'Welcome to two pages',
|
||||
metaLinkText: 'Home',
|
||||
title: 'Welcome to two pages | My Page',
|
||||
url: '/',
|
||||
menus: [
|
||||
{
|
||||
end: {
|
||||
character: 28,
|
||||
line: 7,
|
||||
},
|
||||
name: 'site',
|
||||
start: {
|
||||
character: 28,
|
||||
line: 7,
|
||||
},
|
||||
},
|
||||
],
|
||||
relPath: 'index.html',
|
||||
fileString:
|
||||
'<html>\n <head>\n <title>Welcome to two pages | My Page</title>\n <meta name="menu:link.text" content="Home">\n </head>\n <body>\n <header>\n <web-menu name="site"></web-menu>\n </header>\n <main>\n <h1>Welcome to two pages</h1>\n Content\n </main>\n </body>\n</html>\n',
|
||||
children: [
|
||||
{
|
||||
name: 'About Us',
|
||||
url: '/about.html',
|
||||
level: 1,
|
||||
title: 'About Us',
|
||||
menus: [
|
||||
{
|
||||
end: {
|
||||
character: 28,
|
||||
line: 6,
|
||||
},
|
||||
name: 'site',
|
||||
start: {
|
||||
character: 28,
|
||||
line: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
relPath: 'about.html',
|
||||
fileString:
|
||||
'<html>\n <head>\n <title>About Us</title>\n </head>\n <body>\n <header>\n <web-menu name="site"></web-menu>\n </header>\n <main>\n Content\n </main>\n </body>\n</html>\n',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(tree).to.deep.equal(twoLevels);
|
||||
});
|
||||
|
||||
it('builds a tree for multiple nested page', async () => {
|
||||
const tree = await executeBuildTree('fixtures/nested-pages');
|
||||
|
||||
const nested = treeModel.parse({
|
||||
name: 'Home',
|
||||
level: 0,
|
||||
h1: 'Welcome to two pages',
|
||||
metaLinkText: 'Home',
|
||||
title: 'Welcome to two pages | My Page',
|
||||
url: '/',
|
||||
relPath: 'index.html',
|
||||
children: [
|
||||
{
|
||||
name: 'About Us',
|
||||
url: '/about/',
|
||||
relPath: 'about/index.html',
|
||||
level: 1,
|
||||
title: 'About Us',
|
||||
children: [
|
||||
{
|
||||
name: 'Career',
|
||||
url: '/about/career/',
|
||||
relPath: 'about/career/index.html',
|
||||
level: 2,
|
||||
title: 'Career',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Components',
|
||||
url: '/components/',
|
||||
relPath: 'components/index.html',
|
||||
level: 1,
|
||||
title: 'Components',
|
||||
children: [
|
||||
{
|
||||
name: 'Button Blue',
|
||||
url: '/components/button-blue/',
|
||||
relPath: 'components/button-blue/index.html',
|
||||
level: 2,
|
||||
title: 'Button Blue',
|
||||
},
|
||||
{
|
||||
name: 'Button Red',
|
||||
url: '/components/button-red/',
|
||||
relPath: 'components/button-red/index.html',
|
||||
level: 2,
|
||||
title: 'Button Red',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(cleanup(tree)).to.deep.equal(nested);
|
||||
});
|
||||
|
||||
it('adds info about table of content', async () => {
|
||||
const tree = await executeBuildTree('fixtures/preset-tableOfContents');
|
||||
|
||||
const toc = treeModel.parse({
|
||||
name: 'Welcome to the table of contents preset',
|
||||
level: 0,
|
||||
h1: 'Welcome to the table of contents preset',
|
||||
title: 'Welcome to the toc preset | My Page',
|
||||
url: '/',
|
||||
relPath: 'index.html',
|
||||
children: [
|
||||
{
|
||||
h1: 'Empty because no sub headlines',
|
||||
level: 1,
|
||||
name: 'Empty because no sub headlines',
|
||||
relPath: 'empty/index.html',
|
||||
title: 'Welcome to the toc preset | My Page',
|
||||
url: '/empty/',
|
||||
},
|
||||
],
|
||||
tableOfContentsNode: treeModel.parse({
|
||||
name: 'Welcome to the table of contents preset',
|
||||
level: 1,
|
||||
url: '#welcome-to-the-table-of-contents-preset',
|
||||
children: [
|
||||
{
|
||||
name: 'Every headline',
|
||||
url: '#every-headline',
|
||||
level: 2,
|
||||
children: [
|
||||
{
|
||||
name: 'will be',
|
||||
url: '#will-be',
|
||||
level: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'listed',
|
||||
url: '#listed',
|
||||
level: 2,
|
||||
children: [
|
||||
{
|
||||
name: 'considering',
|
||||
url: '#considering',
|
||||
level: 3,
|
||||
children: [
|
||||
{
|
||||
name: 'nesting',
|
||||
url: '#nesting',
|
||||
level: 4,
|
||||
},
|
||||
{
|
||||
name: 'and',
|
||||
url: '#and',
|
||||
level: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'returning',
|
||||
url: '#returning',
|
||||
level: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'to the',
|
||||
url: '#to-the',
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
name: 'main level',
|
||||
url: '#main-level',
|
||||
level: 2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(cleanup(tree)).to.deep.equal(toc);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user