diff --git a/packages/gem-book/docs/en/002-guide/005-theme.md b/packages/gem-book/docs/en/002-guide/005-theme.md index 95503e01..3ee4c2cf 100644 --- a/packages/gem-book/docs/en/002-guide/005-theme.md +++ b/packages/gem-book/docs/en/002-guide/005-theme.md @@ -19,11 +19,11 @@ Of course, you can also set the theme directly using the DOM API of `` -```js DOM +```js vanilla.js new GemBookElement(config, theme); ``` -```js Lit +```js lit html``; ``` diff --git a/packages/gem-book/docs/en/002-guide/007-extension.md b/packages/gem-book/docs/en/002-guide/007-extension.md index d00e2de3..a7e20420 100644 --- a/packages/gem-book/docs/en/002-guide/007-extension.md +++ b/packages/gem-book/docs/en/002-guide/007-extension.md @@ -95,11 +95,11 @@ import plugin: -```bash CLI +```bash cli gem-book docs --plugin raw ``` -```html HTML +```html html ``` diff --git a/packages/gem-book/docs/zh/002-guide/003-cli.md b/packages/gem-book/docs/zh/002-guide/003-cli.md index a5df7f83..5fe3e139 100644 --- a/packages/gem-book/docs/zh/002-guide/003-cli.md +++ b/packages/gem-book/docs/zh/002-guide/003-cli.md @@ -75,6 +75,10 @@ npx gem-book docs \ 指定文档目录是否支持[国际化](./002-i18n.md)。 +#### `--ignored ` + +忽略的文件,使用 [`anymatch`](https://github.com/micromatch/anymatch) 格式,默认 `**/node_modules/**` + #### `--plugin ` 加载插件,可添加多个。也可以使用 URL 或者相对路径提供自定义插件,支持 TypeScript 模块。 diff --git a/packages/gem-book/docs/zh/002-guide/005-theme.md b/packages/gem-book/docs/zh/002-guide/005-theme.md index 91267329..b9ae7d07 100644 --- a/packages/gem-book/docs/zh/002-guide/005-theme.md +++ b/packages/gem-book/docs/zh/002-guide/005-theme.md @@ -19,11 +19,11 @@ gem-book docs --theme dark -```js DOM +```js vanilla.js new GemBookElement(config, theme); ``` -```js Lit +```js lit html``; ``` diff --git a/packages/gem-book/docs/zh/002-guide/007-extension.md b/packages/gem-book/docs/zh/002-guide/007-extension.md index 4b361f21..9f5e4556 100644 --- a/packages/gem-book/docs/zh/002-guide/007-extension.md +++ b/packages/gem-book/docs/zh/002-guide/007-extension.md @@ -71,11 +71,11 @@ GemBook 使用自定义元素作为插件系统,他们可以自定义渲染 Ma -```bash CLI +```bash cli gem-book docs --plugin raw ``` -```html HTML +```html html ``` @@ -90,8 +90,15 @@ gem-book docs --plugin raw 在[这里](../003-plugins.md)查看所有内置插件。 > [!TIP] -> 在 Markdown 中使用插件时 Attribute 不应该换行,否则会作为内联元素被 `

` 标签打断。 -> GemBook 内置插件支持自动导入,缺点是渲染文档后才会加载,有可能页面会闪烁。 +> +> 1. 在 Markdown 中使用插件时 Attribute 不应该换行,否则会作为内联元素被 `

