Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"Gadget/Searchbox-popout",
"Gadget/SectionPermanentLink",
"Gadget/SendWelcomeMessage",
"Gadget/Shiki",
"Gadget/shortlink",
"Gadget/ShowAvatar",
"Gadget/SideBarPic",
Expand Down
1 change: 1 addition & 0 deletions src/gadgets/Gadgets-definition-list.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- zeroFill
- prism
- ModerationStatus
- Shiki

- name: skin
gadgets:
Expand Down
39 changes: 39 additions & 0 deletions src/gadgets/Shiki/MediaWiki:Gadget-Shiki.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* base styles */
pre.shiki {
position: relative;
font-family: "JetBrainsMono Nerd Font", "JetBrains Mono NF", "JetBrains Mono", "Fira Code", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 1rem;
padding: 1em;
border-radius: 0.5em;
white-space: pre;
overflow-x: auto;
}

/* lang badge */
pre.shiki > .shiki-lang-badge {
position: absolute;
right: 0.5em;
top: 0.5em;
font-size: 10px;
border-radius: 99vw;
background: #000;
padding: 0.2em 0.5em;
user-select: none;
pointer-events: none;
}

/* line number */
pre.shiki.line-number .shiki-code {
counter-reset: step;
counter-increment: step calc(var(--start, 1) - 1);
Comment on lines +27 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为啥不直接

Suggested change
counter-reset: step;
counter-increment: step calc(var(--start, 1) - 1);
counter-reset: step calc(var(--start, 1) - 1);

}

pre.shiki.line-number .shiki-code .line::before {
content: counter(step);
counter-increment: step;
width: 2em;
margin-right: 1em;
display: inline-block;
text-align: right;
color: rgb(115 138 148 / 40%);
}
120 changes: 120 additions & 0 deletions src/gadgets/Shiki/MediaWiki:Gadget-Shiki.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* MediaWiki Gadget Shiki.js Code Highlighter
* @author Dragon-Fish <[email protected]>
* @license MIT
*/

/* eslint-disable prefer-arrow-functions/prefer-arrow-functions */
/* eslint-disable no-use-before-define */

"use strict";

