update at 2025-09-22 13:55:41
This commit is contained in:
84
src/markdown/blockquote.ts
Normal file
84
src/markdown/blockquote.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension, MDRendererCallback } from "./extension";
|
||||
import { NMPSettings } from "src/settings";
|
||||
import { App, Vault } from "obsidian";
|
||||
import AssetsManager from "../assets";
|
||||
import { CalloutRenderer } from "./callouts";
|
||||
import { WidgetBox } from "./widget-box";
|
||||
|
||||
export class Blockquote extends Extension {
|
||||
callout: CalloutRenderer;
|
||||
box: WidgetBox;
|
||||
|
||||
constructor(app: App, settings: NMPSettings, assetsManager: AssetsManager, callback: MDRendererCallback) {
|
||||
super(app, settings, assetsManager, callback);
|
||||
this.callout = new CalloutRenderer(app, settings, assetsManager, callback);
|
||||
if (settings.isAuthKeyVaild()) {
|
||||
this.box = new WidgetBox(app, settings, assetsManager, callback);
|
||||
}
|
||||
}
|
||||
|
||||
async prepare() {
|
||||
if (!this.marked) {
|
||||
console.error("marked is not ready");
|
||||
return;
|
||||
}
|
||||
if (this.callout) this.callout.marked = this.marked;
|
||||
if (this.box) this.box.marked = this.marked;
|
||||
return;
|
||||
}
|
||||
|
||||
async renderer(token: Tokens.Blockquote) {
|
||||
if (this.callout.matched(token.text)) {
|
||||
return await this.callout.renderer(token);
|
||||
}
|
||||
|
||||
if (this.box && this.box.matched(token.text)) {
|
||||
return await this.box.renderer(token);
|
||||
}
|
||||
|
||||
const body = this.marked.parser(token.tokens);
|
||||
return `<blockquote>${body}</blockquote>`;
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
async: true,
|
||||
walkTokens: async (token: Tokens.Generic) => {
|
||||
if (token.type !== 'blockquote') {
|
||||
return;
|
||||
}
|
||||
token.html = await this.renderer(token as Tokens.Blockquote);
|
||||
},
|
||||
extensions: [{
|
||||
name: 'blockquote',
|
||||
level: 'block',
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return token.html;
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
275
src/markdown/callouts.ts
Normal file
275
src/markdown/callouts.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension} from "marked";
|
||||
import { Extension } from "./extension";
|
||||
import AssetsManager from "src/assets";
|
||||
import { wxWidget } from "src/weixin-api";
|
||||
|
||||
const icon_note = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>`
|
||||
const icon_abstract = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-clipboard-list"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>`
|
||||
const icon_info = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>`
|
||||
const icon_todo = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-check-circle-2"><circle cx="12" cy="12" r="10"></circle><path d="m9 12 2 2 4-4"></path></svg>`
|
||||
const icon_tip = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>`
|
||||
const icon_success = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-check"><path d="M20 6 9 17l-5-5"></path></svg>`
|
||||
const icon_question = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><path d="M12 17h.01"></path></svg>`
|
||||
const icon_warning = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-alert-triangle"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>`
|
||||
const icon_failure = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-x"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>`
|
||||
const icon_danger = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`
|
||||
const icon_bug = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-bug"><path d="m8 2 1.88 1.88"></path><path d="M14.12 3.88 16 2"></path><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"></path><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"></path><path d="M12 20v-9"></path><path d="M6.53 9C4.6 8.8 3 7.1 3 5"></path><path d="M6 13H2"></path><path d="M3 21c0-2.1 1.7-3.9 3.8-4"></path><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"></path><path d="M22 13h-4"></path><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"></path></svg>`
|
||||
const icon_example = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`
|
||||
const icon_quote = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-quote"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>`
|
||||
/*
|
||||
note,
|
||||
abstract, summary, tldr
|
||||
info
|
||||
todo
|
||||
tip
|
||||
hint, important
|
||||
success, check, done
|
||||
question, help, faq
|
||||
warning, caution, attention
|
||||
failure, fail, missing
|
||||
danger, error
|
||||
bug
|
||||
example
|
||||
quote, cite
|
||||
*/
|
||||
|
||||
type CalloutInfo = {icon: string, style: string}
|
||||
|
||||
const CalloutTypes = new Map<string, CalloutInfo>(Object.entries({
|
||||
note: {
|
||||
icon: icon_note,
|
||||
style: 'note-callout-note',
|
||||
},
|
||||
abstract: {
|
||||
icon: icon_abstract,
|
||||
style: 'note-callout-abstract',
|
||||
},
|
||||
summary: {
|
||||
icon: icon_abstract,
|
||||
style: 'note-callout-abstract',
|
||||
},
|
||||
tldr: {
|
||||
icon: icon_abstract,
|
||||
style: 'note-callout-abstract',
|
||||
},
|
||||
info: {
|
||||
icon: icon_info,
|
||||
style: 'note-callout-note',
|
||||
},
|
||||
todo: {
|
||||
icon: icon_todo,
|
||||
style: 'note-callout-note',
|
||||
},
|
||||
tip: {
|
||||
icon: icon_tip,
|
||||
style: 'note-callout-abstract',
|
||||
},
|
||||
hint: {
|
||||
icon: icon_tip,
|
||||
style: 'note-callout-abstract',
|
||||
},
|
||||
important: {
|
||||
icon: icon_tip,
|
||||
style: 'note-callout-abstract',
|
||||
},
|
||||
success: {
|
||||
icon: icon_success,
|
||||
style: 'note-callout-success',
|
||||
},
|
||||
check: {
|
||||
icon: icon_success,
|
||||
style: 'note-callout-success',
|
||||
},
|
||||
done: {
|
||||
icon: icon_success,
|
||||
style: 'note-callout-success',
|
||||
},
|
||||
question: {
|
||||
icon: icon_question,
|
||||
style: 'note-callout-question',
|
||||
},
|
||||
help: {
|
||||
icon: icon_question,
|
||||
style: 'note-callout-question',
|
||||
},
|
||||
faq: {
|
||||
icon: icon_question,
|
||||
style: 'note-callout-question',
|
||||
},
|
||||
warning: {
|
||||
icon: icon_warning,
|
||||
style: 'note-callout-question',
|
||||
},
|
||||
caution: {
|
||||
icon: icon_warning,
|
||||
style: 'note-callout-question',
|
||||
},
|
||||
attention: {
|
||||
icon: icon_warning,
|
||||
style: 'note-callout-question',
|
||||
},
|
||||
failure: {
|
||||
icon: icon_failure,
|
||||
style: 'note-callout-failure',
|
||||
},
|
||||
fail: {
|
||||
icon: icon_failure,
|
||||
style: 'note-callout-failure',
|
||||
},
|
||||
missing: {
|
||||
icon: icon_failure,
|
||||
style: 'note-callout-failure',
|
||||
},
|
||||
danger: {
|
||||
icon: icon_danger,
|
||||
style: 'note-callout-failure',
|
||||
},
|
||||
error: {
|
||||
icon: icon_danger,
|
||||
style: 'note-callout-failure',
|
||||
},
|
||||
bug: {
|
||||
icon: icon_bug,
|
||||
style: 'note-callout-failure',
|
||||
},
|
||||
example: {
|
||||
icon: icon_example,
|
||||
style: 'note-callout-example',
|
||||
},
|
||||
quote: {
|
||||
icon: icon_quote,
|
||||
style: 'note-callout-quote',
|
||||
},
|
||||
cite: {
|
||||
icon: icon_quote,
|
||||
style: 'note-callout-quote',
|
||||
}
|
||||
}));
|
||||
|
||||
function GetCallout(type: string) {
|
||||
return CalloutTypes.get(type);
|
||||
};
|
||||
|
||||
function matchCallouts(text:string) {
|
||||
const regex = /\[\!(.*?)\]/g;
|
||||
let m;
|
||||
if( m = regex.exec(text)) {
|
||||
return m[1];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function GetCalloutTitle(callout:string, text:string) {
|
||||
let title = callout.charAt(0).toUpperCase() + callout.slice(1).toLowerCase();
|
||||
let start = text.indexOf(']') + 1;
|
||||
if (text.indexOf(']-') > 0 || text.indexOf(']+') > 0) {
|
||||
start = start + 1;
|
||||
}
|
||||
let end = text.indexOf('\n');
|
||||
if (end === -1) end = text.length;
|
||||
if (start >= end) return title;
|
||||
const customTitle = text.slice(start, end).trim();
|
||||
if (customTitle !== '') {
|
||||
title = customTitle;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
export class CalloutRenderer extends Extension {
|
||||
matched(text: string) {
|
||||
return matchCallouts(text) != '';
|
||||
}
|
||||
|
||||
async renderer(token: Tokens.Blockquote) {
|
||||
let callout = matchCallouts(token.text);
|
||||
if (callout == '') {
|
||||
const body = this.marked.parser(token.tokens);
|
||||
return `<blockquote>${body}</blockquote>`;;
|
||||
}
|
||||
|
||||
const title = GetCalloutTitle(callout, token.text);
|
||||
const index = token.text.indexOf('\n');
|
||||
let body = '';
|
||||
if (index > 0) {
|
||||
token.text = token.text.slice(index+1)
|
||||
body = await this.marked.parse(token.text);
|
||||
}
|
||||
|
||||
const setting = AssetsManager.getInstance().expertSettings.render?.callout as { [key: string]: any };
|
||||
if (setting && callout.toLocaleLowerCase() in setting) {
|
||||
const authkey = this.settings.authKey;
|
||||
const widget = setting[callout.toLocaleLowerCase()];
|
||||
if (typeof widget === 'number') {
|
||||
return await wxWidget(authkey, JSON.stringify({
|
||||
id: `${widget}`,
|
||||
title,
|
||||
content: body,
|
||||
}));
|
||||
}
|
||||
if (typeof widget === 'object') {
|
||||
const {id, style} = widget;
|
||||
return await wxWidget(authkey, JSON.stringify({
|
||||
id: `${id}`,
|
||||
title,
|
||||
style: style || {},
|
||||
content: body,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let info = GetCallout(callout.toLowerCase());
|
||||
if (info == null) {
|
||||
const svg = await this.assetsManager.loadIcon(callout);
|
||||
if (svg) {
|
||||
info = {icon: svg, style: 'note-callout-custom'}
|
||||
}
|
||||
else {
|
||||
info = GetCallout('note');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return `<section class="note-callout ${info?.style}"><section class="note-callout-title-wrap"><span class="note-callout-icon">${info?.icon}</span><span class="note-callout-title">${title}<span></section><section class="note-callout-content">${body}</section></section>`;
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
async: true,
|
||||
walkTokens: async (token: Tokens.Generic) => {
|
||||
if (token.type !== 'blockquote') {
|
||||
return;
|
||||
}
|
||||
token.html = await this.renderer(token as Tokens.Blockquote);
|
||||
},
|
||||
extensions:[{
|
||||
name: 'blockquote',
|
||||
level: 'block',
|
||||
renderer: (token: Tokens.Generic)=> {
|
||||
return token.html;
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
287
src/markdown/code.ts
Normal file
287
src/markdown/code.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
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 += ' ';
|
||||
} else if (char === '\t') {
|
||||
result += ' ';
|
||||
} 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;
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
78
src/markdown/commnet.ts
Normal file
78
src/markdown/commnet.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
|
||||
const commentRegex = /^%%([\s\S]*?)%%/;
|
||||
|
||||
export class Comment extends Extension {
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
extensions: [{
|
||||
name: 'CommentInline',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
let index;
|
||||
let indexSrc = src;
|
||||
|
||||
while (indexSrc) {
|
||||
index = indexSrc.indexOf('%%');
|
||||
if (index === -1) return;
|
||||
return index;
|
||||
}
|
||||
},
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(commentRegex);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'CommentInline',
|
||||
raw: match[0],
|
||||
text: match[1],
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: Tokens.Generic) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'CommentBlock',
|
||||
level: 'block',
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(commentRegex);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'CommentBlock',
|
||||
raw: match[0],
|
||||
text: match[1],
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: Tokens.Generic) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/markdown/embed-block-mark.ts
Normal file
62
src/markdown/embed-block-mark.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
|
||||
const BlockMarkRegex = /^\^[0-9A-Za-z-]+$/;
|
||||
|
||||
export class EmbedBlockMark extends Extension {
|
||||
allLinks:string[] = [];
|
||||
async prepare() {
|
||||
this.allLinks = [];
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
extensions: [{
|
||||
name: 'EmbedBlockMark',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
let index = src.indexOf('^');
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(BlockMarkRegex);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'EmbedBlockMark',
|
||||
raw: match[0],
|
||||
text: match[0]
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return `<span data-txt="${token.text}"></span}`;
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/markdown/empty-line.ts
Normal file
48
src/markdown/empty-line.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
|
||||
export class EmptyLineRenderer extends Extension {
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
extensions: [{
|
||||
name: 'emptyline',
|
||||
level: 'block',
|
||||
tokenizer(src: string) {
|
||||
const match = /^\n\n+/.exec(src);
|
||||
if (match) {
|
||||
console.log('mathced src: ', src)
|
||||
return {
|
||||
type: "emptyline",
|
||||
raw: match[0],
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return '<p><br></p>'.repeat(token.raw.length - 1);
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/markdown/extension.ts
Normal file
55
src/markdown/extension.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { NMPSettings } from "src/settings";
|
||||
import { Marked, MarkedExtension } from "marked";
|
||||
import { App, Vault } from "obsidian";
|
||||
import AssetsManager from "../assets";
|
||||
|
||||
export interface MDRendererCallback {
|
||||
settings: NMPSettings;
|
||||
updateElementByID(id:string, html:string):void; // 改为异步渲染后已废弃
|
||||
cacheElement(category: string, id: string, data: string): void;
|
||||
}
|
||||
|
||||
export abstract class Extension {
|
||||
app: App;
|
||||
vault: Vault;
|
||||
assetsManager: AssetsManager
|
||||
settings: NMPSettings;
|
||||
callback: MDRendererCallback;
|
||||
marked: Marked;
|
||||
|
||||
constructor(app: App, settings: NMPSettings, assetsManager: AssetsManager, callback: MDRendererCallback) {
|
||||
this.app = app;
|
||||
this.vault = app.vault;
|
||||
this.settings = settings;
|
||||
this.assetsManager = assetsManager;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
async prepare() { return; }
|
||||
async postprocess(html:string) { return html; }
|
||||
async beforePublish() { }
|
||||
async cleanup() { return; }
|
||||
abstract markedExtension(): MarkedExtension
|
||||
}
|
||||
104
src/markdown/footnote.ts
Normal file
104
src/markdown/footnote.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
|
||||
const refRule = /^\[\^([^\]]+)\]/; // 匹配 [^label]
|
||||
const defRule = /^ *\[\^([^\]]+)\]:/; // 匹配 [^label]:
|
||||
|
||||
export class FootnoteRenderer extends Extension {
|
||||
allDefs: any[] = [];
|
||||
defCounter = 0;
|
||||
async prepare() {
|
||||
this.allDefs = [];
|
||||
this.defCounter = 0;
|
||||
}
|
||||
|
||||
async postprocess(html: string) {
|
||||
if (this.allDefs.length == 0) {
|
||||
return html;
|
||||
}
|
||||
|
||||
let body = '';
|
||||
for (const def of this.allDefs) {
|
||||
const {label, content} = def;
|
||||
const html = await this.marked.parse(content);
|
||||
const id = `fn-${label}`;
|
||||
body += `<li id="${id}">${html}</li>`;
|
||||
}
|
||||
return html + `<section class="footnotes"><hr><ol>${body}</ol></section>`;
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
extensions: [
|
||||
{
|
||||
name: 'FootnoteRef',
|
||||
level: 'inline',
|
||||
start(src) {
|
||||
const index = src.indexOf('[^');
|
||||
return index > 0 ? index : -1;
|
||||
},
|
||||
tokenizer: (src) => {
|
||||
const match = src.match(refRule);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'FootnoteRef',
|
||||
raw: match[0],
|
||||
text: match[1],
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
this.defCounter += 1;
|
||||
const id = `fnref-${this.defCounter}`;
|
||||
return `<sup id="${id}">${this.defCounter}</sup>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'FootnoteDef',
|
||||
level: 'block',
|
||||
tokenizer: (src) => {
|
||||
const match = src.match(defRule);
|
||||
if (match) {
|
||||
const label = match[1].trim();
|
||||
const end = src.indexOf('\n');
|
||||
const raw = end === -1 ? src: src.substring(0, end + 1);
|
||||
const content = raw.substring(match[0].length);
|
||||
this.allDefs.push({label, content});
|
||||
|
||||
return {
|
||||
type: 'FootnoteDef',
|
||||
raw: raw,
|
||||
text: content,
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/markdown/heading.ts
Normal file
131
src/markdown/heading.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
import AssetsManager from "src/assets";
|
||||
import { ExpertSettings } from "src/expert-settings";
|
||||
import { wxWidget } from "src/weixin-api";
|
||||
|
||||
export class HeadingRenderer extends Extension {
|
||||
index = [0, 0, 0, 0];
|
||||
expertSettings: ExpertSettings;
|
||||
headingSettings: any[]
|
||||
|
||||
async prepare() {
|
||||
this.index = [0, 0, 0, 0];
|
||||
this.expertSettings = AssetsManager.getInstance().expertSettings;
|
||||
this.headingSettings = [undefined, undefined, undefined, undefined];
|
||||
if (!this.expertSettings.render) {
|
||||
return;
|
||||
}
|
||||
if (this.expertSettings.render.h1) {
|
||||
this.headingSettings[1] = this.expertSettings.render.h1;
|
||||
}
|
||||
if (this.expertSettings.render.h2) {
|
||||
this.headingSettings[2] = this.expertSettings.render.h2;
|
||||
}
|
||||
if (this.expertSettings.render.h3) {
|
||||
this.headingSettings[3] = this.expertSettings.render.h3;
|
||||
}
|
||||
}
|
||||
|
||||
async renderWithTemplate(token: Tokens.Heading, template: string) {
|
||||
const content = await this.marked.parseInline(token.text);
|
||||
return template.replace('{content}', content);
|
||||
}
|
||||
|
||||
async renderWithWidgetId(token: Tokens.Heading, widgetId: number) {
|
||||
const authkey = this.settings.authKey;
|
||||
const content = await this.marked.parseInline(token.text);
|
||||
const params = JSON.stringify({
|
||||
id: `${widgetId}`,
|
||||
title: content,
|
||||
});
|
||||
return await wxWidget(authkey, params);
|
||||
}
|
||||
|
||||
async renderWithWidget(token: Tokens.Heading, widgetId: number, counter: boolean|undefined, len: number|undefined, style: object|undefined = undefined) {
|
||||
const authkey = this.settings.authKey;
|
||||
let title = token.text;
|
||||
if (counter === undefined) {
|
||||
counter = false;
|
||||
}
|
||||
if (len === undefined) {
|
||||
len = 1;
|
||||
}
|
||||
if (style === undefined) {
|
||||
style = new Map<string, string>();
|
||||
}
|
||||
if (counter) {
|
||||
title = `${this.index[token.depth]}`;
|
||||
if (title.length < len) {
|
||||
title = title.padStart(len, '0');
|
||||
}
|
||||
}
|
||||
const content = await this.marked.parseInline(token.text);
|
||||
const params = JSON.stringify({
|
||||
id: `${widgetId}`,
|
||||
title,
|
||||
style,
|
||||
content: '<p>' + content + '</p>',
|
||||
});
|
||||
return await wxWidget(authkey, params);
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
async: true,
|
||||
walkTokens: async (token: Tokens.Generic) => {
|
||||
if (token.type !== 'heading') {
|
||||
return;
|
||||
}
|
||||
|
||||
const setting = this.headingSettings[token.depth];
|
||||
this.index[token.depth] += 1;
|
||||
if (setting) {
|
||||
if (typeof setting === 'string') {
|
||||
token.html = await this.renderWithTemplate(token as Tokens.Heading, setting);
|
||||
}
|
||||
else if (typeof setting === 'number') {
|
||||
token.html = await this.renderWithWidgetId(token as Tokens.Heading, setting);
|
||||
}
|
||||
else {
|
||||
const { id, counter, len, style } = setting;
|
||||
token.html = await this.renderWithWidget(token as Tokens.Heading, id, counter, len, style);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await this.marked.parseInline(token.text);
|
||||
token.html = `<h${token.depth}>${body}</h${token.depth}>`;
|
||||
},
|
||||
extensions: [{
|
||||
name: 'heading',
|
||||
level: 'block',
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return token.html;
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/markdown/icons.ts
Normal file
122
src/markdown/icons.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
|
||||
const iconsRegex = /^\[:(.*?):\]/;
|
||||
|
||||
export class SVGIcon extends Extension {
|
||||
isNumeric(str: string): boolean {
|
||||
return !isNaN(Number(str)) && str.trim() !== '';
|
||||
}
|
||||
|
||||
getSize(size: string) {
|
||||
const items = size.split('x');
|
||||
let width, height;
|
||||
if (items.length == 2) {
|
||||
width = items[0];
|
||||
height = items[1];
|
||||
}
|
||||
else {
|
||||
width = items[0];
|
||||
height = items[0];
|
||||
}
|
||||
width = this.isNumeric(width) ? width+'px' : width;
|
||||
height = this.isNumeric(height) ? height+'px' : height;
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
renderStyle(items: string[]) {
|
||||
let size = '';
|
||||
let color = '';
|
||||
if (items.length == 3) {
|
||||
size = items[1];
|
||||
color = items[2];
|
||||
}
|
||||
else if (items.length == 2) {
|
||||
if (items[1].startsWith('#')) {
|
||||
color = items[1];
|
||||
}
|
||||
else {
|
||||
size = items[1];
|
||||
}
|
||||
}
|
||||
let style = '';
|
||||
if (size.length > 0) {
|
||||
const {width, height} = this.getSize(size);
|
||||
style += `width:${width};height:${height};`;
|
||||
}
|
||||
if (color.length > 0) {
|
||||
style += `color:${color};`;
|
||||
}
|
||||
return style.length > 0 ? `style="${style}"` : '';
|
||||
}
|
||||
|
||||
async render(text: string) {
|
||||
const items = text.split('|');
|
||||
const name = items[0];
|
||||
const svg = await this.assetsManager.loadIcon(name);
|
||||
const body = svg==='' ? '未找到图标' + name : svg;
|
||||
const style = this.renderStyle(items);
|
||||
return `<span class="note-svg-icon" ${style}>${body}</span>`
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
async: true,
|
||||
walkTokens: async (token: Tokens.Generic) => {
|
||||
if (token.type !== 'SVGIcon') {
|
||||
return;
|
||||
}
|
||||
token.html = await this.render(token.text);
|
||||
},
|
||||
extensions: [{
|
||||
name: 'SVGIcon',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
let index;
|
||||
let indexSrc = src;
|
||||
|
||||
while (indexSrc) {
|
||||
index = indexSrc.indexOf('[:');
|
||||
if (index === -1) return;
|
||||
return index;
|
||||
}
|
||||
},
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(iconsRegex);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'SVGIcon',
|
||||
raw: match[0],
|
||||
text: match[1],
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: Tokens.Generic) {
|
||||
return token.html;
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/markdown/link.ts
Normal file
69
src/markdown/link.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
|
||||
export class LinkRenderer extends Extension {
|
||||
allLinks:string[] = [];
|
||||
async prepare() {
|
||||
this.allLinks = [];
|
||||
}
|
||||
|
||||
async postprocess(html: string) {
|
||||
if (this.settings.linkStyle !== 'footnote'
|
||||
|| this.allLinks.length == 0) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const links = this.allLinks.map((href, i) => {
|
||||
return `<li>${href} ↩</li>`;
|
||||
});
|
||||
return `${html}<seciton class="footnotes"><hr><ol>${links.join('')}</ol></section>`;
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
extensions: [{
|
||||
name: 'link',
|
||||
level: 'inline',
|
||||
renderer: (token: Tokens.Link) => {
|
||||
if (token.href.startsWith('mailto:')) {
|
||||
return token.text;
|
||||
}
|
||||
if (token.text.indexOf(token.href) === 0
|
||||
|| (token.href.indexOf('https://mp.weixin.qq.com/mp') === 0)
|
||||
|| (token.href.indexOf('https://mp.weixin.qq.com/s') === 0)) {
|
||||
return `<a href="${token.href}">${token.text}</a>`;
|
||||
}
|
||||
this.allLinks.push(token.href);
|
||||
if (this.settings.linkStyle == 'footnote') {
|
||||
return `<a>${token.text}<sup>[${this.allLinks.length}]</sup></a>`;
|
||||
}
|
||||
else {
|
||||
return `<a>${token.text}[${token.href}]</a>`;
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
831
src/markdown/local-file.ts
Normal file
831
src/markdown/local-file.ts
Normal file
@@ -0,0 +1,831 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Token, Tokens, MarkedExtension } from "marked";
|
||||
import { Notice, TAbstractFile, TFile, Vault, MarkdownView, requestUrl, Platform } from "obsidian";
|
||||
import { Extension } from "./extension";
|
||||
import { NMPSettings } from "../settings";
|
||||
import { IsImageLibReady, PrepareImageLib, WebpToJPG, UploadImageToWx } from "../imagelib";
|
||||
|
||||
declare module 'obsidian' {
|
||||
interface Vault {
|
||||
config: {
|
||||
attachmentFolderPath: string;
|
||||
newLinkFormat: string;
|
||||
useMarkdownLinks: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const LocalFileRegex = /^!\[\[(.*?)\]\]/;
|
||||
|
||||
interface ImageInfo {
|
||||
resUrl: string;
|
||||
filePath: string;
|
||||
url: string | null;
|
||||
media_id: string | null;
|
||||
}
|
||||
|
||||
export class LocalImageManager {
|
||||
private images: Map<string, ImageInfo>;
|
||||
private static instance: LocalImageManager;
|
||||
|
||||
private constructor() {
|
||||
this.images = new Map<string, ImageInfo>();
|
||||
}
|
||||
|
||||
// 静态方法,用于获取实例
|
||||
public static getInstance(): LocalImageManager {
|
||||
if (!LocalImageManager.instance) {
|
||||
LocalImageManager.instance = new LocalImageManager();
|
||||
}
|
||||
return LocalImageManager.instance;
|
||||
}
|
||||
|
||||
public setImage(path: string, info: ImageInfo): void {
|
||||
if (!this.images.has(path)) {
|
||||
this.images.set(path, info);
|
||||
}
|
||||
}
|
||||
|
||||
isWebp(file: TFile | string): boolean {
|
||||
if (file instanceof TFile) {
|
||||
return file.extension.toLowerCase() === 'webp';
|
||||
}
|
||||
const name = file.toLowerCase();
|
||||
return name.endsWith('.webp');
|
||||
}
|
||||
|
||||
async uploadLocalImage(token: string, vault: Vault, type: string = '') {
|
||||
const keys = this.images.keys();
|
||||
await PrepareImageLib();
|
||||
const result = [];
|
||||
for (let key of keys) {
|
||||
const value = this.images.get(key);
|
||||
if (value == null) continue;
|
||||
if (value.url != null) continue;
|
||||
const file = vault.getFileByPath(value.filePath);
|
||||
if (file == null) continue;
|
||||
let fileData = await vault.readBinary(file);
|
||||
let name = file.name;
|
||||
if (this.isWebp(file)) {
|
||||
if (IsImageLibReady()) {
|
||||
fileData = WebpToJPG(fileData);
|
||||
name = name.toLowerCase().replace('.webp', '.jpg');
|
||||
}
|
||||
else {
|
||||
console.error('wasm not ready for webp');
|
||||
}
|
||||
}
|
||||
|
||||
const res = await UploadImageToWx(new Blob([fileData]), name, token, type);
|
||||
if (res.errcode != 0) {
|
||||
const msg = `上传图片失败: ${res.errcode} ${res.errmsg}`;
|
||||
new Notice(msg);
|
||||
console.error(msg);
|
||||
}
|
||||
|
||||
value.url = res.url;
|
||||
value.media_id = res.media_id;
|
||||
result.push(res);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
checkImageExt(filename: string ): boolean {
|
||||
const name = filename.toLowerCase();
|
||||
|
||||
if (name.endsWith('.jpg')
|
||||
|| name.endsWith('.jpeg')
|
||||
|| name.endsWith('.png')
|
||||
|| name.endsWith('.gif')
|
||||
|| name.endsWith('.bmp')
|
||||
|| name.endsWith('.tiff')
|
||||
|| name.endsWith('.svg')
|
||||
|| name.endsWith('.webp')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getImageNameFromUrl(url: string, type: string): string {
|
||||
try {
|
||||
// 创建URL对象
|
||||
const urlObj = new URL(url);
|
||||
// 获取pathname部分
|
||||
const pathname = urlObj.pathname;
|
||||
// 获取最后一个/后的内容作为文件名
|
||||
let filename = pathname.split('/').pop() || '';
|
||||
filename = decodeURIComponent(filename);
|
||||
if (!this.checkImageExt(filename)) {
|
||||
filename = filename + this.getImageExt(type);
|
||||
}
|
||||
return filename;
|
||||
} catch (e) {
|
||||
// 如果URL解析失败,尝试简单的字符串处理
|
||||
const queryIndex = url.indexOf('?');
|
||||
if (queryIndex !== -1) {
|
||||
url = url.substring(0, queryIndex);
|
||||
}
|
||||
return url.split('/').pop() || '';
|
||||
}
|
||||
}
|
||||
|
||||
getImageExtFromBlob(blob: Blob): string {
|
||||
// MIME类型到文件扩展名的映射
|
||||
const mimeToExt: { [key: string]: string } = {
|
||||
'image/jpeg': '.jpg',
|
||||
'image/jpg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/gif': '.gif',
|
||||
'image/bmp': '.bmp',
|
||||
'image/webp': '.webp',
|
||||
'image/svg+xml': '.svg',
|
||||
'image/tiff': '.tiff'
|
||||
};
|
||||
|
||||
// 获取MIME类型
|
||||
const mimeType = blob.type.toLowerCase();
|
||||
|
||||
// 返回对应的扩展名,如果找不到则返回空字符串
|
||||
return mimeToExt[mimeType] || '';
|
||||
}
|
||||
|
||||
base64ToBlob(src: string) {
|
||||
const items = src.split(',');
|
||||
if (items.length != 2) {
|
||||
throw new Error('base64格式错误');
|
||||
}
|
||||
const mineType = items[0].replace('data:', '');
|
||||
const base64 = items[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 {blob: new Blob([byteArray], { type: mineType }), ext: this.getImageExt(mineType)};
|
||||
}
|
||||
|
||||
async uploadImageFromUrl(url: string, token: string, type: string = '') {
|
||||
try {
|
||||
const rep = await requestUrl(url);
|
||||
await PrepareImageLib();
|
||||
let data = rep.arrayBuffer;
|
||||
let blob = new Blob([data]);
|
||||
|
||||
let filename = this.getImageNameFromUrl(url, rep.headers['content-type']);
|
||||
if (filename == '' || filename == null) {
|
||||
filename = 'remote_img' + this.getImageExtFromBlob(blob);
|
||||
}
|
||||
|
||||
if (this.isWebp(filename)) {
|
||||
if (IsImageLibReady()) {
|
||||
data = WebpToJPG(data);
|
||||
blob = new Blob([data]);
|
||||
filename = filename.toLowerCase().replace('.webp', '.jpg');
|
||||
}
|
||||
else {
|
||||
console.error('wasm not ready for webp');
|
||||
}
|
||||
}
|
||||
|
||||
return await UploadImageToWx(blob, filename, token, type);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
throw new Error('上传图片失败:' + e.message + '|' + url);
|
||||
}
|
||||
}
|
||||
|
||||
getImageExt(type: string): string {
|
||||
const mimeToExt: { [key: string]: string } = {
|
||||
'image/jpeg': '.jpg',
|
||||
'image/jpg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/gif': '.gif',
|
||||
'image/bmp': '.bmp',
|
||||
'image/webp': '.webp',
|
||||
'image/svg+xml': '.svg',
|
||||
'image/tiff': '.tiff'
|
||||
};
|
||||
return mimeToExt[type] || '.jpg';
|
||||
}
|
||||
|
||||
getMimeType(ext: string): string {
|
||||
const extToMime: { [key: string]: string } = {
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif',
|
||||
'.bmp': 'image/bmp',
|
||||
'.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.tiff': 'image/tiff'
|
||||
};
|
||||
return extToMime[ext.toLowerCase()] || 'image/jpeg';
|
||||
}
|
||||
|
||||
getImageInfos(root: HTMLElement) {
|
||||
const images = root.getElementsByTagName('img');
|
||||
const result = [];
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
const res = this.images.get(img.src);
|
||||
if (res) {
|
||||
result.push(res);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async uploadRemoteImage(root: HTMLElement, token: string, type: string = '') {
|
||||
const images = root.getElementsByTagName('img');
|
||||
const result = [];
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.src.includes('mmbiz.qpic.cn')) continue;
|
||||
// 移动端本地图片不通过src上传
|
||||
if (img.src.startsWith('http://localhost/') && Platform.isMobileApp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (img.src.startsWith('http')) {
|
||||
const res = await this.uploadImageFromUrl(img.src, token, type);
|
||||
if (res.errcode != 0) {
|
||||
const msg = `上传图片失败: ${img.src} ${res.errcode} ${res.errmsg}`;
|
||||
new Notice(msg);
|
||||
console.error(msg);
|
||||
}
|
||||
const info = {
|
||||
resUrl: img.src,
|
||||
filePath: "",
|
||||
url: res.url,
|
||||
media_id: res.media_id,
|
||||
};
|
||||
this.images.set(img.src, info);
|
||||
result.push(res);
|
||||
}
|
||||
else if (img.src.startsWith('data:image/')) {
|
||||
const {blob, ext} = this.base64ToBlob(img.src);
|
||||
if (!img.id) {
|
||||
img.id = `local-img-${i}`;
|
||||
}
|
||||
const name = img.id + ext;
|
||||
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 info = {
|
||||
resUrl: '#' + img.id,
|
||||
filePath: "",
|
||||
url: res.url,
|
||||
media_id: res.media_id,
|
||||
};
|
||||
this.images.set('#' + img.id, info);
|
||||
result.push(res);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
replaceImages(root: HTMLElement) {
|
||||
const images = root.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
let value = this.images.get(img.src);
|
||||
if (value == null) {
|
||||
if (!img.id) {
|
||||
console.error('miss image id, ' + img.src);
|
||||
continue;
|
||||
}
|
||||
value = this.images.get('#' + img.id);
|
||||
}
|
||||
if (value == null) continue;
|
||||
if (value.url == null) continue;
|
||||
img.setAttribute('src', value.url);
|
||||
}
|
||||
}
|
||||
|
||||
arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async localImagesToBase64(vault: Vault) {
|
||||
const keys = this.images.keys();
|
||||
const result = new Map<string, string>();
|
||||
for (let key of keys) {
|
||||
const value = this.images.get(key);
|
||||
if (value == null) continue;
|
||||
const file = vault.getFileByPath(value.filePath);
|
||||
if (file == null) continue;
|
||||
let fileData = await vault.readBinary(file);
|
||||
const base64 = this.arrayBufferToBase64(fileData);
|
||||
const mimeType = this.getMimeType(file.extension);
|
||||
const data = `data:${mimeType};base64,${base64}`;
|
||||
result.set(value.resUrl, data);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async downloadRemoteImage(url: string) {
|
||||
try {
|
||||
const rep = await requestUrl(url);
|
||||
let data = rep.arrayBuffer;
|
||||
let blob = new Blob([data]);
|
||||
|
||||
let ext = this.getImageExtFromBlob(blob);
|
||||
if (ext == '' || ext == null) {
|
||||
const filename = this.getImageNameFromUrl(url, rep.headers['content-type']);
|
||||
ext = '.' + filename.split('.').pop() || 'jpg';
|
||||
}
|
||||
|
||||
const base64 = this.arrayBufferToBase64(data);
|
||||
const mimeType = this.getMimeType(ext);
|
||||
return `data:${mimeType};base64,${base64}`;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async remoteImagesToBase64(root: HTMLElement) {
|
||||
const images = root.getElementsByTagName('img');
|
||||
const result = new Map<string, string>();
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (!img.src.startsWith('http')) continue;
|
||||
const base64 = await this.downloadRemoteImage(img.src);
|
||||
if (base64 == '') continue;
|
||||
result.set(img.src, base64);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async embleImages(root: HTMLElement, vault: Vault) {
|
||||
const localImages = await this.localImagesToBase64(vault);
|
||||
const remoteImages = await this.remoteImagesToBase64(root);
|
||||
const result = root.cloneNode(true) as HTMLElement;
|
||||
const images = result.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.src.startsWith('http')) {
|
||||
const base64 = remoteImages.get(img.src);
|
||||
if (base64 != null) {
|
||||
img.setAttribute('src', base64);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const base64 = localImages.get(img.src);
|
||||
if (base64 != null) {
|
||||
img.setAttribute('src', base64);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.innerHTML;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
this.images.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class LocalFile extends Extension{
|
||||
index: number = 0;
|
||||
public static fileCache: Map<string, string> = new Map<string, string>();
|
||||
|
||||
generateId() {
|
||||
this.index += 1;
|
||||
return `fid-${this.index}`;
|
||||
}
|
||||
|
||||
getImagePath(path: string) {
|
||||
const res = this.assetsManager.getResourcePath(path);
|
||||
if (res == null) {
|
||||
console.error('找不到文件:' + path);
|
||||
return '';
|
||||
}
|
||||
const info = {
|
||||
resUrl: res.resUrl,
|
||||
filePath: res.filePath,
|
||||
media_id: null,
|
||||
url: null
|
||||
};
|
||||
LocalImageManager.getInstance().setImage(res.resUrl, info);
|
||||
return res.resUrl;
|
||||
}
|
||||
|
||||
isImage(file: string) {
|
||||
file = file.toLowerCase();
|
||||
return file.endsWith('.png')
|
||||
|| file.endsWith('.jpg')
|
||||
|| file.endsWith('.jpeg')
|
||||
|| file.endsWith('.gif')
|
||||
|| file.endsWith('.bmp')
|
||||
|| file.endsWith('.webp');
|
||||
}
|
||||
|
||||
parseImageLink(link: string) {
|
||||
if (link.includes('|')) {
|
||||
const parts = link.split('|');
|
||||
const path = parts[0];
|
||||
if (!this.isImage(path)) return null;
|
||||
|
||||
let width = null;
|
||||
let height = null;
|
||||
if (parts.length == 2) {
|
||||
const size = parts[1].toLowerCase().split('x');
|
||||
width = parseInt(size[0]);
|
||||
if (size.length == 2 && size[1] != '') {
|
||||
height = parseInt(size[1]);
|
||||
}
|
||||
}
|
||||
return { path, width, height };
|
||||
}
|
||||
if (this.isImage(link)) {
|
||||
return { path: link, width: null, height: null };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getHeaderLevel(line: string) {
|
||||
const match = line.trimStart().match(/^#{1,6}/);
|
||||
if (match) {
|
||||
return match[0].length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async getFileContent(file: TAbstractFile, header: string | null, block: string | null) {
|
||||
const content = await this.app.vault.adapter.read(file.path);
|
||||
if (header == null && block == null) {
|
||||
return content;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
const lines = content.split('\n');
|
||||
if (header) {
|
||||
let level = 0;
|
||||
let append = false;
|
||||
for (let line of lines) {
|
||||
if (append) {
|
||||
if (level == this.getHeaderLevel(line)) {
|
||||
break;
|
||||
}
|
||||
result += line + '\n';
|
||||
continue;
|
||||
}
|
||||
if (!line.trim().startsWith('#')) continue;
|
||||
const items = line.trim().split(' ');
|
||||
if (items.length != 2) continue;
|
||||
if (header.trim() != items[1].trim()) continue;
|
||||
if (this.getHeaderLevel(line)) {
|
||||
result += line + '\n';
|
||||
level = this.getHeaderLevel(line);
|
||||
append = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isStructuredBlock(line: string) {
|
||||
const trimmed = line.trim();
|
||||
return trimmed.startsWith('-') || trimmed.startsWith('>') || trimmed.startsWith('|') || trimmed.match(/^\d+\./);
|
||||
}
|
||||
|
||||
if (block) {
|
||||
let stopAtEmpty = false;
|
||||
let totalLen = 0;
|
||||
let structured = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.indexOf(block) >= 0) {
|
||||
result = line.replace(block, '').trim();
|
||||
|
||||
// 标记和结构化内容位于同一行的时候只返回当前的条目
|
||||
if (isStructuredBlock(line)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 向上查找内容
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
const l = lines[j];
|
||||
|
||||
if (l.startsWith('#')) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (l.trim() == '') {
|
||||
if (stopAtEmpty) break;
|
||||
if (j < i - 1 && totalLen > 0) break;
|
||||
stopAtEmpty = true;
|
||||
result = l + '\n' + result;
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
stopAtEmpty = true;
|
||||
}
|
||||
|
||||
if (structured && !isStructuredBlock(l)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (totalLen === 0 && isStructuredBlock(l)) {
|
||||
structured = true;
|
||||
}
|
||||
|
||||
totalLen += result.length;
|
||||
result = l + '\n' + result;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
parseFileLink(link: string) {
|
||||
const info = link.split('|')[0];
|
||||
const items = info.split('#');
|
||||
let path = items[0];
|
||||
let header = null;
|
||||
let block = null;
|
||||
if (items.length == 2) {
|
||||
if (items[1].startsWith('^')) {
|
||||
block = items[1];
|
||||
} else {
|
||||
header = items[1];
|
||||
}
|
||||
}
|
||||
return { path, head: header, block };
|
||||
}
|
||||
|
||||
async renderFile(link: string, id: string) {
|
||||
let { path, head: header, block} = this.parseFileLink(link);
|
||||
let file = null;
|
||||
if (path === '') {
|
||||
file = this.app.workspace.getActiveFile();
|
||||
}
|
||||
else {
|
||||
if (!path.endsWith('.md')) {
|
||||
path = path + '.md';
|
||||
}
|
||||
file = this.assetsManager.searchFile(path);
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
const msg = '找不到文件:' + path;
|
||||
console.error(msg)
|
||||
return msg;
|
||||
}
|
||||
|
||||
let content = await this.getFileContent(file, header, block);
|
||||
if (content.startsWith('---')) {
|
||||
content = content.replace(/^(---)$.+?^(---)$.+?/ims, '');
|
||||
}
|
||||
const body = await this.marked.parse(content);
|
||||
return body;
|
||||
}
|
||||
|
||||
static async readBlob(src: string) {
|
||||
return await fetch(src).then(response => response.blob())
|
||||
}
|
||||
|
||||
static async getExcalidrawUrl(data: string) {
|
||||
const url = 'https://obplugin.sunboshi.tech/math/excalidraw';
|
||||
const req = await requestUrl({
|
||||
url,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
authkey: NMPSettings.getInstance().authKey
|
||||
},
|
||||
body: JSON.stringify({ data })
|
||||
});
|
||||
|
||||
if (req.status != 200) {
|
||||
console.error(req.status);
|
||||
return null;
|
||||
}
|
||||
return req.json.url;
|
||||
}
|
||||
|
||||
parseLinkStyle(link: string) {
|
||||
let filename = '';
|
||||
let style = 'style="width:100%;height:100%"';
|
||||
let postion = 'left';
|
||||
const postions = ['left', 'center', 'right'];
|
||||
if (link.includes('|')) {
|
||||
const items = link.split('|');
|
||||
filename = items[0];
|
||||
let size = '';
|
||||
if (items.length == 2) {
|
||||
if (postions.includes(items[1])) {
|
||||
postion = items[1];
|
||||
}
|
||||
else {
|
||||
size = items[1];
|
||||
}
|
||||
}
|
||||
else if (items.length == 3) {
|
||||
size = items[1];
|
||||
if (postions.includes(items[1])) {
|
||||
size = items[2];
|
||||
postion = items[1];
|
||||
}
|
||||
else {
|
||||
size = items[1];
|
||||
postion = items[2];
|
||||
}
|
||||
}
|
||||
if (size != '') {
|
||||
const sizes = size.split('x');
|
||||
if (sizes.length == 2) {
|
||||
style = `style="width:${sizes[0]}px;height:${sizes[1]}px;"`
|
||||
}
|
||||
else {
|
||||
style = `style="width:${sizes[0]}px;"`
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
filename = link;
|
||||
}
|
||||
return { filename, style, postion };
|
||||
}
|
||||
|
||||
parseExcalidrawLink(link: string) {
|
||||
let classname = 'note-embed-excalidraw-left';
|
||||
const postions = new Map<string, string>([
|
||||
['left', 'note-embed-excalidraw-left'],
|
||||
['center', 'note-embed-excalidraw-center'],
|
||||
['right', 'note-embed-excalidraw-right']
|
||||
])
|
||||
|
||||
let {filename, style, postion} = this.parseLinkStyle(link);
|
||||
classname = postions.get(postion) || classname;
|
||||
|
||||
if(filename.endsWith('excalidraw') || filename.endsWith('excalidraw.md')) {
|
||||
return { filename, style, classname };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async renderExcalidraw(html: string) {
|
||||
try {
|
||||
const src = await this.getExcalidrawUrl(html);
|
||||
let svg = '';
|
||||
if (src === '') {
|
||||
svg = '渲染失败';
|
||||
console.log('Failed to get Excalidraw URL');
|
||||
}
|
||||
else {
|
||||
const blob = await this.readBlob(src);
|
||||
if (blob.type === 'image/svg+xml') {
|
||||
svg = await blob.text();
|
||||
}
|
||||
else {
|
||||
svg = '暂不支持' + blob.type;
|
||||
}
|
||||
}
|
||||
return svg;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return '渲染失败:' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
parseSVGLink(link: string) {
|
||||
let classname = 'note-embed-svg-left';
|
||||
const postions = new Map<string, string>([
|
||||
['left', 'note-embed-svg-left'],
|
||||
['center', 'note-embed-svg-center'],
|
||||
['right', 'note-embed-svg-right']
|
||||
])
|
||||
|
||||
let {filename, style, postion} = this.parseLinkStyle(link);
|
||||
classname = postions.get(postion) || classname;
|
||||
|
||||
return { filename, style, classname };
|
||||
}
|
||||
|
||||
async renderSVGFile(filename: string, id: string) {
|
||||
const file = this.assetsManager.searchFile(filename);
|
||||
|
||||
if (file == null) {
|
||||
const msg = '找不到文件:' + file;
|
||||
console.error(msg)
|
||||
return msg;
|
||||
}
|
||||
|
||||
const content = await this.getFileContent(file, null, null);
|
||||
LocalFile.fileCache.set(filename, content);
|
||||
return content;
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
async: true,
|
||||
walkTokens: async (token: Tokens.Generic) => {
|
||||
if (token.type !== 'LocalImage') {
|
||||
return;
|
||||
}
|
||||
// 渲染本地图片
|
||||
let item = this.parseImageLink(token.href);
|
||||
if (item) {
|
||||
const src = this.getImagePath(item.path);
|
||||
const width = item.width ? `width="${item.width}"` : '';
|
||||
const height = item.height? `height="${item.height}"` : '';
|
||||
token.html = `<img src="${src}" alt="${token.text}" ${width} ${height} />`;
|
||||
return;
|
||||
}
|
||||
|
||||
const info = this.parseExcalidrawLink(token.href);
|
||||
if (info) {
|
||||
if (!NMPSettings.getInstance().isAuthKeyVaild()) {
|
||||
token.html = "<span>请设置注册码</span>";
|
||||
return;
|
||||
}
|
||||
const id = this.generateId();
|
||||
this.callback.cacheElement('excalidraw', id, token.raw);
|
||||
token.html = `<span class="${info.classname}"><span class="note-embed-excalidraw" id="${id}" ${info.style}></span></span>`
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.href.endsWith('.svg') || token.href.includes('.svg|')) {
|
||||
const info = this.parseSVGLink(token.href);
|
||||
const id = this.generateId();
|
||||
let svg = '渲染中';
|
||||
if (LocalFile.fileCache.has(info.filename)) {
|
||||
svg = LocalFile.fileCache.get(info.filename) || '渲染失败';
|
||||
}
|
||||
else {
|
||||
svg = await this.renderSVGFile(info.filename, id) || '渲染失败';
|
||||
}
|
||||
token.html = `<span class="${info.classname}"><span class="note-embed-svg" id="${id}" ${info.style}>${svg}</span></span>`
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.generateId();
|
||||
const content = await this.renderFile(token.href, id);
|
||||
const tag = this.callback.settings.embedStyle === 'quote' ? 'blockquote' : 'section';
|
||||
token.html = `<${tag} class="note-embed-file" id="${id}">${content}</${tag}>`
|
||||
},
|
||||
|
||||
extensions:[{
|
||||
name: 'LocalImage',
|
||||
level: 'block',
|
||||
start: (src: string) => {
|
||||
const index = src.indexOf('![[');
|
||||
if (index === -1) return;
|
||||
return index;
|
||||
},
|
||||
tokenizer: (src: string) => {
|
||||
const matches = src.match(LocalFileRegex);
|
||||
if (matches == null) return;
|
||||
const token: Token = {
|
||||
type: 'LocalImage',
|
||||
raw: matches[0],
|
||||
href: matches[1],
|
||||
text: matches[1]
|
||||
};
|
||||
return token;
|
||||
},
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return token.html;
|
||||
}
|
||||
}]};
|
||||
}
|
||||
}
|
||||
208
src/markdown/math.ts
Normal file
208
src/markdown/math.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { MarkedExtension, Token, Tokens } from "marked";
|
||||
import { requestUrl } from "obsidian";
|
||||
import { Extension } from "./extension";
|
||||
import { NMPSettings } from "src/settings";
|
||||
|
||||
const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/;
|
||||
const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
|
||||
|
||||
const svgCache = new Map<string, string>();
|
||||
|
||||
export function cleanMathCache() {
|
||||
svgCache.clear();
|
||||
}
|
||||
|
||||
export class MathRendererQueue {
|
||||
private host = 'https://obplugin.sunboshi.tech';
|
||||
private static instance: MathRendererQueue;
|
||||
private mathIndex: number = 0;
|
||||
|
||||
// 静态方法,用于获取实例
|
||||
public static getInstance(): MathRendererQueue {
|
||||
if (!MathRendererQueue.instance) {
|
||||
MathRendererQueue.instance = new MathRendererQueue();
|
||||
}
|
||||
return MathRendererQueue.instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
async getMathSVG(expression: string, inline: boolean, type: string) {
|
||||
try {
|
||||
let success = false;
|
||||
let path = '';
|
||||
if (type === 'asciimath') {
|
||||
path = '/math/am';
|
||||
}
|
||||
else {
|
||||
path = '/math/tex';
|
||||
}
|
||||
|
||||
const url = `${this.host}${path}`;
|
||||
const res = await requestUrl({
|
||||
url,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
authkey: NMPSettings.getInstance().authKey
|
||||
},
|
||||
body: JSON.stringify({
|
||||
expression,
|
||||
inline
|
||||
})
|
||||
})
|
||||
let svg = ''
|
||||
if (res.status === 200) {
|
||||
svg = res.text;
|
||||
success = true;
|
||||
}
|
||||
else {
|
||||
console.error('render error: ' + res.json.msg)
|
||||
svg = '渲染失败: ' + res.json.msg;
|
||||
}
|
||||
return { svg, success };
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err.msg);
|
||||
const svg = '渲染失败: ' + err.message;
|
||||
return { svg, success: false };
|
||||
}
|
||||
}
|
||||
|
||||
generateId() {
|
||||
this.mathIndex += 1;
|
||||
return `math-id-${this.mathIndex}`;
|
||||
}
|
||||
|
||||
async render(token: Tokens.Generic, inline: boolean, type: string) {
|
||||
if (!NMPSettings.getInstance().isAuthKeyVaild()) {
|
||||
return '<span>注册码无效或已过期</span>';
|
||||
}
|
||||
|
||||
const id = this.generateId();
|
||||
let svg = '渲染中';
|
||||
const expression = token.text;
|
||||
if (svgCache.has(token.text)) {
|
||||
svg = svgCache.get(expression) as string;
|
||||
}
|
||||
else {
|
||||
const res = await this.getMathSVG(expression, inline, type)
|
||||
if (res.success) {
|
||||
svgCache.set(expression, res.svg);
|
||||
}
|
||||
svg = res.svg;
|
||||
}
|
||||
|
||||
const className = inline ? 'inline-math-svg' : 'block-math-svg';
|
||||
const body = inline ? svg : `<section class="block-math-section">${svg}</section>`;
|
||||
return `<span id="${id}" class="${className}">${body}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class MathRenderer extends Extension {
|
||||
async renderer(token: Tokens.Generic, inline: boolean, type: string = '') {
|
||||
if (type === '') {
|
||||
type = this.settings.math;
|
||||
}
|
||||
return await MathRendererQueue.getInstance().render(token, inline, type);
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
async: true,
|
||||
walkTokens: async (token: Tokens.Generic) => {
|
||||
if (token.type === 'InlineMath' || token.type === 'BlockMath') {
|
||||
token.html = await this.renderer(token, token.type === 'InlineMath', token.displayMode ? 'latex' : 'asciimath');
|
||||
}
|
||||
},
|
||||
extensions: [
|
||||
this.inlineMath(),
|
||||
this.blockMath()
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
inlineMath() {
|
||||
return {
|
||||
name: 'InlineMath',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
let index;
|
||||
let indexSrc = src;
|
||||
|
||||
while (indexSrc) {
|
||||
index = indexSrc.indexOf('$');
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const possibleKatex = indexSrc.substring(index);
|
||||
|
||||
if (possibleKatex.match(inlineRule)) {
|
||||
return index;
|
||||
}
|
||||
|
||||
indexSrc = indexSrc.substring(index + 1).replace(/^\$+/, '');
|
||||
}
|
||||
},
|
||||
tokenizer(src: string, tokens: Token[]) {
|
||||
const match = src.match(inlineRule);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'InlineMath',
|
||||
raw: match[0],
|
||||
text: match[2].trim(),
|
||||
displayMode: match[1].length === 2
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return token.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
blockMath() {
|
||||
return {
|
||||
name: 'BlockMath',
|
||||
level: 'block',
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(blockRule);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'BlockMath',
|
||||
raw: match[0],
|
||||
text: match[2].trim(),
|
||||
displayMode: match[1].length === 2
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return token.html;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
167
src/markdown/parser.ts
Normal file
167
src/markdown/parser.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Marked } from "marked";
|
||||
import { NMPSettings } from "src/settings";
|
||||
import { App, Vault } from "obsidian";
|
||||
import AssetsManager from "../assets";
|
||||
import { Extension, MDRendererCallback } from "./extension";
|
||||
import { Blockquote} from "./blockquote";
|
||||
import { CodeRenderer } from "./code";
|
||||
import { EmbedBlockMark } from "./embed-block-mark";
|
||||
import { SVGIcon } from "./icons";
|
||||
import { LinkRenderer } from "./link";
|
||||
import { LocalFile, LocalImageManager } from "./local-file";
|
||||
import { MathRenderer } from "./math";
|
||||
import { TextHighlight } from "./text-highlight";
|
||||
import { Comment } from "./commnet";
|
||||
import { Topic } from "./topic";
|
||||
import { HeadingRenderer } from "./heading";
|
||||
import { FootnoteRenderer } from "./footnote";
|
||||
import { EmptyLineRenderer } from "./empty-line";
|
||||
import { cleanUrl } from "../utils";
|
||||
|
||||
|
||||
const markedOptiones = {
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
};
|
||||
|
||||
const customRenderer = {
|
||||
hr(): string {
|
||||
return '<hr>';
|
||||
},
|
||||
list(body: string, ordered: boolean, start: number | ''): string {
|
||||
const type = ordered ? 'ol' : 'ul';
|
||||
const startatt = (ordered && start !== 1) ? (' start="' + start + '"') : '';
|
||||
return '<' + type + startatt + ' class="list-paddingleft-1">' + body + '</' + type + '>';
|
||||
},
|
||||
listitem(text: string, task: boolean, checked: boolean): string {
|
||||
return `<li><section><span data-leaf="">${text}<span></section></li>`;
|
||||
},
|
||||
image(href: string, title: string | null, text: string): string {
|
||||
const cleanHref = cleanUrl(href);
|
||||
if (cleanHref === null) {
|
||||
return text;
|
||||
}
|
||||
href = cleanHref;
|
||||
|
||||
if (!href.startsWith('http')) {
|
||||
const res = AssetsManager.getInstance().getResourcePath(decodeURI(href));
|
||||
if (res) {
|
||||
href = res.resUrl;
|
||||
const info = {
|
||||
resUrl: res.resUrl,
|
||||
filePath: res.filePath,
|
||||
media_id: null,
|
||||
url: null
|
||||
};
|
||||
LocalImageManager.getInstance().setImage(res.resUrl, info);
|
||||
}
|
||||
}
|
||||
let out = '';
|
||||
if (NMPSettings.getInstance().useFigcaption) {
|
||||
out = `<figure style="display: flex; flex-direction: column; align-items: center;"><img src="${href}" alt="${text}"`;
|
||||
if (title) {
|
||||
out += ` title="${title}"`;
|
||||
}
|
||||
if (text.length > 0) {
|
||||
out += `><figcaption>${text}</figcaption></figure>`;
|
||||
}
|
||||
else {
|
||||
out += '></figure>'
|
||||
}
|
||||
}
|
||||
else {
|
||||
out = `<img src="${href}" alt="${text}"`;
|
||||
if (title) {
|
||||
out += ` title="${title}"`;
|
||||
}
|
||||
out += '>';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
export class MarkedParser {
|
||||
extensions: Extension[] = [];
|
||||
marked: Marked;
|
||||
app: App;
|
||||
vault: Vault;
|
||||
|
||||
constructor(app: App, callback: MDRendererCallback) {
|
||||
this.app = app;
|
||||
this.vault = app.vault;
|
||||
|
||||
const settings = NMPSettings.getInstance();
|
||||
const assetsManager = AssetsManager.getInstance();
|
||||
|
||||
this.extensions.push(new LocalFile(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new Blockquote(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new EmbedBlockMark(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new SVGIcon(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new LinkRenderer(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new TextHighlight(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new CodeRenderer(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new Comment(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new Topic(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new HeadingRenderer(app, settings, assetsManager, callback));
|
||||
this.extensions.push(new FootnoteRenderer(app, settings, assetsManager, callback));
|
||||
if (settings.enableEmptyLine) {
|
||||
this.extensions.push(new EmptyLineRenderer(app, settings, assetsManager, callback));
|
||||
}
|
||||
if (settings.isAuthKeyVaild()) {
|
||||
this.extensions.push(new MathRenderer(app, settings, assetsManager, callback));
|
||||
}
|
||||
}
|
||||
|
||||
async buildMarked() {
|
||||
this.marked = new Marked();
|
||||
this.marked.use(markedOptiones);
|
||||
for (const ext of this.extensions) {
|
||||
this.marked.use(ext.markedExtension());
|
||||
ext.marked = this.marked;
|
||||
await ext.prepare();
|
||||
}
|
||||
this.marked.use({renderer: customRenderer});
|
||||
}
|
||||
|
||||
async prepare() {
|
||||
this.extensions.forEach(async ext => await ext.prepare());
|
||||
}
|
||||
|
||||
async postprocess(html: string) {
|
||||
let result = html;
|
||||
for (let ext of this.extensions) {
|
||||
result = await ext.postprocess(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async parse(content: string) {
|
||||
if (!this.marked) await this.buildMarked();
|
||||
await this.prepare();
|
||||
let html = await this.marked.parse(content);
|
||||
html = await this.postprocess(html);
|
||||
return html;
|
||||
}
|
||||
}
|
||||
66
src/markdown/text-highlight.ts
Normal file
66
src/markdown/text-highlight.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Token, Tokens, Lexer, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
|
||||
const highlightRegex = /^==(.*?)==/;
|
||||
|
||||
export class TextHighlight extends Extension {
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
extensions: [{
|
||||
name: 'InlineHighlight',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
let index;
|
||||
let indexSrc = src;
|
||||
|
||||
while (indexSrc) {
|
||||
index = indexSrc.indexOf('==');
|
||||
if (index === -1) return;
|
||||
return index;
|
||||
}
|
||||
},
|
||||
tokenizer(src: string, tokens: Token[]) {
|
||||
const match = src.match(highlightRegex);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'InlineHighlight',
|
||||
raw: match[0],
|
||||
text: match[1],
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: Tokens.Generic) {
|
||||
const lexer = new Lexer();
|
||||
const tokens = lexer.lex(token.text);
|
||||
// TODO: 优化一下
|
||||
let body = this.parser.parse(tokens)
|
||||
body = body.replace('<p>', '')
|
||||
body = body.replace('</p>', '')
|
||||
return `<span class="note-highlight">${body}</span>`;
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
61
src/markdown/topic.ts
Normal file
61
src/markdown/topic.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
|
||||
const topicRegex = /^#([^\s#]+)/;
|
||||
|
||||
export class Topic extends Extension {
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
extensions: [{
|
||||
name: 'Topic',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
let index;
|
||||
let indexSrc = src;
|
||||
|
||||
while (indexSrc) {
|
||||
index = indexSrc.indexOf('#');
|
||||
if (index === -1) return;
|
||||
return index;
|
||||
}
|
||||
},
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(topicRegex);
|
||||
if (match) {
|
||||
return {
|
||||
type: 'Topic',
|
||||
raw: match[0],
|
||||
text: match[1],
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: Tokens.Generic) {
|
||||
return `<a class="wx_topic_link" style="color: #576B95 !important;" data-topic="1">${'#' + token.text.trim()}</a>`;
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
179
src/markdown/widget-box.ts
Normal file
179
src/markdown/widget-box.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Sun Booshi
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Tokens, MarkedExtension } from "marked";
|
||||
import { Extension } from "./extension";
|
||||
import { NMPSettings } from "src/settings";
|
||||
import { uevent } from "src/utils";
|
||||
import { wxWidget } from "src/weixin-api";
|
||||
|
||||
const widgetCache = new Map<string, string>();
|
||||
|
||||
export function cleanWidgetCache() {
|
||||
widgetCache.clear();
|
||||
}
|
||||
|
||||
export class WidgetBox extends Extension {
|
||||
mapToString(map: Map<string, string>): string {
|
||||
if (map.size === 0) return "";
|
||||
|
||||
return Array.from(map.entries())
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("&"); // 用 "&" 连接键值对,可换成其他分隔符
|
||||
}
|
||||
|
||||
calcKey(id: string, title: string, style: Map<string, string>, content: string) {
|
||||
const styleStr = this.mapToString(style);
|
||||
const key = `${id}-${title}-${styleStr}-${content}`;
|
||||
return key;
|
||||
}
|
||||
|
||||
cacheWidget(id: string, title: string, style: Map<string, string>, content: string, result: string) {
|
||||
const key = this.calcKey(id, title, style, content);
|
||||
widgetCache.set(key, result);
|
||||
}
|
||||
|
||||
getWidget(id: string, title: string, style: Map<string, string>, content: string) {
|
||||
const key = this.calcKey(id, title, style, content);
|
||||
if (!widgetCache.has(key)) {
|
||||
return null;
|
||||
}
|
||||
return widgetCache.get(key);
|
||||
}
|
||||
|
||||
getBoxTitle(text: string) {
|
||||
let start = text.indexOf(']') + 1;
|
||||
let end = text.indexOf('\n');
|
||||
if (end === -1) end = text.length;
|
||||
if (start >= end) return '';
|
||||
return text.slice(start, end).trim();
|
||||
}
|
||||
|
||||
getBoxId(text: string) {
|
||||
const regex = /\[#(.*?)\]/g;
|
||||
let m;
|
||||
if( m = regex.exec(text)) {
|
||||
return m[1];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
matched(text: string) {
|
||||
return this.getBoxId(text) != "";
|
||||
}
|
||||
|
||||
parseStyle(text: string) {
|
||||
const style = text.split(':').map((s) => s.trim());
|
||||
if (style.length != 2) return null;
|
||||
const key = style[0];
|
||||
const value = style[1];
|
||||
return {key, value};
|
||||
}
|
||||
|
||||
parseBox(text: string) {
|
||||
const lines = text.split('\n');
|
||||
let style = new Map<string, string>();
|
||||
let content = [];
|
||||
let isStyle = false;
|
||||
for (let line of lines) {
|
||||
if (line === '===') {
|
||||
isStyle = !isStyle;
|
||||
continue;
|
||||
}
|
||||
if (isStyle) {
|
||||
const s = this.parseStyle(line);
|
||||
if (s) style.set(s.key, s.value);
|
||||
} else {
|
||||
content.push(line);
|
||||
}
|
||||
}
|
||||
const contentStr = content.join('\n');
|
||||
return { style, contentStr };
|
||||
}
|
||||
|
||||
async reqContent(id: string, title: string, style: Map<string, string>, content: string) {
|
||||
const params = JSON.stringify({
|
||||
id,
|
||||
title,
|
||||
style: Object.fromEntries(style),
|
||||
content
|
||||
});
|
||||
return wxWidget(NMPSettings.getInstance().authKey, params)
|
||||
}
|
||||
|
||||
processColor(style: Map<string, string>) {
|
||||
const keys = style.keys();
|
||||
for (let key of keys) {
|
||||
if (key.includes('color')) {
|
||||
const value = style.get(key);
|
||||
if (!value) continue;
|
||||
if (value.startsWith('rgb') || value.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
style.set(key, '#' + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async renderer(token: Tokens.Blockquote) {
|
||||
let boxId = this.getBoxId(token.text);
|
||||
if (boxId == '') {
|
||||
const body = this.marked.parser(token.tokens);
|
||||
return `<blockquote>${body}</blockquote>`;;
|
||||
}
|
||||
|
||||
const title = this.getBoxTitle(token.text);
|
||||
let style = new Map<string, string>();
|
||||
let content = '';
|
||||
const index = token.text.indexOf('\n');
|
||||
if (index > 0) {
|
||||
const pared = this.parseBox(token.text.slice(index + 1))
|
||||
style = pared.style;
|
||||
content = await this.marked.parse(pared.contentStr);
|
||||
}
|
||||
|
||||
this.processColor(style);
|
||||
|
||||
const cached = this.getWidget(boxId, title, style, content);
|
||||
if (cached) {
|
||||
uevent('render-widgets-cached');
|
||||
return cached;
|
||||
}
|
||||
else {
|
||||
const reqContent = await this.reqContent(boxId, title, style, content);
|
||||
this.cacheWidget(boxId, title, style, content, reqContent);
|
||||
uevent('render-widgets');
|
||||
return reqContent;
|
||||
}
|
||||
}
|
||||
|
||||
markedExtension(): MarkedExtension {
|
||||
return {
|
||||
extensions: [{
|
||||
name: 'blockquote',
|
||||
level: 'block',
|
||||
renderer: (token: Tokens.Generic) => {
|
||||
return token.html;
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user