` 标签打断。 +> 2. GemBook 内置插件支持自动导入,缺点是渲染文档后才会加载,有可能页面会闪烁。 +> 3. VSCode 默认不能在 MarkDown 文件中使用 [Emmet](https://code.visualstudio.com/docs/editor/emmet),你可以通过设置启用: +> +> ```json +> "emmet.excludeLanguages": [], +> "emmet.includeLanguages": {"markdown": "html"}, +> ``` ### 开发插件 diff --git a/packages/gem-book/docs/zh/003-plugins.md b/packages/gem-book/docs/zh/003-plugins.md index 12904b27..d26c4ed2 100644 --- a/packages/gem-book/docs/zh/003-plugins.md +++ b/packages/gem-book/docs/zh/003-plugins.md @@ -10,11 +10,11 @@ isNav: true -```bash NPM +```bash npm npm i gem-book ``` -```bash YARN +```bash yarn yarn add gem-book ``` @@ -23,11 +23,11 @@ yarn add gem-book ````md -```bash NPM +```bash npm npm i gem-book ``` -```bash YARN +```bash yarn yarn add gem-book ``` @@ -36,16 +36,26 @@ yarn add gem-book ## `` -用于显示远端代码,如果提供的 `src` 只包含路径,则会从当前项目的 GitHub 上读取内容(受 [`sourceDir`](./002-guide/003-cli.md#--source-dir),[`sourceBranch`](./002-guide/003-cli.md#--source-branch) 影响),比如: +用于显示远端代码,如果提供的 `src` 只包含路径,则会从当前项目的 GitHub 上读取内容(受 [`sourceDir`](./002-guide/003-cli.md#--source-dir),[`sourceBranch`](./002-guide/003-cli.md#--source-branch) 影响),例如: - + ```md - + - + ``` +## `` + +引用全局变量:hello + +```md +hello +``` + +变量需要在[配置文件](./002-guide/003-cli.md)中定义。 + ## `` 显示远端多媒体内容,比如图片,视频,获取资源方式和 `` 一样: @@ -54,6 +64,18 @@ yarn add gem-book ``` +## `` + +动态加载显示 MarkDown 片段: + + + +```md + + + +``` + ## `` 动态导入模块,这可以用来按需加载插件,比如下面这个自定义元素是动态(`.ts` 文件会使用 [esm.sh](https://esm.sh/) 编译 )加载的: diff --git a/packages/gem-book/gem-book.cli.json b/packages/gem-book/gem-book.cli.json index 39fe5632..cf112783 100644 --- a/packages/gem-book/gem-book.cli.json +++ b/packages/gem-book/gem-book.cli.json @@ -8,5 +8,8 @@ "plugin": ["raw", "docsearch", "api", "sandpack", "code-group", "import"], "template": "docs/template.html", "ga": "G-PZYZ441YD3", - "debug": true + "debug": true, + "global": { + "hello": "Hello World!" + } } diff --git a/packages/gem-book/package.json b/packages/gem-book/package.json index 869fcaf6..85f14c5a 100644 --- a/packages/gem-book/package.json +++ b/packages/gem-book/package.json @@ -1,6 +1,6 @@ { "name": "gem-book", - "version": "1.5.31", + "version": "1.5.32", "description": "Create your document website easily and quickly", "keywords": [ "doc", diff --git a/packages/gem-book/schema.json b/packages/gem-book/schema.json index 16e34a15..4e8cc1d1 100644 --- a/packages/gem-book/schema.json +++ b/packages/gem-book/schema.json @@ -29,6 +29,7 @@ "github": { "type": "string" }, + "global": {}, "homeMode": { "type": "boolean" }, @@ -38,6 +39,12 @@ "icon": { "type": "string" }, + "ignored": { + "items": { + "type": "string" + }, + "type": "array" + }, "json": { "type": "boolean" }, diff --git a/packages/gem-book/src/common/config.ts b/packages/gem-book/src/common/config.ts index 916fa730..81a13250 100644 --- a/packages/gem-book/src/common/config.ts +++ b/packages/gem-book/src/common/config.ts @@ -24,6 +24,7 @@ interface CommonConfig { displayRank?: boolean; homeMode?: boolean; footer?: string; + global?: any; } export type BookConfig = { diff --git a/packages/gem-book/src/element/elements/homepage.ts b/packages/gem-book/src/element/elements/homepage.ts index 33233dbb..98704d28 100644 --- a/packages/gem-book/src/element/elements/homepage.ts +++ b/packages/gem-book/src/element/elements/homepage.ts @@ -2,7 +2,7 @@ import { html, GemElement, customElement, part, connectStore, css } from '@manto import { mediaQuery } from '@mantou/gem/helper/mediaquery'; import { theme } from '../helper/theme'; -import { getRemotePath, getUserLink, NavItemWithLink } from '../lib/utils'; +import { getUserLink, joinPath, NavItemWithLink } from '../lib/utils'; import { bookStore } from '../store'; import { icons } from './icons'; @@ -167,8 +167,7 @@ export class Homepage extends GemElement { ${feature.icon ? html`` : ''}