(async () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (复杂性): 考虑通过使用 async/await 并将转换器配置提取到单独的变量中来简化 renderBlock 函数。

renderBlock 函数可以在保持功能的同时简化:

  1. 使用 async/await 而不是 promise 链
  2. 提取转换器配置以减少嵌套

这是一个建议的重构:

Original comment in English

issue (complexity): Consider simplifying the renderBlock function by using async/await and extracting the transformer configuration into a separate variable.

The renderBlock function could be simplified while maintaining functionality:

  1. Use async/await instead of promise chains
  2. Extract transformer config to reduce nesting

Here's a suggested refactor:

const shikiTransformer = {
  pre: (node) => {
    node.properties.class += ` lang-${langLabel}`;
    node.properties.style += ";";
    node.properties.style += `padding-right: ${(10 * langLabel.length + 12).toFixed()}px;`;
    if (lineFrom) {
      node.properties.class += " line-number";
    }
  },
  code: (node) => {
    node.properties.class += " shiki-code";
    node.tagName = "div";
    node.properties.style += `;--start: ${+lineFrom};`;
  },
  line: (node, line) => {
    node.properties["data-line-number-raw"] = line;
  },
  postprocess: (html) => {
    if (langLabel) {
      return html.replace("</pre>", `<span class="shiki-lang-badge">${langLabel}</span></pre>`);
    }
  },
};

async function renderBlock(shiki, el) {
  if (el.classList.contains("shiki") || !!el.dataset.shikiRendered) {
    return null;
  }

  const lang = getLangFromElement(el) || getLangFromContentModel();
  if (!lang) {
    return null;
  }

  try {
    const html = await shiki.codeToHtml(el.innerText.trimEnd(), {
      lang,
      theme: "one-dark-pro",
      transformers: [shikiTransformer],
    });

    el.style.display = "none";
    el.dataset.shikiRendered = "1";
    const wrapper = document.createElement("div");
    wrapper.innerHTML = html;
    const pre = wrapper.querySelector("pre");
    el.insertAdjacentElement("afterend", pre);
    return pre;
  } catch (e) {
    console.error("[SHIKI] Render failed", el, e);
    return null;
  }
}

</details>

const SHIKI_CDN = "https://esm.sh/[email protected]";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议: 考虑为 Shiki 导入添加备用 CDN 和超时处理

外部 CDN 依赖可能会失败。考虑实现备用 CDN 并添加超时处理,以在 CDN 不可用时优雅降级。

Original comment in English

suggestion: Consider adding fallback CDN and timeout handling for the Shiki import

External CDN dependencies can fail. Consider implementing a fallback CDN and adding timeout handling to gracefully degrade when the CDN is unavailable.

const TARGET_ELEMENTS = document.querySelectorAll(["pre.highlight", "pre.hljs", "pre.prettyprint", "pre.mw-code", "pre[lang]", "code[lang]", "pre[data-lang]", "code[data-lang]"]);

const shiki = await import(SHIKI_CDN);
TARGET_ELEMENTS.forEach((el) => {
renderBlock(shiki, el);
});
mw.hook("npm:shiki").fire(shiki);
Comment on lines +16 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VM20415:59 Uncaught (in promise) ReferenceError: Cannot access 'getLangFromElement' before initialization

这5行是不是应该放在最后(?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (bug_risk): 钩子在所有块渲染之前触发,这可能导致竞争条件

考虑在所有块渲染后触发钩子,方法是等待所有 renderBlock 承诺解决。

Original comment in English

issue (bug_risk): Hook is fired before all blocks are rendered, which could cause race conditions

Consider firing the hook after all blocks have been rendered by waiting for all renderBlock promises to resolve.


const getLangFromContentModel = () => {
const nsNumber = mw.config.get("wgNamespaceNumber");
const pageName = mw.config.get("wgPageName");
const contentModel = mw.config.get("wgPageContentModel", "").toLowerCase();
if (pageName.endsWith(".js") || contentModel === "javascript") {
return "javascript";
} else if (pageName.endsWith(".css") || contentModel === "css") {
return "css";
} else if (
// Lua
(nsNumber === 828 || ["scribunto", "lua"].includes(contentModel))
&& !pageName.endsWith("/doc")
) {
return "lua";
}
};

/**
* @param {HTMLElement} el
*/
const getLangFromElement = (el) => {
const lang = el.getAttribute("lang") || el.dataset.lang || Array.from(el.classList).find((c) => c.startsWith("language-") || c.startsWith("lang-"));
if (lang) {
return lang.includes("-") ? lang.split("-")[1] : lang;
}
return "";
};

/**
* @param {import('shiki')} shiki
* @param {HTMLElement} el
* @returns {Promise<HTMLElement | null>}
*/
async function renderBlock(shiki, el) {
if (el.classList.contains("shiki") || !!el.dataset.shikiRendered) {
return Promise.resolve(null);
}
const lang = getLangFromElement(el) || getLangFromContentModel();
if (!lang) {
return Promise.resolve(null);
}

const langInfo = shiki.bundledLanguagesInfo.find((i) => i.aliases?.includes(lang) || i.id === lang || i.name === lang);
const langLabel = (() => {
if (!langInfo) {
return lang;
}
return [langInfo.aliases?.[0], langInfo.name, langInfo.id, lang].filter(Boolean).sort((a, b) => a.length - b.length)[0];
})();
console.info("[SHIKI]", "Rendering", el, lang, langInfo);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

能不能不要拉屎(至少改成console.debug吧(

Suggested change
console.info("[SHIKI]", "Rendering", el, lang, langInfo);
console.debug("[SHIKI]", "Rendering", el, lang, langInfo);


const lineFrom = el.dataset.lineFrom || el.dataset.from || "1";

const renderedEl = await shiki
.codeToHtml(el.innerText.trimEnd(), {
lang,
theme: "one-dark-pro",
transformers: [
{
pre: (node) => {
node.properties.class += ` lang-${langLabel}`;
node.properties.style += ";";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

妈妈生的

node.properties.style += `padding-right: ${(10 * langLabel.length + 12).toFixed()}px;`;
if (lineFrom) {
node.properties.class += " line-number";
}
},
code: (node) => {
node.properties.class += " shiki-code";
node.tagName = "div";
node.properties.style += `;--start: ${+lineFrom};`;
},
line: (node, line) => {
node.properties["data-line-number-raw"] = line;
},
postprocess: (html) => {
if (langLabel) {
return html.replace("</pre>", `<span class="shiki-lang-badge">${langLabel}</span></pre>`);
}
},
},
],
})
.then((html) => {
el.style.display = "none";
el.dataset.shikiRendered = "1";
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
const pre = wrapper.querySelector("pre");
el.insertAdjacentElement("afterend", pre);
Comment on lines +106 to +111
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

希望能把 el 的属性继承到 pre,例如 style

return pre;
})
.catch((e) => {
console.error("[SHIKI] Render failed", el, e);
return null;
});
return renderedEl;
Comment on lines +75 to +118
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议 (代码质量): 内联立即返回的变量 (inline-immediately-returned-variable)

Suggested change
const renderedEl = await shiki
.codeToHtml(el.innerText.trimEnd(), {
lang,
theme: "one-dark-pro",
transformers: [
{
pre: (node) => {
node.properties.class += ` lang-${langLabel}`;
node.properties.style += ";";
node.properties.style += `padding-right: ${(10 * langLabel.length + 12).toFixed()}px;`;
if (lineFrom) {
node.properties.class += " line-number";
}
},
code: (node) => {
node.properties.class += " shiki-code";
node.tagName = "div";
node.properties.style += `;--start: ${+lineFrom};`;
},
line: (node, line) => {
node.properties["data-line-number-raw"] = line;
},
postprocess: (html) => {
if (langLabel) {
return html.replace("</pre>", `<span class="shiki-lang-badge">${langLabel}</span></pre>`);
}
},
},
],
})
.then((html) => {
el.style.display = "none";
el.dataset.shikiRendered = "1";
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
const pre = wrapper.querySelector("pre");
el.insertAdjacentElement("afterend", pre);
return pre;
})
.catch((e) => {
console.error("[SHIKI] Render failed", el, e);
return null;
});
return renderedEl;
return await shiki
.codeToHtml(el.innerText.trimEnd(), {
lang,
theme: "one-dark-pro",
transformers: [
{
pre: (node) => {
node.properties.class += ` lang-${langLabel}`;
node.properties.style += ";";
node.properties.style += `padding-right: ${(10 * langLabel.length + 12).toFixed()}px;`;
if (lineFrom) {
node.properties.class += " line-number";
}
},
code: (node) => {
node.properties.class += " shiki-code";
node.tagName = "div";
node.properties.style += `;--start: ${+lineFrom};`;
},
line: (node, line) => {
node.properties["data-line-number-raw"] = line;
},
postprocess: (html) => {
if (langLabel) {
return html.replace("</pre>", `<span class="shiki-lang-badge">${langLabel}</span></pre>`);
}
},
},
],
})
.then((html) => {
el.style.display = "none";
el.dataset.shikiRendered = "1";
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
const pre = wrapper.querySelector("pre");
el.insertAdjacentElement("afterend", pre);
return pre;
})
.catch((e) => {
console.error("[SHIKI] Render failed", el, e);
return null;
});


解释我们经常在代码中看到的是将结果赋值给一个变量
然后立即返回它。

直接返回结果可以缩短代码并删除不必要的
变量,减少阅读函数的心理负担。

中间变量可能有用的地方是如果它们随后被用作
参数或条件,并且名称可以作为变量代表的注释。在你从函数返回它的情况下,
函数名称用于告诉你结果是什么,因此变量名称
是不必要的。

Original comment in English

suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)

Suggested change
const renderedEl = await shiki
.codeToHtml(el.innerText.trimEnd(), {
lang,
theme: "one-dark-pro",
transformers: [
{
pre: (node) => {
node.properties.class += ` lang-${langLabel}`;
node.properties.style += ";";
node.properties.style += `padding-right: ${(10 * langLabel.length + 12).toFixed()}px;`;
if (lineFrom) {
node.properties.class += " line-number";
}
},
code: (node) => {
node.properties.class += " shiki-code";
node.tagName = "div";
node.properties.style += `;--start: ${+lineFrom};`;
},
line: (node, line) => {
node.properties["data-line-number-raw"] = line;
},
postprocess: (html) => {
if (langLabel) {
return html.replace("</pre>", `<span class="shiki-lang-badge">${langLabel}</span></pre>`);
}
},
},
],
})
.then((html) => {
el.style.display = "none";
el.dataset.shikiRendered = "1";
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
const pre = wrapper.querySelector("pre");
el.insertAdjacentElement("afterend", pre);
return pre;
})
.catch((e) => {
console.error("[SHIKI] Render failed", el, e);
return null;
});
return renderedEl;
return await shiki
.codeToHtml(el.innerText.trimEnd(), {
lang,
theme: "one-dark-pro",
transformers: [
{
pre: (node) => {
node.properties.class += ` lang-${langLabel}`;
node.properties.style += ";";
node.properties.style += `padding-right: ${(10 * langLabel.length + 12).toFixed()}px;`;
if (lineFrom) {
node.properties.class += " line-number";
}
},
code: (node) => {
node.properties.class += " shiki-code";
node.tagName = "div";
node.properties.style += `;--start: ${+lineFrom};`;
},
line: (node, line) => {
node.properties["data-line-number-raw"] = line;
},
postprocess: (html) => {
if (langLabel) {
return html.replace("</pre>", `<span class="shiki-lang-badge">${langLabel}</span></pre>`);
}
},
},
],
})
.then((html) => {
el.style.display = "none";
el.dataset.shikiRendered = "1";
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
const pre = wrapper.querySelector("pre");
el.insertAdjacentElement("afterend", pre);
return pre;
})
.catch((e) => {
console.error("[SHIKI] Render failed", el, e);
return null;
});


ExplanationSomething that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.

};
Comment on lines +55 to +119
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (代码质量): 避免在块内声明函数,倾向于使用函数赋值表达式。 (avoid-function-declarations-in-blocks)

解释函数声明可能在 Javascript 中被提升,但不同浏览器之间的行为不一致。 提升通常令人困惑,应避免。在块内使用函数声明时, 应使用函数表达式,这会在作用域内创建函数。
Original comment in English

issue (code-quality): Avoid function declarations, favouring function assignment expressions, inside blocks. (avoid-function-declarations-in-blocks)

ExplanationFunction declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers. Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you should use function expressions, which create functions in-scope.

})();
33 changes: 33 additions & 0 deletions src/gadgets/Shiki/definition.yaml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✖ ./src/gadgets/Shiki/definition.yaml is invalid

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
ResourceLoader: true

hidden: false

default: false

supportsUrlLoad: false

targets: []

skins: []

actions: []

type: general

package: false

rights: []

peers: []

dependencies:

_sites:
- zh
- commons

_section: browsing

_files:
- MediaWiki:Gadget-Shiki.js
- MediaWiki:Gadget-Shiki.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// <pre>
"use strict";
$(() => {
if (mw.user.options.get("gadget-prism")) {
if (mw.user.options.get("gadget-prism") || mw.user.options.get("gadget-Shiki")) {
return;
}
if (mw.config.get("wgPageName").match(/\.js$/)) {
Expand Down
Loading