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 1a1dde11..bbab1409 100644 --- a/packages/gem-book/docs/zh/002-guide/007-extension.md +++ b/packages/gem-book/docs/zh/002-guide/007-extension.md @@ -67,7 +67,7 @@ render('这是一个 `` 例子', document.getElementById('root')); [插槽](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot)能让你自定义 `` 的内容,目前支持的插槽有 `sidebar-before`, `main-before`, `main-after`, `nav-inside`, `logo-after`。 - + > [!NOTE] > 可以使用 `--template` 指定模板文件 diff --git a/packages/gem-book/docs/zh/003-plugins.md b/packages/gem-book/docs/zh/003-plugins.md index d2c5b0a0..0f810368 100644 --- a/packages/gem-book/docs/zh/003-plugins.md +++ b/packages/gem-book/docs/zh/003-plugins.md @@ -38,12 +38,12 @@ yarn add gem-book 用于显示远端代码,如果提供的 `src` 只包含路径,则会从当前项目的 GitHub 上读取内容(受 [`sourceDir`](./002-guide/003-cli.md#--source-dir),[`sourceBranch`](./002-guide/003-cli.md#--source-branch) 影响),比如: - + ```md - + ``` ## `` diff --git a/packages/gem-book/src/element/elements/main.ts b/packages/gem-book/src/element/elements/main.ts index 141c5b4b..6efdd463 100644 --- a/packages/gem-book/src/element/elements/main.ts +++ b/packages/gem-book/src/element/elements/main.ts @@ -102,6 +102,7 @@ export class Main extends GemElement { width: 100%; box-sizing: border-box; z-index: 1; + container-type: inline-size; } :host > :first-child { margin-top: 0; diff --git a/packages/gem-book/src/element/elements/pre.ts b/packages/gem-book/src/element/elements/pre.ts index c3b36825..a5163dd2 100644 --- a/packages/gem-book/src/element/elements/pre.ts +++ b/packages/gem-book/src/element/elements/pre.ts @@ -207,6 +207,8 @@ const langAliases: Record = { yml: 'yaml', }; +const IGNORE_LINE = 2; + /** * @customElement gem-book-pre */ @@ -218,9 +220,18 @@ export class Pre extends GemElement { @attribute filename: string; @attribute status: 'hidden' | 'active' | ''; @boolattribute editable: boolean; + @boolattribute linenumber: boolean; @refobject codeRef: RefObject; + get #range() { + return this.range || '1-'; + } + + get #linenumber() { + return !!this.range || this.linenumber; + } + constructor() { super(); new MutationObserver(() => this.update()).observe(this, { @@ -230,15 +241,8 @@ export class Pre extends GemElement { }); } - #getStartIndex(start: number, arr: any[]) { - return start < 0 ? arr.length + start + 1 : start; - } - - #getEndIndex(end: number, arr: any[]) { - return end < 0 ? arr.length + end + 1 : end || arr.length; - } - - #getRanges(range: string) { + #getRanges(range: string, lines: string[]) { + const len = lines.length; const ranges = range.split(/,\s*/); return ranges.map((range) => { // 第二位可以省略,第一位不行 @@ -249,23 +253,32 @@ export class Pre extends GemElement { // -2- => (-2)-max // 2--2 => 2-(-2) // -3--2 => (-3)-(-2) - const [start, end = start] = range.split(/(? { - let result = ''; - for (let i = this.#getStartIndex(start, lines) - 1; i < this.#getEndIndex(end, lines); i++) { - result += (lines[i] || '') + '\n'; - } - return result; - }) - : [s]; - return parts.join('\n...\n\n'); + const ranges = this.#getRanges(this.#range, lines); + const highlightLineSet = new Set( + this.highlight + ? this.#getRanges(this.highlight, lines) + .map(([start, end]) => Array.from({ length: end - start + 1 }, (_, i) => start + i)) + .flat() + : [], + ); + 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, ranges, lineNumbersParts, highlightLineSet }; } #composing = false; @@ -359,12 +372,21 @@ export class Pre extends GemElement { const content = Prism.languages[this.codelang] ? Prism.highlight(this.textContent || '', Prism.languages[this.codelang], this.codelang) : this.innerHTML; - this.codeRef.element.innerHTML = this.#getParts(content); + const { parts, lineNumbersParts } = this.#getParts(content); + this.codeRef.element.innerHTML = parts.reduce( + (p, c, i) => + p + + ` @@ ${lineNumbersParts[i - 1].at(-1)! + 1}-${ + lineNumbersParts[i].at(0)! - 1 + } @@` + + c, + ); this.#setOffset(); }); } render() { + const { parts, lineNumbersParts, highlightLineSet } = this.#getParts(this.textContent || ''); // Safari 精度问题所以使用整数像素单位 const lineHeight = '24px'; const padding = '1em'; @@ -374,9 +396,14 @@ export class Pre extends GemElement { display: none; } :host { + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: currentColor; position: relative; - display: block; + display: flex; font-size: 0.875em; + font-family: ${theme.codeFont}; + line-height: ${lineHeight}; background: rgba(${theme.textColorRGB}, 0.05); --comment-color: var(--code-comment-color, #6e6e6e); --section-color: var(--code-section-color, #c9252d); @@ -388,33 +415,74 @@ export class Pre extends GemElement { --keyword-color: var(--code-keyword-color, #93219e); --attribute-color: var(--code-attribute-color, #4646c6); } - .gem-highlight { - display: block; - position: absolute; - pointer-events: none; - background: black; - opacity: 0.05; - width: 100%; + .gem-code, + .linenumber { + box-sizing: border-box; + min-height: 100%; + height: max-content; } .gem-code { - height: 100%; + overflow-x: auto; + scrollbar-width: thin; + scrollbar-color: currentColor; + font-family: inherit; + flex-grow: 1; box-sizing: border-box; display: block; - font-family: ${theme.codeFont}; text-align: left; white-space: pre; tab-size: 2; - padding: 1em; + padding: ${padding}; hyphens: none; - overflow: auto; overflow-clip-box: content-box; box-shadow: none; border: none; background: transparent; - scrollbar-width: thin; outline: none; caret-color: ${theme.textColor}; } + .linenumber { + display: inline-flex; + flex-direction: column; + flex-shrink: 0; + padding: 1em; + min-width: 1em; + text-align: right; + border-right: 1px solid ${theme.borderColor}; + color: rgba(${theme.textColorRGB}, 0.5); + user-select: none; + } + .linenumber-ignore, + .code-ignore { + display: flex; + place-items: center; + height: calc(${IGNORE_LINE} * ${lineHeight}); + user-select: none; + } + .code-ignore { + place-content: start; + } + .linenumber-ignore { + place-content: center; + } + .linenumber-ignore::before { + content: ''; + width: 2px; + height: 2px; + background: currentColor; + border-radius: 1em; + box-shadow: + 0 0.34em, + 0 -0.34em; + } + .gem-highlight { + display: block; + position: absolute; + pointer-events: none; + background: rgba(${theme.textColorRGB}, 0.05); + width: 100%; + height: ${lineHeight}; + } .token.comment, .token.prolog, .token.doctype, @@ -490,18 +558,32 @@ export class Pre extends GemElement { } } - ${this.highlight - ? this.#getRanges(this.highlight).map( - ([start, end]) => html` - - `, - ) + ${lineNumbersParts + .reduce((p, c) => p.concat(Array(IGNORE_LINE)).concat(c)) + .map((linenumber, index) => + highlightLineSet.has(linenumber) + ? html` + + ` + : '', + )} + ${this.#linenumber + ? html` +
+ ${lineNumbersParts.map( + (numbers, index, arr) => html` + ${numbers.map((n) => html`${n}`)}${arr.length - 1 !== index + ? html`` + : ''} + `, + )} +
+ ` : ''} ${this.#getParts(this.textContent || '')}${parts.join('\n'.repeat(IGNORE_LINE + 1))} - `; } } diff --git a/packages/gem-book/src/element/helper/default-theme.ts b/packages/gem-book/src/element/helper/default-theme.ts index 3cad6dd0..9a15c0ae 100644 --- a/packages/gem-book/src/element/helper/default-theme.ts +++ b/packages/gem-book/src/element/helper/default-theme.ts @@ -6,7 +6,7 @@ export const defaultTheme = { sidebarWidthSmall: '270px', sidebarWidth: '304px', - maxMainWidth: '46rem', + maxMainWidth: '48rem', headerHeight: '56px', normalRound: '0.5rem', smallRound: '0.25rem', diff --git a/packages/gem-book/src/plugins/sandpack.ts b/packages/gem-book/src/plugins/sandpack.ts index b968f92e..4768fa8c 100644 --- a/packages/gem-book/src/plugins/sandpack.ts +++ b/packages/gem-book/src/plugins/sandpack.ts @@ -91,7 +91,6 @@ customElements.whenDefined('gem-book').then(() => { width: 1em; } ::slotted(*) { - display: none; max-height: 70vh; grid-area: code; background: rgba(${theme.textColorRGB}, 0.03); @@ -119,21 +118,24 @@ customElements.whenDefined('gem-book').then(() => { background: ${theme.backgroundColor}; border-radius: ${theme.normalRound}; } - @media (max-width: 700px) { + @container (max-width: 700px) { :host { grid-template: 'tabs' 'code' 'preview' / 100%; } .preview { padding: 0; + border-top: 1px solid ${theme.borderColor}; } .sandbox { + min-height: auto; + height: 35vh; background: transparent; } .actions { display: none; } ::slotted(*) { - max-height: 60vh; + height: 35vh; } } `); @@ -224,6 +226,7 @@ customElements.whenDefined('gem-book').then(() => { #parseContents = () => { return [...this.querySelectorAll
('gem-book-pre')].map((element) => {
         element.setAttribute('editable', '');
+        element.setAttribute('linenumber', '');
         return {
           element,
           code: element.textContent,
@@ -358,8 +361,8 @@ customElements.whenDefined('gem-book').then(() => {
       const currentFile = files.find(({ status }) => status === 'active') || files.find(({ status }) => status === '');
       const currentFileSelector =
         currentFile?.filename === this.#defaultEntryFilename
-          ? `::slotted([filename='']),::slotted([filename='${currentFile?.filename}'])`
-          : `::slotted([filename='${currentFile?.filename}'])`;
+          ? `::slotted(:not([filename=''], [filename='${currentFile?.filename}']))`
+          : `::slotted(:not([filename='${currentFile?.filename}']))`;
       return html`
         
    @@ -385,7 +388,7 @@ customElements.whenDefined('gem-book').then(() => {