${feature.title}
diff --git a/packages/gem-book/src/element/elements/main.ts b/packages/gem-book/src/element/elements/main.ts index acd60e5c..aadcaaa1 100644 --- a/packages/gem-book/src/element/elements/main.ts +++ b/packages/gem-book/src/element/elements/main.ts @@ -26,19 +26,23 @@ const parser = new DOMParser(); // https://github.com/w3c/csswg-drafts/issues/9712 const style = createCSSSheet(css` - :not(:defined) { - display: block; + :not(gbp-var):not(:defined) { + display: contents; + color: transparent; + /* maybe browser limit */ font-size: 0; } - :not(:defined) * { + :not(gbp-var):not(:defined) * { display: none; } - :not(:defined)::before { + :not(gbp-var):not(:defined)::before { font-size: 1rem; display: block; content: 'The element is not defined'; - padding: 1em; + padding: 2rem; + margin-block: 2rem; text-align: center; + color: ${theme.textColor}; background: ${theme.borderColor}; border-radius: ${theme.normalRound}; } @@ -350,7 +354,10 @@ export class Main extends GemElement { this.#updateToc(); - const mo = new MutationObserver(this.#updateToc); + const mo = new MutationObserver(() => { + checkBuiltInPlugin(this.shadowRoot!); + this.#updateToc(); + }); mo.observe(this.shadowRoot!, { childList: true, subtree: true, diff --git a/packages/gem-book/src/element/elements/meta.ts b/packages/gem-book/src/element/elements/meta.ts index 01208938..dae468fa 100644 --- a/packages/gem-book/src/element/elements/meta.ts +++ b/packages/gem-book/src/element/elements/meta.ts @@ -1,7 +1,7 @@ import { connectStore, customElement, GemElement, html } from '@mantou/gem'; import { mediaQuery } from '@mantou/gem/helper/mediaquery'; -import { getRemotePath, getURL } from '../lib/utils'; +import { getURL, joinPath } from '../lib/utils'; import { themeStore } from '../helper/theme'; import { bookStore, locationStore } from '../store'; @@ -10,7 +10,7 @@ import '@mantou/gem/elements/reflect'; function getAlternateUrl(lang: string, pathname?: string) { const { origin } = location; const { path, query, hash } = locationStore; - const fullPath = getRemotePath(pathname || path, lang); + const fullPath = joinPath(lang, pathname || path); if (pathname) return `${origin}${fullPath}`; return `${origin}${fullPath}${query}${hash}`; } @@ -36,7 +36,10 @@ export class Meta extends GemElement { ? null : currentLinks ?.filter((e) => e.type === 'file') - .map(({ originLink, hash }) => html``)} + .map( + ({ originLink, hash }) => + html``, + )} diff --git a/packages/gem-book/src/element/elements/plugin.ts b/packages/gem-book/src/element/elements/plugin.ts index 29ee40d5..86428a10 100644 --- a/packages/gem-book/src/element/elements/plugin.ts +++ b/packages/gem-book/src/element/elements/plugin.ts @@ -1,6 +1,5 @@ import { GemElement, connectStore, globalemitter, Emitter, customElement } from '@mantou/gem'; import * as Gem from '@mantou/gem'; -import { marked } from 'marked'; import { mediaQuery } from '@mantou/gem/helper/mediaquery'; import { logger } from '@mantou/gem/helper/logger'; @@ -8,12 +7,51 @@ import { theme } from '../helper/theme'; import { bookStore, locationStore } from '../store'; import { BookConfig } from '../../common/config'; import { icons } from '../elements/icons'; +import { getRanges, getParts, joinPath, getURL } from '../lib/utils'; + +import { Main } from './main'; + +/** + * 获取资源的远端 GitHub raw 地址,如果使用 `DEV_MODE`,则返回本机服务的 URL + * + * - 支持相对路径 + * - `/docs/readme.md` 和 `docs/readme.md` 等效 + */ +function getRemoteURL(originSrc = '', dev = GemBookPluginElement.devMode) { + const { currentLink, lang, config, links } = GemBookPluginElement; + const { github, sourceBranch, sourceDir, base } = config; + const { originLink } = currentLink!; + let url = originSrc; + if (originSrc && !/^(https?:)?\/\//.test(originSrc)) { + if (!github || !sourceBranch) return ''; + const rawOrigin = 'https://raw.githubusercontent.com'; + const repo = new URL(github).pathname; + let src = originSrc.startsWith('/') ? originSrc : `/${originSrc}`; + if (originSrc.startsWith('.')) { + const absPath = new URL(originSrc, `${location.origin}${originLink}`).pathname; + const link = links?.find(({ originLink }) => originLink === absPath); + if (link) return getURL(joinPath(lang, link.originLink), link.hash); + src = new URL(originSrc, `${location.origin}${joinPath(sourceDir, lang, originLink)}`).pathname; + } + url = dev ? `/_assets${src}` : `${rawOrigin}${repo}/${sourceBranch}${joinPath(base, src)}`; + } + return url; +} @connectStore(bookStore) @customElement('gem-book-plugin') export class GemBookPluginElement extends GemElement { - static marked = marked; static Gem = Gem; + static Utils = { + getRanges, + getParts, + getRemoteURL, + parseMarkdown(md: string) { + return Main.parseMarkdown(md); + }, + }; + + static caches = new Map>(); static theme = theme; static icons = icons; static mediaQuery = mediaQuery; @@ -26,6 +64,7 @@ export class GemBookPluginElement extends GemElement { }, }, ); + static get links() { return bookStore.links; } @@ -57,36 +96,31 @@ export class GemBookPluginElement extends GemElement { return bookStore.getCurrentLink?.(); } - static caches = new Map>(); + @globalemitter error: Emitter = logger.error; - cacheState(getDeps: () => string[]) { + /** + * 自动缓存状态,下次挂载时应用状态 + * + * @example + * ```js + * constructor() { + * super(); + * this.cacheState(() => [this.src, this.range]); + *} + * ``` + */ + cacheState(getDeps: () => (string | number | undefined | null)[]) { if (!this.state) throw new Error('Only cache state'); const cons = this.constructor as typeof GemBookPluginElement; const cache = cons.caches.get(cons) || new Map(); cons.caches.set(cons, cache); this.memo( () => { - Object.assign(this.state!, cache.get(getDeps().join())); - return () => cache.set(getDeps().join(), this.state); + const key = locationStore.path + getDeps().join(); + Object.assign(this.state!, cache.get(key)); + return () => cache.set(key, this.state); }, () => getDeps(), ); } - - @globalemitter error: Emitter = logger.error; - - /**获取资源的远端 GitHub raw 地址,如果使用 `DEV_MODE`,则返回本机服务的 URL */ - getRemoteURL(originSrc = '', dev = GemBookPluginElement.devMode) { - const config = GemBookPluginElement.config; - let url = originSrc; - if (originSrc && !/^(https?:)?\/\//.test(originSrc)) { - if (!config.github || !config.sourceBranch) return ''; - const rawOrigin = 'https://raw.githubusercontent.com'; - const repo = new URL(config.github).pathname; - const src = `${originSrc.startsWith('/') ? '' : '/'}${originSrc}`; - const basePath = config.base ? `/${config.base}` : ''; - url = dev ? `/_assets${src}` : `${rawOrigin}${repo}/${config.sourceBranch}${basePath}${src}`; - } - return url; - } } diff --git a/packages/gem-book/src/element/elements/pre.ts b/packages/gem-book/src/element/elements/pre.ts index 0c095f62..77e59238 100644 --- a/packages/gem-book/src/element/elements/pre.ts +++ b/packages/gem-book/src/element/elements/pre.ts @@ -3,6 +3,7 @@ import { GemElement, html } from '@mantou/gem/lib/element'; import { styleMap } from '@mantou/gem/lib/utils'; import { theme } from '../helper/theme'; +import { getParts, getRanges } from '../lib/utils'; const prismjs = 'https://esm.sh/prismjs@v1.26.0'; @@ -246,10 +247,10 @@ export class Pre extends GemElement { this.memo( () => { const lines = (this.textContent || '').split(/\n|\r\n/); - this.#ranges = this.#getRanges(this.range, lines); + this.#ranges = getRanges(this.range, lines); this.#highlightLineSet = new Set( this.highlight - ? this.#getRanges(this.highlight, lines) + ? getRanges(this.highlight, lines) .map(([start, end]) => Array.from({ length: end - start + 1 }, (_, i) => start + i)) .flat() : [], @@ -259,52 +260,8 @@ export class Pre extends GemElement { ); } - #getRanges(range: string, lines: string[]) { - const len = lines.length; - const findLineNumber = (str: string) => (!str.trim() ? 0 : lines.findIndex((line) => line.includes(str)) + 1); - const ranges = range.split(',').map((range) => { - // 第二位可以省略,第一位不行,0 无意义,解析数字忽略空格,字符匹配包含空格 - // 3-4 - // 2 => 2-2 - // 2- => 2-max - // -2 => (-2)-(-2) - // -2- => (-2)-max - // 2--2 => 2-(-2) - // -3--2 => (-3)-(-2) - const [startStr, endStr = startStr] = range.split(/(? a - b); - }); - const result: number[][] = []; - ranges - .sort((a, b) => a[0] - b[0]) - .forEach((range, index, arr) => { - const prev = arr[index - 1]; - // 连号时并入前一个 range - if (prev && prev[1] + 1 === range[0]) { - prev[1] = range[1]; - } else { - result.push(range); - } - }); - return result; - } - #getParts(s: string) { - const lines = s.split(/\n|\r\n/); - const lineNumbersParts = Array.from(this.#ranges, () => []); - const parts = this.#ranges.map(([start, end], index) => { - return Array.from({ length: end - start + 1 }, (_, i) => { - const j = start + i - 1; - lineNumbersParts[index].push(j + 1); - return lines[j]; - }).join('\n'); - }); - return { parts, lineNumbersParts }; + return getParts(s.split(/\n|\r\n/), this.#ranges); } #composing = false; @@ -393,10 +350,10 @@ export class Pre extends GemElement { // } } - const content = Prism.languages[this.codelang] + const html = Prism.languages[this.codelang] ? Prism.highlight(this.textContent || '', Prism.languages[this.codelang], this.codelang) : this.innerHTML; - const { parts, lineNumbersParts } = this.#getParts(content); + const { parts, lineNumbersParts } = this.#getParts(html); this.codeRef.element.innerHTML = parts.reduce( (p, c, i) => p + diff --git a/packages/gem-book/src/element/index.ts b/packages/gem-book/src/element/index.ts index cb9fc72f..c80dc87b 100644 --- a/packages/gem-book/src/element/index.ts +++ b/packages/gem-book/src/element/index.ts @@ -23,7 +23,7 @@ import { UPDATE_EVENT } from '../common/constant'; import { theme, changeTheme, Theme, themeProps } from './helper/theme'; import { bookStore, updateBookConfig, locationStore } from './store'; -import { checkBuiltInPlugin, getRemotePath } from './lib/utils'; +import { checkBuiltInPlugin, joinPath } from './lib/utils'; import { GemBookPluginElement } from './elements/plugin'; import { Loadbar } from './elements/loadbar'; import { Homepage } from './elements/homepage'; @@ -101,7 +101,7 @@ export class GemBookElement extends GemElement { queueMicrotask(() => routeELement.update()); } else if (routeELement.currentRoute?.pattern === '*') { routeELement.update(); - } else if (getRemotePath(bookStore.getCurrentLink?.().originLink || '', bookStore.lang) === '/' + filePath) { + } else if (joinPath(bookStore.lang, bookStore.getCurrentLink?.().originLink) === `/${filePath}`) { const firstElementChild = routeELement.firstElementChild! as Main; firstElementChild.content = content; } diff --git a/packages/gem-book/src/element/lib/renderer.ts b/packages/gem-book/src/element/lib/renderer.ts index 6f13fdec..0a9dde1a 100644 --- a/packages/gem-book/src/element/lib/renderer.ts +++ b/packages/gem-book/src/element/lib/renderer.ts @@ -3,7 +3,7 @@ import { Renderer } from 'marked'; import { icons } from '../elements/icons'; import { normalizeId, parseTitle } from '../../common/utils'; -import { getRemotePath, isSameOrigin, getUserLink, escapeHTML, textContent } from './utils'; +import { isSameOrigin, getUserLink, escapeHTML, textContent, joinPath } from './utils'; export function getRenderer({ lang, link, displayRank }: { lang: string; link: string; displayRank?: boolean }) { const renderer = new Renderer(); @@ -45,7 +45,7 @@ export function getRenderer({ lang, link, displayRank }: { lang: string; link: s renderer.image = (href, title, text) => { if (href === null) return text; - const url = new URL(href, `${location.origin}${getRemotePath(link, lang)}`); + const url = new URL(href, `${location.origin}${joinPath(lang, link)}`); return `${text}`; }; diff --git a/packages/gem-book/src/element/lib/utils.ts b/packages/gem-book/src/element/lib/utils.ts index 52c7ebad..b05aa8d9 100644 --- a/packages/gem-book/src/element/lib/utils.ts +++ b/packages/gem-book/src/element/lib/utils.ts @@ -12,6 +12,56 @@ export function capitalize(s: string) { return s.replace(/^\w/, (s: string) => s.toUpperCase()); } +export function getRanges(range: string, lines: string[]) { + const len = lines.length; + const findLineNumber = (str: string, start = 1) => { + if (!str.trim()) return 0; + return lines.findIndex((line, index) => index >= start - 1 && line.includes(str)) + 1; + }; + const ranges = range.split(',').map((range) => { + // 第二位可以省略,第一位不行,0 无意义,解析数字忽略空格,字符匹配包含空格 + // 3-4 + // 2 => 2-2 + // 2-2 => 2-2 // 使用字符串搜索时 end 大于 start + // 2- => 2-max + // -2 => (-2)-(-2) + // -2- => (-2)-max + // 2--2 => 2-(-2) + // -3--2 => (-3)-(-2) + const [startStr, endStr = startStr] = range.split(/(? a - b); + }); + const result: number[][] = []; + ranges + .sort((a, b) => a[0] - b[0]) + .forEach((range, index, arr) => { + const prev = arr[index - 1]; + // 连号时并入前一个 range + if (prev && prev[1] + 1 === range[0]) { + prev[1] = range[1]; + } else { + result.push(range); + } + }); + return result; +} + +export function getParts(lines: string[], ranges: number[][]) { + const lineNumbersParts = Array.from(ranges, () => []); + const parts = ranges.map(([start, end], index) => { + return Array.from({ length: end - start + 1 }, (_, i) => { + const j = start + i - 1; + lineNumbersParts[index].push(j + 1); + return lines[j]; + }).join('\n'); + }); + return { parts, lineNumbersParts }; +} + // type error export function flatNav(nav: NavItem[]): NavItemWithLink[] { return nav @@ -22,21 +72,21 @@ export function flatNav(nav: NavItem[]): NavItemWithLink[] { .flat(); } -export function getRemotePath(originPath: string, lang = '') { - const langPath = lang && `/${lang}`; - return `${langPath}${originPath}`; +export function joinPath(...paths: (string | undefined)[]) { + const toPath = (base: string) => (base.startsWith('/') ? base : base ? `/${base}` : ''); + return paths.reduce((base = '', path = '') => { + return `${toPath(base)}${toPath(path)}`; + }, ''); } export function getGithubPath(link: string) { const { config, lang } = bookStore; const { sourceDir, base } = config || {}; - const basePath = base ? `/${base}` : ''; - const sourcePath = sourceDir ? `/${sourceDir}` : ''; - return `${basePath}${sourcePath}${getRemotePath(link, lang)}`; + return joinPath(base, sourceDir, lang, link); } -export function getURL(originPath: string, lang = '', hash = '') { - return `${getRemotePath(originPath, lang)}?hash=${hash}`; +export function getURL(originPath: string, hash = '') { + return `${originPath}?hash=${hash}`; } export function isSameOrigin(link: string) { diff --git a/packages/gem-book/src/element/store.ts b/packages/gem-book/src/element/store.ts index f50d5f18..2bec55b4 100644 --- a/packages/gem-book/src/element/store.ts +++ b/packages/gem-book/src/element/store.ts @@ -5,7 +5,7 @@ import { I18n } from '@mantou/gem/helper/i18n'; import { BookConfig, NavItem } from '../common/config'; import { fallbackLanguage, selfI18n } from './helper/i18n'; -import { getLinkPath, getUserLink, NavItemWithLink, flatNav, capitalize, getURL } from './lib/utils'; +import { getLinkPath, getUserLink, NavItemWithLink, flatNav, capitalize, getURL, joinPath } from './lib/utils'; import { getRenderer } from './lib/renderer'; import type { GemBookElement } from '.'; @@ -142,7 +142,7 @@ function getLinkRouters(links: NavItemWithLink[], title: string, lang: string, d async getContent() { await import('./elements/main'); const renderer = getRenderer({ lang, link: originLink, displayRank }); - const content = await (await fetch(getURL(originLink, lang, hash))).text(); + const content = await (await fetch(getURL(joinPath(lang, originLink), hash))).text(); if (bookStore.isDevMode?.()) await new Promise((res) => setTimeout(res, 500)); return html``; }, diff --git a/packages/gem-book/src/plugins/api.ts b/packages/gem-book/src/plugins/api.ts index 7309bbc2..6d5e8d8f 100644 --- a/packages/gem-book/src/plugins/api.ts +++ b/packages/gem-book/src/plugins/api.ts @@ -1,9 +1,5 @@ -/** - * TODO: support json - */ import type { ElementDetail, ExportDetail } from 'gem-analyzer'; -import type { Main } from '../element/elements/main'; import type { GemBookElement } from '../element'; const tsMorph = 'https://esm.sh/ts-morph@13.0.3'; @@ -11,11 +7,9 @@ const gemAnalyzer = 'https://esm.sh/gem-analyzer'; type State = { elements?: ElementDetail[]; exports?: ExportDetail[]; error?: any }; -customElements.whenDefined('gem-book').then(() => { - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; - const { Gem, theme } = GemBookPluginElement; +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { + const { Gem, theme, Utils } = GemBookPluginElement; const { html, customElement, attribute, numattribute, createCSSSheet, css, adoptedStyle } = Gem; - const MainElement = customElements.get('gem-book-main') as typeof Main; const style = createCSSSheet(css` gbp-api table { @@ -187,11 +181,11 @@ customElements.whenDefined('gem-book').then(() => { ['Type', 'Value'], [({ type }) => type, ({ value }) => value.map((e) => this.#renderCode(e)).join(', ')], ); - return MainElement.parseMarkdown(text); + return Utils.parseMarkdown(text); }; #renderExports = (exports: ExportDetail[]) => { - return MainElement.parseMarkdown( + return Utils.parseMarkdown( this.#renderTable( exports.filter(({ kindName }) => kindName === 'FunctionDeclaration' || kindName === 'ClassDeclaration'), ['Name', 'Description'], @@ -222,7 +216,7 @@ customElements.whenDefined('gem-book').then(() => { }); this.effect( async () => { - const url = this.getRemoteURL(this.src); + const url = Utils.getRemoteURL(this.src); if (!url) return; try { diff --git a/packages/gem-book/src/plugins/code-group.ts b/packages/gem-book/src/plugins/code-group.ts index 045b85c3..1f56526f 100644 --- a/packages/gem-book/src/plugins/code-group.ts +++ b/packages/gem-book/src/plugins/code-group.ts @@ -14,10 +14,9 @@ type State = { copyEnd: boolean; }; -customElements.whenDefined('gem-book').then(() => { - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; - const { theme, icons } = GemBookPluginElement; - const { html, customElement, adoptedStyle, css, createCSSSheet, classMap } = GemBookPluginElement.Gem; +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { + const { Gem, theme, icons } = GemBookPluginElement; + const { html, customElement, adoptedStyle, css, createCSSSheet, classMap } = Gem; const style = createCSSSheet(css` :host { @@ -77,6 +76,11 @@ customElements.whenDefined('gem-book').then(() => { @customElement('gbp-code-group') @adoptedStyle(style) class _GbpCodeGroupElement extends GemBookPluginElement { + constructor() { + super(); + this.cacheState(() => [this.textContent]); + } + state: State = { files: [], copyEnd: false, diff --git a/packages/gem-book/src/plugins/comment.ts b/packages/gem-book/src/plugins/comment.ts index e5820104..a3723b28 100644 --- a/packages/gem-book/src/plugins/comment.ts +++ b/packages/gem-book/src/plugins/comment.ts @@ -3,8 +3,7 @@ import type { GemBookElement } from '../element'; const gitalkUrl = 'https://esm.sh/gitalk@1.7.2'; const gitalkCSSUrl = 'https://esm.sh/gitalk@1.7.2/dist/gitalk.css'; -customElements.whenDefined('gem-book').then(async () => { - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; +customElements.whenDefined('gem-book').then(async ({ GemBookPluginElement }: typeof GemBookElement) => { const { config, Gem, theme, locationStore } = GemBookPluginElement; const { html, customElement, attribute, connectStore } = Gem; diff --git a/packages/gem-book/src/plugins/docsearch.ts b/packages/gem-book/src/plugins/docsearch.ts index 5e197504..944936dc 100644 --- a/packages/gem-book/src/plugins/docsearch.ts +++ b/packages/gem-book/src/plugins/docsearch.ts @@ -5,8 +5,7 @@ import type { GemBookElement } from '../element'; const moduleLink = 'https://esm.sh/@docsearch/js@3.0.0-alpha.50'; const styleLink = 'https://esm.sh/@docsearch/css@3.0.0-alpha.50/dist/style.css'; -customElements.whenDefined('gem-book').then(async () => { - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { const { Gem, theme, mediaQuery } = GemBookPluginElement; const { css, html, customElement, attribute, refobject } = Gem; diff --git a/packages/gem-book/src/plugins/example.ts b/packages/gem-book/src/plugins/example.ts index 75e743f1..2bcb97e8 100644 --- a/packages/gem-book/src/plugins/example.ts +++ b/packages/gem-book/src/plugins/example.ts @@ -9,10 +9,9 @@ type PropValue = string | number | boolean | Record; type Props = Record; -customElements.whenDefined('gem-book').then(() => { - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; - const { theme, icons } = GemBookPluginElement; - const { html, customElement, attribute, createCSSSheet, css, adoptedStyle, styleMap } = GemBookPluginElement.Gem; +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { + const { Gem, theme, icons } = GemBookPluginElement; + const { html, customElement, attribute, createCSSSheet, css, adoptedStyle, styleMap } = Gem; const style = createCSSSheet(css` :host { diff --git a/packages/gem-book/src/plugins/import.ts b/packages/gem-book/src/plugins/import.ts index 0b5bb276..3b95efd4 100644 --- a/packages/gem-book/src/plugins/import.ts +++ b/packages/gem-book/src/plugins/import.ts @@ -2,11 +2,10 @@ import type { GemBookElement } from '../element'; const esmBuilder = 'https://esm.sh/build'; -customElements.whenDefined('gem-book').then(async () => { +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { const esmBuilderPromise = import(/* webpackIgnore: true */ esmBuilder); - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; - const { Gem } = GemBookPluginElement; + const { Gem, Utils } = GemBookPluginElement; const { html, customElement, attribute } = Gem; @customElement('gbp-import') @@ -29,7 +28,7 @@ customElements.whenDefined('gem-book').then(async () => { mounted() { this.effect( async () => { - const url = this.getRemoteURL(this.src); + const url = Utils.getRemoteURL(this.src); if (!url) return; try { const resp = await fetch(url); diff --git a/packages/gem-book/src/plugins/include.ts b/packages/gem-book/src/plugins/include.ts new file mode 100644 index 00000000..eaa20335 --- /dev/null +++ b/packages/gem-book/src/plugins/include.ts @@ -0,0 +1,56 @@ +import type { GemBookElement } from '../element'; + +type State = { + content: Element[]; + error?: any; +}; + +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { + const { Gem, theme, Utils } = GemBookPluginElement; + const { attribute, customElement, html } = Gem; + + @customElement('gbp-include') + class _GbpIncludeElement extends GemBookPluginElement { + @attribute src: string; + @attribute range: string; + + constructor() { + super({ isLight: true }); + this.cacheState(() => [this.src, this.range]); + } + + state: State = { + content: [], + }; + + mounted() { + this.effect( + async () => { + const url = Utils.getRemoteURL(this.src); + if (!url) return; + this.setState({ error: false }); + try { + const resp = await fetch(url); + if (resp.status === 404) throw new Error(resp.statusText || 'Not Found'); + const md = await resp.text(); + const lines = md.split(/\n|\r\n/); + const ranges = Utils.getRanges(this.range, lines); + const { parts } = Utils.getParts(lines, ranges); + this.setState({ content: Utils.parseMarkdown(parts.join('\n')) }); + } catch (error) { + this.setState({ error }); + } + }, + () => [this.src, this.range], + ); + } + + render() { + const { content, error } = this.state; + if (error) return html`
${error}
`; + if (!content) return html`
Loading...
`; + + return html`${content}`; + } + } +}); diff --git a/packages/gem-book/src/plugins/media.ts b/packages/gem-book/src/plugins/media.ts index eec24e57..3eee8092 100644 --- a/packages/gem-book/src/plugins/media.ts +++ b/packages/gem-book/src/plugins/media.ts @@ -2,9 +2,8 @@ import type { GemBookElement } from '../element'; type MediaType = 'img' | 'video' | 'audio' | 'unknown'; -customElements.whenDefined('gem-book').then(() => { - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; - const { Gem, theme } = GemBookPluginElement; +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { + const { Gem, theme, Utils } = GemBookPluginElement; const { html, customElement, attribute } = Gem; @customElement('gbp-media') @@ -46,15 +45,15 @@ customElements.whenDefined('gem-book').then(() => { } #renderImage() { - return html``; + return html``; } #renderVideo() { - return html``; + return html``; } #renderAudio() { - return html``; + return html``; } #renderContent() { diff --git a/packages/gem-book/src/plugins/raw.ts b/packages/gem-book/src/plugins/raw.ts index 181a9cae..af08ee55 100644 --- a/packages/gem-book/src/plugins/raw.ts +++ b/packages/gem-book/src/plugins/raw.ts @@ -5,9 +5,8 @@ type State = { error?: any; }; -customElements.whenDefined('gem-book').then(() => { - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; - const { Gem, theme } = GemBookPluginElement; +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { + const { Gem, theme, Utils } = GemBookPluginElement; const { attribute, customElement, html, createCSSSheet, css, adoptedStyle } = Gem; const style = createCSSSheet(css` @@ -67,7 +66,7 @@ customElements.whenDefined('gem-book').then(() => { mounted() { this.effect( async () => { - const url = this.getRemoteURL(this.src); + const url = Utils.getRemoteURL(this.src); if (!url) return; this.setState({ error: false }); try { diff --git a/packages/gem-book/src/plugins/sandpack.ts b/packages/gem-book/src/plugins/sandpack.ts index ca80f0b4..bcdb0f4c 100644 --- a/packages/gem-book/src/plugins/sandpack.ts +++ b/packages/gem-book/src/plugins/sandpack.ts @@ -35,8 +35,7 @@ type State = { status: ClientStatus | 'initialization' | 'done'; }; -customElements.whenDefined('gem-book').then(() => { - const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { const { theme, icons } = GemBookPluginElement; const { html, customElement, refobject, attribute, boolattribute, adoptedStyle, css, createCSSSheet, classMap } = GemBookPluginElement.Gem; @@ -207,7 +206,7 @@ customElements.whenDefined('gem-book').then(() => { constructor() { super(); - new MutationObserver(async () => { + new MutationObserver(() => { const files = this.#parseContents(); this.setState({ files }); this.#updateSandbox(); diff --git a/packages/gem-book/src/plugins/var.ts b/packages/gem-book/src/plugins/var.ts new file mode 100644 index 00000000..9baca10f --- /dev/null +++ b/packages/gem-book/src/plugins/var.ts @@ -0,0 +1,31 @@ +import type { GemBookElement } from '../element'; + +type State = { + content: Element[]; + error?: any; +}; + +customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { + const { Gem, config } = GemBookPluginElement; + const { customElement, html } = Gem; + + @customElement('gbp-var') + class _GbpVarElement extends GemBookPluginElement { + mounted() { + new MutationObserver(() => this.update()).observe(this, { + childList: true, + characterData: true, + subtree: true, + }); + } + + render() { + const paths = (this.textContent || '').split('.'); + try { + return html`${paths.reduce((p, prop) => p[prop], config.global)}`; + } catch { + return html``; + } + } + } +});