Files
note2any/src/markdown/code.ts
2025-10-08 09:18:20 +08:00

268 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 文件markdown/code.ts — 代码区块与内联代码的解析与渲染。 */
import { Notice } from "obsidian";
import { MarkedExtension, Tokens } from "marked";
import hljs from "highlight.js";
import { MathRendererQueue } from "./math";
import { Extension } from "./extension";
import { UploadImageToWx } from "../imagelib";
import AssetsManager from "src/assets";
import { wxWidget } from "src/weixin-api";
export class CardDataManager {
private cardData: Map<string, string>;
private static instance: CardDataManager;
private constructor() {
this.cardData = new Map<string, string>();
}
// 静态方法,用于获取实例
public static getInstance(): CardDataManager {
if (!CardDataManager.instance) {
CardDataManager.instance = new CardDataManager();
}
return CardDataManager.instance;
}
public setCardData(id: string, cardData: string) {
this.cardData.set(id, cardData);
}
public cleanup() {
this.cardData.clear();
}
public restoreCard(html: string) {
for (const [key, value] of this.cardData.entries()) {
const exp = `<section[^>]*\\sdata-id="${key}"[^>]*>(.*?)<\\/section>`;
const regex = new RegExp(exp, 'gs');
if (!regex.test(html)) {
console.warn('没有公众号信息:', key);
continue;
}
html = html.replace(regex, value);
}
return html;
}
}
const MermaidSectionClassName = 'note-mermaid';
const MermaidImgClassName = 'note-mermaid-img';
export class CodeRenderer extends Extension {
showLineNumber: boolean;
mermaidIndex: number;
async prepare() {
this.mermaidIndex = 0;
}
static srcToBlob(src: string) {
const base64 = src.split(',')[1];
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: 'image/png' });
}
static async uploadMermaidImages(root: HTMLElement, token: string) {
const imgs = root.querySelectorAll('.' + MermaidImgClassName);
for (let img of imgs) {
const src = img.getAttribute('src');
if (!src) continue;
if (src.startsWith('http')) continue;
const blob = CodeRenderer.srcToBlob(img.getAttribute('src')!);
const name = img.id + '.png';
const res = await UploadImageToWx(blob, name, token);
if (res.errcode != 0) {
const msg = `上传图片失败: ${res.errcode} ${res.errmsg}`;
new Notice(msg);
console.error(msg);
continue;
}
const url = res.url;
img.setAttribute('src', url);
}
}
replaceSpaces(text: string) {
let result = '';
let inTag = false;
for (let char of text) {
if (char === '<') {
inTag = true;
result += char;
continue;
} else if (char === '>') {
inTag = false;
result += char;
continue;
}
if (inTag) {
result += char;
} else {
if (char === ' ') {
result += '&nbsp;';
} else if (char === '\t') {
result += '&nbsp;&nbsp;&nbsp;&nbsp;';
} else {
result += char;
}
}
}
return result;
}
async codeRenderer(code: string, infostring: string | undefined) {
const lang = (infostring || '').match(/^\S*/)?.[0];
code = code.replace(/\n$/, '');
try {
if (lang && hljs.getLanguage(lang)) {
code = hljs.highlight(code, { language: lang }).value;
}
else {
code = hljs.highlightAuto(code).value;
}
} catch (err) {
console.error(err);
}
code = this.replaceSpaces(code);
const lines = code.split('\n');
let body = '';
let liItems = '';
for (let line in lines) {
let text = lines[line];
if (text.length === 0) {
text = '<br>'
}
body = body + '<code>' + text + '</code>';
liItems = liItems + `<li>${parseInt(line)+1}</li>`;
}
let codeSection = '<section class="code-section code-snippet__fix hljs">';
if (this.settings.lineNumber) {
codeSection = codeSection + '<ul>'
+ liItems
+ '</ul>';
}
let html = '';
if (lang) {
html = codeSection + '<pre style="max-width:1000% !important;" class="hljs language-'
+ lang
+ '">'
+ body
+ '</pre></section>';
}
else {
html = codeSection + '<pre>'
+ body
+ '</pre></section>';
}
if (!this.settings.isAuthKeyVaild()) {
return html;
}
const settings = AssetsManager.getInstance().expertSettings;
const id = settings.render?.code;
if (id && typeof id === 'number') {
const params = JSON.stringify({
id: `${id}`,
content: html,
});
html = await wxWidget(this.settings.authKey, params);
}
return html;
}
static getMathType(lang: string | null) {
if (!lang) return null;
let l = lang.toLowerCase();
l = l.trim();
if (l === 'am' || l === 'asciimath') return 'asciimath';
if (l === 'latex' || l === 'tex') return 'latex';
return null;
}
parseCard(htmlString: string) {
const id = /data-id="([^"]+)"/;
const headimgRegex = /data-headimg="([^"]+)"/;
const nicknameRegex = /data-nickname="([^"]+)"/;
const signatureRegex = /data-signature="([^"]+)"/;
const idMatch = htmlString.match(id);
const headimgMatch = htmlString.match(headimgRegex);
const nicknameMatch = htmlString.match(nicknameRegex);
const signatureMatch = htmlString.match(signatureRegex);
return {
id: idMatch ? idMatch[1] : '',
headimg: headimgMatch ? headimgMatch[1] : '',
nickname: nicknameMatch ? nicknameMatch[1] : '公众号名称',
signature: signatureMatch ? signatureMatch[1] : '公众号介绍'
};
}
renderCard(token: Tokens.Code) {
const { id, headimg, nickname, signature } = this.parseCard(token.text);
if (id === '') {
return '<span>公众号卡片数据错误没有id</span>';
}
CardDataManager.getInstance().setCardData(id, token.text);
return `<section data-id="${id}" class="note-mpcard-wrapper"><div class="note-mpcard-content"><img class="note-mpcard-headimg" width="54" height="54" src="${headimg}"></img><div class="note-mpcard-info"><div class="note-mpcard-nickname">${nickname}</div><div class="note-mpcard-signature">${signature}</div></div></div><div class="note-mpcard-foot">公众号</div></section>`;
}
renderMermaid(token: Tokens.Code) {
try {
const meraidIndex = this.mermaidIndex;
const containerId = `mermaid-${meraidIndex}`;
this.callback.cacheElement('mermaid', containerId, token.raw);
this.mermaidIndex += 1;
return `<section id="${containerId}" class="${MermaidSectionClassName}"></section>`;
} catch (error) {
console.error(error.message);
return '<span>mermaid渲染失败</span>';
}
}
markedExtension(): MarkedExtension {
return {
async: true,
walkTokens: async (token: Tokens.Generic) => {
if (token.type !== 'code') return;
if (this.settings.isAuthKeyVaild()) {
const type = CodeRenderer.getMathType(token.lang ?? '');
if (type) {
token.html = await MathRendererQueue.getInstance().render(token, false, type);
return;
}
if (token.lang && token.lang.trim().toLocaleLowerCase() == 'mermaid') {
token.html = this.renderMermaid(token as Tokens.Code);
return;
}
}
if (token.lang && token.lang.trim().toLocaleLowerCase() == 'mpcard') {
token.html = this.renderCard(token as Tokens.Code);
return;
}
token.html = await this.codeRenderer(token.text, token.lang);
},
extensions: [{
name: 'code',
level: 'block',
renderer: (token: Tokens.Generic) => {
return token.html;
},
}]
}
}
}