update at 2025-09-22 13:55:41
This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
assets
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 sunbooshi
|
||||
|
||||
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.
|
||||
48
esbuild.config.mjs
Normal file
48
esbuild.config.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
BIN
images/20240630221748.jpg
Normal file
BIN
images/20240630221748.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
images/20240702203745.jpg
Normal file
BIN
images/20240702203745.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
BIN
images/20240728183041.png
Normal file
BIN
images/20240728183041.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
images/clipboard-paste.png
Normal file
BIN
images/clipboard-paste.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 409 B |
BIN
images/screenshot.png
Normal file
BIN
images/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
10
manifest.json
Normal file
10
manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "note-to-mp",
|
||||
"name": "NoteToMP",
|
||||
"version": "1.3.0",
|
||||
"minAppVersion": "1.4.5",
|
||||
"description": "Send notes to WeChat MP drafts, or copy notes to WeChat MP editor, perfect preservation of note styles, support code highlighting, line numbers in code, and support local image uploads.",
|
||||
"author": "Sun Booshi",
|
||||
"authorUrl": "https://sunboshi.tech",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
2278
package-lock.json
generated
Normal file
2278
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "note-to-mp",
|
||||
"version": "1.3.0",
|
||||
"description": "This is a plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"download": "node tools/download.mjs",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "^2.7.43",
|
||||
"highlight.js": "^11.9.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"marked": "^12.0.1",
|
||||
"marked-highlight": "^2.1.3"
|
||||
}
|
||||
}
|
||||
20
run.sh
Executable file
20
run.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# 自动添加所有修改
|
||||
git add .
|
||||
|
||||
# 如果没有提交信息,默认用时间戳
|
||||
msg="update at $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
# 支持自定义提交信息:./run.sh "your message"
|
||||
if [ $# -gt 0 ]; then
|
||||
msg="$*"
|
||||
fi
|
||||
|
||||
# 提交
|
||||
git commit -m "$msg"
|
||||
|
||||
# 推送到远程 main 分支
|
||||
git push origin main
|
||||
|
||||
630
src/article-render.ts
Normal file
630
src/article-render.ts
Normal file
@@ -0,0 +1,630 @@
|
||||
/*
|
||||
* 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 { App, ItemView, Workspace, Notice, sanitizeHTMLToDom, apiVersion, TFile, MarkdownRenderer, FrontMatterCache } from 'obsidian';
|
||||
import { applyCSS } from './utils';
|
||||
import { UploadImageToWx } from './imagelib';
|
||||
import { NMPSettings } from './settings';
|
||||
import AssetsManager from './assets';
|
||||
import InlineCSS from './inline-css';
|
||||
import { wxGetToken, wxAddDraft, wxBatchGetMaterial, DraftArticle, DraftImageMediaId, DraftImages, wxAddDraftImages } from './weixin-api';
|
||||
import { MDRendererCallback } from './markdown/extension';
|
||||
import { MarkedParser } from './markdown/parser';
|
||||
import { LocalImageManager, LocalFile } from './markdown/local-file';
|
||||
import { CardDataManager } from './markdown/code';
|
||||
import { debounce } from './utils';
|
||||
import { PrepareImageLib, IsImageLibReady, WebpToJPG } from './imagelib';
|
||||
import { toPng } from 'html-to-image';
|
||||
|
||||
|
||||
const FRONT_MATTER_REGEX = /^(---)$.+?^(---)$.+?/ims;
|
||||
|
||||
export class ArticleRender implements MDRendererCallback {
|
||||
app: App;
|
||||
itemView: ItemView;
|
||||
workspace: Workspace;
|
||||
styleEl: HTMLElement;
|
||||
articleDiv: HTMLDivElement;
|
||||
settings: NMPSettings;
|
||||
assetsManager: AssetsManager;
|
||||
articleHTML: string;
|
||||
title: string;
|
||||
_currentTheme: string;
|
||||
_currentHighlight: string;
|
||||
_currentAppId: string;
|
||||
markedParser: MarkedParser;
|
||||
cachedElements: Map<string, string> = new Map();
|
||||
debouncedRenderMarkdown: (...args: any[]) => void;
|
||||
|
||||
constructor(app: App, itemView: ItemView, styleEl: HTMLElement, articleDiv: HTMLDivElement) {
|
||||
this.app = app;
|
||||
this.itemView = itemView;
|
||||
this.styleEl = styleEl;
|
||||
this.articleDiv = articleDiv;
|
||||
this.settings = NMPSettings.getInstance();
|
||||
this.assetsManager = AssetsManager.getInstance();
|
||||
this.articleHTML = '';
|
||||
this.title = '';
|
||||
this._currentTheme = 'default';
|
||||
this._currentHighlight = 'default';
|
||||
this.markedParser = new MarkedParser(app, this);
|
||||
this.debouncedRenderMarkdown = debounce(this.renderMarkdown.bind(this), 1000);
|
||||
}
|
||||
|
||||
set currentTheme(value: string) {
|
||||
this._currentTheme = value;
|
||||
}
|
||||
|
||||
get currentTheme() {
|
||||
const { theme } = this.getMetadata();
|
||||
if (theme) {
|
||||
return theme;
|
||||
}
|
||||
return this._currentTheme;
|
||||
}
|
||||
|
||||
set currentHighlight(value: string) {
|
||||
this._currentHighlight = value;
|
||||
}
|
||||
|
||||
get currentHighlight() {
|
||||
const { highlight } = this.getMetadata();
|
||||
if (highlight) {
|
||||
return highlight;
|
||||
}
|
||||
return this._currentHighlight;
|
||||
}
|
||||
|
||||
isOldTheme() {
|
||||
const theme = this.assetsManager.getTheme(this.currentTheme);
|
||||
if (theme) {
|
||||
return theme.css.indexOf('.note-to-mp') < 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setArticle(article: string) {
|
||||
this.articleDiv.empty();
|
||||
let className = 'note-to-mp';
|
||||
// 兼容旧版本样式
|
||||
if (this.isOldTheme()) {
|
||||
className = this.currentTheme;
|
||||
}
|
||||
const html = `<section class="${className}" id="article-section">${article}</section>`;
|
||||
const doc = sanitizeHTMLToDom(html);
|
||||
if (doc.firstChild) {
|
||||
this.articleDiv.appendChild(doc.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
setStyle(css: string) {
|
||||
this.styleEl.empty();
|
||||
this.styleEl.appendChild(document.createTextNode(css));
|
||||
}
|
||||
|
||||
reloadStyle() {
|
||||
this.setStyle(this.getCSS());
|
||||
}
|
||||
|
||||
getArticleSection() {
|
||||
return this.articleDiv.querySelector('#article-section') as HTMLElement;
|
||||
}
|
||||
|
||||
getArticleContent() {
|
||||
const content = this.articleDiv.innerHTML;
|
||||
let html = applyCSS(content, this.getCSS());
|
||||
// 处理话题多余内容
|
||||
html = html.replace(/rel="noopener nofollow"/g, '');
|
||||
html = html.replace(/target="_blank"/g, '');
|
||||
html = html.replace(/data-leaf=""/g, 'leaf=""');
|
||||
return CardDataManager.getInstance().restoreCard(html);
|
||||
}
|
||||
|
||||
getArticleText() {
|
||||
return this.articleDiv.innerText.trimStart();
|
||||
}
|
||||
|
||||
errorContent(error: any) {
|
||||
return '<h1>渲染失败!</h1><br/>'
|
||||
+ '如需帮助请前往 <a href="https://github.com/sunbooshi/note-to-mp/issues">https://github.com/sunbooshi/note-to-mp/issues</a> 反馈<br/><br/>'
|
||||
+ '如果方便,请提供引发错误的完整Markdown内容。<br/><br/>'
|
||||
+ '<br/>Obsidian版本:' + apiVersion
|
||||
+ '<br/>错误信息:<br/>'
|
||||
+ `${error}`;
|
||||
}
|
||||
|
||||
async renderMarkdown(af: TFile | null = null) {
|
||||
try {
|
||||
let md = '';
|
||||
if (af && af.extension.toLocaleLowerCase() === 'md') {
|
||||
md = await this.app.vault.adapter.read(af.path);
|
||||
this.title = af.basename;
|
||||
}
|
||||
else {
|
||||
md = '没有可渲染的笔记或文件不支持渲染';
|
||||
}
|
||||
if (md.startsWith('---')) {
|
||||
md = md.replace(FRONT_MATTER_REGEX, '');
|
||||
}
|
||||
|
||||
this.articleHTML = await this.markedParser.parse(md);
|
||||
this.setStyle(this.getCSS());
|
||||
this.setArticle(this.articleHTML);
|
||||
await this.processCachedElements();
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
this.setArticle(this.errorContent(e));
|
||||
}
|
||||
}
|
||||
getCSS() {
|
||||
try {
|
||||
const theme = this.assetsManager.getTheme(this.currentTheme);
|
||||
const highlight = this.assetsManager.getHighlight(this.currentHighlight);
|
||||
const customCSS = this.settings.customCSSNote.length > 0 || this.settings.useCustomCss ? this.assetsManager.customCSS : '';
|
||||
const baseCSS = this.settings.baseCSS ? `.note-to-mp {${this.settings.baseCSS}}` : '';
|
||||
return `${InlineCSS}\n\n${highlight!.css}\n\n${theme!.css}\n\n${baseCSS}\n\n${customCSS}`;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
new Notice(`获取样式失败${this.currentTheme}|${this.currentHighlight},请检查主题是否正确安装。`);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
updateStyle(styleName: string) {
|
||||
this.currentTheme = styleName;
|
||||
this.setStyle(this.getCSS());
|
||||
}
|
||||
|
||||
updateHighLight(styleName: string) {
|
||||
this.currentHighlight = styleName;
|
||||
this.setStyle(this.getCSS());
|
||||
}
|
||||
|
||||
getFrontmatterValue(frontmatter: FrontMatterCache, key: string) {
|
||||
const value = frontmatter[key];
|
||||
|
||||
if (value instanceof Array) {
|
||||
return value[0];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
let res: DraftArticle = {
|
||||
title: '',
|
||||
author: undefined,
|
||||
digest: undefined,
|
||||
content: '',
|
||||
content_source_url: undefined,
|
||||
cover: undefined,
|
||||
thumb_media_id: '',
|
||||
need_open_comment: undefined,
|
||||
only_fans_can_comment: undefined,
|
||||
pic_crop_235_1: undefined,
|
||||
pic_crop_1_1: undefined,
|
||||
appid: undefined,
|
||||
theme: undefined,
|
||||
highlight: undefined,
|
||||
}
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return res;
|
||||
const metadata = this.app.metadataCache.getFileCache(file);
|
||||
if (metadata?.frontmatter) {
|
||||
const keys = this.assetsManager.expertSettings.frontmatter;
|
||||
const frontmatter = metadata.frontmatter;
|
||||
res.title = this.getFrontmatterValue(frontmatter, keys.title);
|
||||
res.author = this.getFrontmatterValue(frontmatter, keys.author);
|
||||
res.digest = this.getFrontmatterValue(frontmatter, keys.digest);
|
||||
res.content_source_url = this.getFrontmatterValue(frontmatter, keys.content_source_url);
|
||||
res.cover = this.getFrontmatterValue(frontmatter, keys.cover);
|
||||
res.thumb_media_id = this.getFrontmatterValue(frontmatter, keys.thumb_media_id);
|
||||
res.need_open_comment = frontmatter[keys.need_open_comment] ? 1 : undefined;
|
||||
res.only_fans_can_comment = frontmatter[keys.only_fans_can_comment] ? 1 : undefined;
|
||||
res.appid = this.getFrontmatterValue(frontmatter, keys.appid);
|
||||
if (res.appid && !res.appid.startsWith('wx')) {
|
||||
res.appid = this.settings.wxInfo.find(wx => wx.name === res.appid)?.appid;
|
||||
}
|
||||
res.theme = this.getFrontmatterValue(frontmatter, keys.theme);
|
||||
res.highlight = this.getFrontmatterValue(frontmatter, keys.highlight);
|
||||
if (frontmatter[keys.crop]) {
|
||||
res.pic_crop_235_1 = '0_0_1_0.5';
|
||||
res.pic_crop_1_1 = '0_0.525_0.404_1';
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async uploadVaultCover(name: string, token: string) {
|
||||
const LocalFileRegex = /^!\[\[(.*?)\]\]/;
|
||||
const matches = name.match(LocalFileRegex);
|
||||
let fileName = '';
|
||||
if (matches && matches.length > 1) {
|
||||
fileName = matches[1];
|
||||
}
|
||||
else {
|
||||
fileName = name;
|
||||
}
|
||||
const vault = this.app.vault;
|
||||
const file = this.assetsManager.searchFile(fileName) as TFile;
|
||||
if (!file) {
|
||||
throw new Error('找不到封面文件: ' + fileName);
|
||||
}
|
||||
const fileData = await vault.readBinary(file);
|
||||
|
||||
return await this.uploadCover(new Blob([fileData]), file.name, token);
|
||||
}
|
||||
|
||||
async uploadCover(data: Blob, filename: string, token: string) {
|
||||
if (filename.toLowerCase().endsWith('.webp')) {
|
||||
await PrepareImageLib();
|
||||
if (IsImageLibReady()) {
|
||||
data = new Blob([WebpToJPG(await data.arrayBuffer())]);
|
||||
filename = filename.toLowerCase().replace('.webp', '.jpg');
|
||||
}
|
||||
}
|
||||
|
||||
const res = await UploadImageToWx(data, filename, token, 'image');
|
||||
if (res.media_id) {
|
||||
return res.media_id;
|
||||
}
|
||||
console.error('upload cover fail: ' + res.errmsg);
|
||||
throw new Error('上传封面失败: ' + res.errmsg);
|
||||
}
|
||||
|
||||
async getDefaultCover(token: string) {
|
||||
const res = await wxBatchGetMaterial(token, 'image');
|
||||
if (res.item_count > 0) {
|
||||
return res.item[0].media_id;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async getToken(appid: string) {
|
||||
const secret = this.getSecret(appid);
|
||||
const res = await wxGetToken(this.settings.authKey, appid, secret);
|
||||
if (res.status != 200) {
|
||||
const data = res.json;
|
||||
throw new Error('获取token失败: ' + data.message);
|
||||
}
|
||||
const token = res.json.token;
|
||||
if (token === '') {
|
||||
throw new Error('获取token失败: ' + res.json.message);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async uploadImages(appid: string) {
|
||||
if (!this.settings.authKey) {
|
||||
throw new Error('请先设置注册码(AuthKey)');
|
||||
}
|
||||
|
||||
let metadata = this.getMetadata();
|
||||
if (metadata.appid) {
|
||||
appid = metadata.appid;
|
||||
}
|
||||
|
||||
if (!appid || appid.length == 0) {
|
||||
throw new Error('请先选择公众号');
|
||||
}
|
||||
|
||||
// 获取token
|
||||
const token = await this.getToken(appid);
|
||||
if (token === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.cachedElementsToImages();
|
||||
|
||||
const lm = LocalImageManager.getInstance();
|
||||
// 上传图片
|
||||
await lm.uploadLocalImage(token, this.app.vault);
|
||||
// 上传图床图片
|
||||
await lm.uploadRemoteImage(this.articleDiv, token);
|
||||
// 替换图片链接
|
||||
lm.replaceImages(this.articleDiv);
|
||||
|
||||
await this.copyArticle();
|
||||
}
|
||||
|
||||
async copyArticle() {
|
||||
const content = this.getArticleContent();
|
||||
await navigator.clipboard.write([new ClipboardItem({
|
||||
'text/html': new Blob([content], { type: 'text/html' })
|
||||
})])
|
||||
}
|
||||
|
||||
getSecret(appid: string) {
|
||||
for (const wx of this.settings.wxInfo) {
|
||||
if (wx.appid === appid) {
|
||||
return wx.secret.replace('SECRET', '');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async postArticle(appid:string, localCover: File | null = null) {
|
||||
if (!this.settings.authKey) {
|
||||
throw new Error('请先设置注册码(AuthKey)');
|
||||
}
|
||||
|
||||
let metadata = this.getMetadata();
|
||||
if (metadata.appid) {
|
||||
appid = metadata.appid;
|
||||
}
|
||||
|
||||
if (!appid || appid.length == 0) {
|
||||
throw new Error('请先选择公众号');
|
||||
}
|
||||
// 获取token
|
||||
const token = await this.getToken(appid);
|
||||
if (token === '') {
|
||||
throw new Error('获取token失败,请检查网络链接!');
|
||||
}
|
||||
await this.cachedElementsToImages();
|
||||
const lm = LocalImageManager.getInstance();
|
||||
// 上传图片
|
||||
await lm.uploadLocalImage(token, this.app.vault);
|
||||
// 上传图床图片
|
||||
await lm.uploadRemoteImage(this.articleDiv, token);
|
||||
// 替换图片链接
|
||||
lm.replaceImages(this.articleDiv);
|
||||
// 上传封面
|
||||
let mediaId = metadata.thumb_media_id;
|
||||
if (!mediaId) {
|
||||
if (metadata.cover) {
|
||||
// 上传仓库里的图片
|
||||
if (metadata.cover.startsWith('http')) {
|
||||
const res = await LocalImageManager.getInstance().uploadImageFromUrl(metadata.cover, token, 'image');
|
||||
if (res.media_id) {
|
||||
mediaId = res.media_id;
|
||||
}
|
||||
else {
|
||||
throw new Error('上传封面失败:' + res.errmsg);
|
||||
}
|
||||
}
|
||||
else {
|
||||
mediaId = await this.uploadVaultCover(metadata.cover, token);
|
||||
}
|
||||
}
|
||||
else if (localCover) {
|
||||
mediaId = await this.uploadCover(localCover, localCover.name, token);
|
||||
}
|
||||
else {
|
||||
mediaId = await this.getDefaultCover(token);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaId === '') {
|
||||
throw new Error('请先上传图片或者设置默认封面');
|
||||
}
|
||||
|
||||
metadata.title = metadata.title || this.title;
|
||||
metadata.content = this.getArticleContent();
|
||||
metadata.thumb_media_id = mediaId;
|
||||
|
||||
// 创建草稿
|
||||
const res = await wxAddDraft(token, metadata);
|
||||
|
||||
if (res.status != 200) {
|
||||
console.error(res.text);
|
||||
throw new Error(`创建草稿失败, https状态码: ${res.status} 可能是文章包含异常内容,请尝试手动复制到公众号编辑器!`);
|
||||
}
|
||||
|
||||
const draft = res.json;
|
||||
if (draft.media_id) {
|
||||
return draft.media_id;
|
||||
}
|
||||
else {
|
||||
console.error(JSON.stringify(draft));
|
||||
throw new Error('发布失败!' + draft.errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async postImages(appid: string) {
|
||||
if (!this.settings.authKey) {
|
||||
throw new Error('请先设置注册码(AuthKey)');
|
||||
}
|
||||
|
||||
let metadata = this.getMetadata();
|
||||
if (metadata.appid) {
|
||||
appid = metadata.appid;
|
||||
}
|
||||
|
||||
if (!appid || appid.length == 0) {
|
||||
throw new Error('请先选择公众号');
|
||||
}
|
||||
|
||||
// 获取token
|
||||
const token = await this.getToken(appid);
|
||||
if (token === '') {
|
||||
throw new Error('获取token失败,请检查网络链接!');
|
||||
}
|
||||
|
||||
const imageList: DraftImageMediaId[] = [];
|
||||
const lm = LocalImageManager.getInstance();
|
||||
// 上传图片
|
||||
await lm.uploadLocalImage(token, this.app.vault, 'image');
|
||||
// 上传图床图片
|
||||
await lm.uploadRemoteImage(this.articleDiv, token, 'image');
|
||||
|
||||
const images = lm.getImageInfos(this.articleDiv);
|
||||
for (const image of images) {
|
||||
if (!image.media_id) {
|
||||
console.warn('miss media id:', image.resUrl);
|
||||
continue;
|
||||
}
|
||||
imageList.push({
|
||||
image_media_id: image.media_id,
|
||||
});
|
||||
}
|
||||
|
||||
if (imageList.length === 0) {
|
||||
throw new Error('没有图片需要发布!');
|
||||
}
|
||||
|
||||
const content = this.getArticleText();
|
||||
|
||||
const imagesData: DraftImages = {
|
||||
article_type: 'newspic',
|
||||
title: metadata.title || this.title,
|
||||
content: content,
|
||||
need_open_commnet: metadata.need_open_comment || 0,
|
||||
only_fans_can_comment: metadata.only_fans_can_comment || 0,
|
||||
image_info: {
|
||||
image_list: imageList,
|
||||
}
|
||||
}
|
||||
// 创建草稿
|
||||
const res = await wxAddDraftImages(token, imagesData);
|
||||
|
||||
if (res.status != 200) {
|
||||
console.error(res.text);
|
||||
throw new Error(`创建图片/文字失败, https状态码: ${res.status} ${res.text}!`);
|
||||
}
|
||||
|
||||
const draft = res.json;
|
||||
if (draft.media_id) {
|
||||
return draft.media_id;
|
||||
}
|
||||
else {
|
||||
console.error(JSON.stringify(draft));
|
||||
throw new Error('发布失败!' + draft.errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async exportHTML() {
|
||||
await this.cachedElementsToImages();
|
||||
const lm = LocalImageManager.getInstance();
|
||||
const content = await lm.embleImages(this.articleDiv, this.app.vault);
|
||||
const globalStyle = await this.assetsManager.getStyle();
|
||||
const html = applyCSS(content, this.getCSS() + globalStyle);
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = this.title + '.html';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
}
|
||||
|
||||
async processCachedElements() {
|
||||
const af = this.app.workspace.getActiveFile();
|
||||
if (!af) {
|
||||
console.error('当前没有打开文件,无法处理缓存元素');
|
||||
return;
|
||||
}
|
||||
for (const [key, value] of this.cachedElements) {
|
||||
const [category, id] = key.split(':');
|
||||
if (category === 'mermaid' || category === 'excalidraw') {
|
||||
const container = this.articleDiv.querySelector('#' + id) as HTMLElement;
|
||||
if (container) {
|
||||
await MarkdownRenderer.render(this.app, value, container, af.path, this.itemView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cachedElementsToImages() {
|
||||
for (const [key, cached] of this.cachedElements) {
|
||||
const [category, elementId] = key.split(':');
|
||||
const container = this.articleDiv.querySelector(`#${elementId}`) as HTMLElement;
|
||||
if (!container) continue;
|
||||
|
||||
if (category === 'mermaid') {
|
||||
await this.replaceMermaidWithImage(container, elementId);
|
||||
} else if (category === 'excalidraw') {
|
||||
await this.replaceExcalidrawWithImage(container, elementId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async replaceMermaidWithImage(container: HTMLElement, id: string) {
|
||||
const mermaidContainer = container.querySelector('.mermaid') as HTMLElement;
|
||||
if (!mermaidContainer || !mermaidContainer.children.length) return;
|
||||
|
||||
const svg = mermaidContainer.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
try {
|
||||
const pngDataUrl = await toPng(mermaidContainer.firstElementChild as HTMLElement, { pixelRatio: 2 });
|
||||
const img = document.createElement('img');
|
||||
img.id = `img-${id}`;
|
||||
img.src = pngDataUrl;
|
||||
img.style.width = `${svg.clientWidth}px`;
|
||||
img.style.height = 'auto';
|
||||
|
||||
container.replaceChild(img, mermaidContainer);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to render Mermaid diagram: ${id}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async replaceExcalidrawWithImage(container: HTMLElement, id: string) {
|
||||
const innerDiv = container.querySelector('div') as HTMLElement;
|
||||
if (!innerDiv) return;
|
||||
|
||||
if (NMPSettings.getInstance().excalidrawToPNG) {
|
||||
const originalImg = container.querySelector('img') as HTMLImageElement;
|
||||
if (!originalImg) return;
|
||||
|
||||
const style = originalImg.getAttribute('style') || '';
|
||||
try {
|
||||
const pngDataUrl = await toPng(originalImg, { pixelRatio: 2 });
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.id = `img-${id}`;
|
||||
img.src = pngDataUrl;
|
||||
img.setAttribute('style', style);
|
||||
|
||||
container.replaceChild(img, container.firstChild!);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to render Excalidraw image: ${id}`, error);
|
||||
}
|
||||
} else {
|
||||
const svg = await LocalFile.renderExcalidraw(innerDiv.innerHTML);
|
||||
this.updateElementByID(id, svg);
|
||||
}
|
||||
}
|
||||
|
||||
updateElementByID(id: string, html: string): void {
|
||||
const item = this.articleDiv.querySelector('#' + id) as HTMLElement;
|
||||
if (!item) return;
|
||||
const doc = sanitizeHTMLToDom(html);
|
||||
item.empty();
|
||||
if (doc.childElementCount > 0) {
|
||||
for (const child of doc.children) {
|
||||
item.appendChild(child.cloneNode(true)); // 使用 cloneNode 复制节点以避免移动它
|
||||
}
|
||||
}
|
||||
else {
|
||||
item.innerText = '渲染失败';
|
||||
}
|
||||
}
|
||||
|
||||
cacheElement(category: string, id: string, data: string): void {
|
||||
const key = category + ':' + id;
|
||||
this.cachedElements.set(key, data);
|
||||
}
|
||||
}
|
||||
461
src/assets.ts
Normal file
461
src/assets.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
* 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 { App, PluginManifest, Notice, requestUrl, FileSystemAdapter, TAbstractFile, TFile, TFolder } from "obsidian";
|
||||
import * as zip from "@zip.js/zip.js";
|
||||
import DefaultTheme from "./default-theme";
|
||||
import DefaultHighlight from "./default-highlight";
|
||||
import { NMPSettings } from "./settings";
|
||||
import { ExpertSettings, defaultExpertSettings, expertSettingsFromString } from "./expert-settings";
|
||||
|
||||
|
||||
export interface Theme {
|
||||
name: string
|
||||
className: string
|
||||
desc: string
|
||||
author: string
|
||||
css: string
|
||||
}
|
||||
|
||||
export interface Highlight {
|
||||
name: string
|
||||
url: string
|
||||
css: string
|
||||
}
|
||||
|
||||
export default class AssetsManager {
|
||||
app: App;
|
||||
defaultTheme: Theme = DefaultTheme;
|
||||
manifest: PluginManifest;
|
||||
themes: Theme[];
|
||||
highlights: Highlight[];
|
||||
assetsPath: string;
|
||||
themesPath: string;
|
||||
hilightPath: string;
|
||||
customCSS: string = '';
|
||||
themeCfg: string;
|
||||
hilightCfg: string;
|
||||
customCSSPath: string;
|
||||
iconsPath: string;
|
||||
wasmPath: string;
|
||||
expertSettings: ExpertSettings;
|
||||
isLoaded: boolean = false;
|
||||
|
||||
private static instance: AssetsManager;
|
||||
|
||||
// 静态方法,用于获取实例
|
||||
public static getInstance(): AssetsManager {
|
||||
if (!AssetsManager.instance) {
|
||||
AssetsManager.instance = new AssetsManager();
|
||||
}
|
||||
return AssetsManager.instance;
|
||||
}
|
||||
|
||||
public static setup(app: App, manifest: PluginManifest) {
|
||||
AssetsManager.getInstance()._setup(app, manifest);
|
||||
}
|
||||
|
||||
private _setup(app: App, manifest: PluginManifest) {
|
||||
this.app = app;
|
||||
this.manifest = manifest;
|
||||
this.assetsPath = this.app.vault.configDir + '/plugins/' + this.manifest.id + '/assets/';
|
||||
this.themesPath = this.assetsPath + 'themes/';
|
||||
this.hilightPath = this.assetsPath + 'highlights/';
|
||||
this.themeCfg = this.assetsPath + 'themes.json';
|
||||
this.hilightCfg = this.assetsPath + 'highlights.json';
|
||||
this.customCSSPath = this.assetsPath + 'custom.css';
|
||||
this.iconsPath = this.assetsPath + 'icons/';
|
||||
this.wasmPath = this.assetsPath + 'lib.wasm';
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
|
||||
}
|
||||
|
||||
async loadAssets() {
|
||||
await this.loadThemes();
|
||||
await this.loadHighlights();
|
||||
await this.loadCustomCSS();
|
||||
await this.loadExpertSettings();
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
async loadThemes() {
|
||||
try {
|
||||
if (!await this.app.vault.adapter.exists(this.themeCfg)) {
|
||||
new Notice('主题资源未下载,请前往设置下载!');
|
||||
this.themes = [this.defaultTheme];
|
||||
return;
|
||||
}
|
||||
const data = await this.app.vault.adapter.read(this.themeCfg);
|
||||
if (data) {
|
||||
const themes = JSON.parse(data);
|
||||
await this.loadCSS(themes);
|
||||
this.themes = [this.defaultTheme, ... themes];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
new Notice('themes.json解析失败!');
|
||||
}
|
||||
}
|
||||
|
||||
async loadCSS(themes: Theme[]) {
|
||||
try {
|
||||
for (const theme of themes) {
|
||||
const cssFile = this.themesPath + theme.className + '.css';
|
||||
const cssContent = await this.app.vault.adapter.read(cssFile);
|
||||
if (cssContent) {
|
||||
theme.css = cssContent;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
new Notice('读取CSS失败!');
|
||||
}
|
||||
}
|
||||
|
||||
async loadCustomCSS() {
|
||||
try {
|
||||
const customCSSNote = NMPSettings.getInstance().customCSSNote;
|
||||
if (customCSSNote != '') {
|
||||
const file = this.searchFile(customCSSNote);
|
||||
if (file) {
|
||||
const cssContent = await this.app.vault.adapter.read(file.path);
|
||||
if (cssContent) {
|
||||
this.customCSS = cssContent.replace(/```css/gi, '').replace(/```/g, '');
|
||||
}
|
||||
}
|
||||
else {
|
||||
new Notice(customCSSNote + '自定义CSS文件不存在!');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await this.app.vault.adapter.exists(this.customCSSPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cssContent = await this.app.vault.adapter.read(this.customCSSPath);
|
||||
if (cssContent) {
|
||||
this.customCSS = cssContent;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
new Notice('读取CSS失败!');
|
||||
}
|
||||
}
|
||||
|
||||
async loadExpertSettings() {
|
||||
try {
|
||||
const note = NMPSettings.getInstance().expertSettingsNote;
|
||||
if (note != '') {
|
||||
const file = this.searchFile(note);
|
||||
if (file) {
|
||||
let content = await this.app.vault.adapter.read(file.path);
|
||||
if (content) {
|
||||
this.expertSettings = expertSettingsFromString(content);
|
||||
}
|
||||
else {
|
||||
this.expertSettings = defaultExpertSettings;
|
||||
new Notice(note + '专家设置文件内容为空!');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.expertSettings = defaultExpertSettings;
|
||||
new Notice(note + '专家设置不存在!');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.expertSettings = defaultExpertSettings;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
new Notice('读取专家设置失败!');
|
||||
}
|
||||
}
|
||||
|
||||
async loadHighlights() {
|
||||
try {
|
||||
const defaultHighlight = {name: '默认', url: '', css: DefaultHighlight};
|
||||
this.highlights = [defaultHighlight];
|
||||
if (!await this.app.vault.adapter.exists(this.hilightCfg)) {
|
||||
new Notice('高亮资源未下载,请前往设置下载!');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.app.vault.adapter.read(this.hilightCfg);
|
||||
if (data) {
|
||||
const items = JSON.parse(data);
|
||||
for (const item of items) {
|
||||
const cssFile = this.hilightPath + item.name + '.css';
|
||||
const cssContent = await this.app.vault.adapter.read(cssFile);
|
||||
this.highlights.push({name: item.name, url: item.url, css: cssContent});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
new Notice('highlights.json解析失败!');
|
||||
}
|
||||
}
|
||||
|
||||
async loadIcon(name: string) {
|
||||
const icon = this.iconsPath + name + '.svg';
|
||||
if (!await this.app.vault.adapter.exists(icon)) {
|
||||
return '';
|
||||
}
|
||||
const iconContent = await this.app.vault.adapter.read(icon);
|
||||
if (iconContent) {
|
||||
return iconContent;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async loadWasm() {
|
||||
if (!await this.app.vault.adapter.exists(this.wasmPath)) {
|
||||
return null;
|
||||
}
|
||||
const wasmContent = await this.app.vault.adapter.readBinary(this.wasmPath);
|
||||
if (wasmContent) {
|
||||
return wasmContent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getTheme(themeName: string) {
|
||||
if (themeName === '') {
|
||||
return this.themes[0];
|
||||
}
|
||||
|
||||
for (const theme of this.themes) {
|
||||
if (theme.name.toLowerCase() === themeName.toLowerCase() || theme.className.toLowerCase() === themeName.toLowerCase()) {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getHighlight(highlightName: string) {
|
||||
if (highlightName === '') {
|
||||
return this.highlights[0];
|
||||
}
|
||||
|
||||
for (const highlight of this.highlights) {
|
||||
if (highlight.name.toLowerCase() === highlightName.toLowerCase()) {
|
||||
return highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getThemeURL() {
|
||||
const version = this.manifest.version;
|
||||
return `https://github.com/sunbooshi/note-to-mp/releases/download/${version}/assets.zip`;
|
||||
}
|
||||
|
||||
async getStyle() {
|
||||
const file = this.app.vault.configDir + '/plugins/' + this.manifest.id + '/styles.css';
|
||||
if (!await this.app.vault.adapter.exists(file)) {
|
||||
return '';
|
||||
}
|
||||
const data = await this.app.vault.adapter.read(file);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async downloadThemes() {
|
||||
try {
|
||||
if (await this.app.vault.adapter.exists(this.themeCfg)) {
|
||||
new Notice('主题资源已存在!')
|
||||
return;
|
||||
}
|
||||
const res = await requestUrl(this.getThemeURL());
|
||||
const data = res.arrayBuffer;
|
||||
await this.unzip(new Blob([data]));
|
||||
await this.loadAssets();
|
||||
new Notice('主题下载完成!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await this.removeThemes();
|
||||
new Notice('主题下载失败, 请检查网络!');
|
||||
}
|
||||
}
|
||||
|
||||
async unzip(data:Blob) {
|
||||
const zipFileReader = new zip.BlobReader(data);
|
||||
const zipReader = new zip.ZipReader(zipFileReader);
|
||||
const entries = await zipReader.getEntries();
|
||||
|
||||
if (!await this.app.vault.adapter.exists(this.assetsPath)) {
|
||||
await this.app.vault.adapter.mkdir(this.assetsPath);
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.directory) {
|
||||
const dirPath = this.assetsPath + entry.filename;
|
||||
await this.app.vault.adapter.mkdir(dirPath);
|
||||
}
|
||||
else {
|
||||
const filePath = this.assetsPath + entry.filename;
|
||||
const blobWriter = new zip.Uint8ArrayWriter();
|
||||
if (entry.getData) {
|
||||
const data = await entry.getData(blobWriter);
|
||||
await this.app.vault.adapter.writeBinary(filePath, data.buffer as ArrayBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await zipReader.close();
|
||||
}
|
||||
|
||||
async removeThemes() {
|
||||
try {
|
||||
const adapter = this.app.vault.adapter;
|
||||
if (await adapter.exists(this.themeCfg)) {
|
||||
await adapter.remove(this.themeCfg);
|
||||
}
|
||||
if (await adapter.exists(this.hilightCfg)) {
|
||||
await adapter.remove(this.hilightCfg);
|
||||
}
|
||||
if (await adapter.exists(this.themesPath)) {
|
||||
await adapter.rmdir(this.themesPath, true);
|
||||
}
|
||||
if (await adapter.exists(this.hilightPath)) {
|
||||
await adapter.rmdir(this.hilightPath, true);
|
||||
}
|
||||
await this.loadAssets();
|
||||
new Notice('清空完成!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
new Notice('清空主题失败!');
|
||||
}
|
||||
}
|
||||
|
||||
async openAssets() {
|
||||
const path = require('path');
|
||||
const adapter = this.app.vault.adapter as FileSystemAdapter;
|
||||
const vaultRoot = adapter.getBasePath();
|
||||
const assets = this.assetsPath;
|
||||
if (!await adapter.exists(assets)) {
|
||||
await adapter.mkdir(assets);
|
||||
}
|
||||
const dst = path.join(vaultRoot, assets);
|
||||
const { shell } = require('electron');
|
||||
shell.openPath(dst);
|
||||
}
|
||||
|
||||
searchFile(nameOrPath: string): TAbstractFile | null {
|
||||
const resolvedPath = this.resolvePath(nameOrPath);
|
||||
const vault= this.app.vault;
|
||||
const attachmentFolderPath = vault.config.attachmentFolderPath || '';
|
||||
let localPath = resolvedPath;
|
||||
let file = null;
|
||||
|
||||
// 先按路径查找
|
||||
file = vault.getFileByPath(resolvedPath);
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
// 在根目录查找
|
||||
file = vault.getFileByPath(nameOrPath);
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
// 从附件文件夹查找
|
||||
if (attachmentFolderPath != '') {
|
||||
localPath = attachmentFolderPath + '/' + nameOrPath;
|
||||
file = vault.getFileByPath(localPath)
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
localPath = attachmentFolderPath + '/' + resolvedPath;
|
||||
file = vault.getFileByPath(localPath)
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
// 最后查找所有文件,这里只需要判断文件名
|
||||
const files = vault.getAllLoadedFiles();
|
||||
for (let f of files) {
|
||||
if (f instanceof TFolder) continue
|
||||
file = f as TFile;
|
||||
if (file.basename === nameOrPath || file.name === nameOrPath) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getResourcePath(path: string): {resUrl:string, filePath:string}|null {
|
||||
const file = this.searchFile(path) as TFile;
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
const resUrl = this.app.vault.getResourcePath(file);
|
||||
return {resUrl, filePath: file.path};
|
||||
}
|
||||
|
||||
resolvePath(relativePath: string): string {
|
||||
const basePath = this.getActiveFileDir();
|
||||
if (!relativePath.includes('/')) {
|
||||
return relativePath;
|
||||
}
|
||||
const stack = basePath.split("/");
|
||||
const parts = relativePath.split("/");
|
||||
|
||||
stack.pop(); // Remove the current file name (or empty string)
|
||||
|
||||
for (const part of parts) {
|
||||
if (part === ".") continue;
|
||||
if (part === "..") stack.pop();
|
||||
else stack.push(part);
|
||||
}
|
||||
return stack.join("/");
|
||||
}
|
||||
|
||||
getActiveFileDir() {
|
||||
const af = this.app.workspace.getActiveFile();
|
||||
if (af == null) {
|
||||
return '';
|
||||
}
|
||||
const parts = af.path.split('/');
|
||||
parts.pop();
|
||||
if (parts.length == 0) {
|
||||
return '';
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
async readFileBinary(path: string) {
|
||||
const vault= this.app.vault;
|
||||
const file = this.searchFile(path) as TFile;
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
return await vault.readBinary(file);
|
||||
}
|
||||
}
|
||||
109
src/default-highlight.ts
Normal file
109
src/default-highlight.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export default `
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
}
|
||||
/*
|
||||
|
||||
XCode style (c) Angel Garcia <angelgarcia.mail@gmail.com>
|
||||
|
||||
*/
|
||||
.hljs {
|
||||
background: #fff;
|
||||
color: black
|
||||
}
|
||||
/* Gray DOCTYPE selectors like WebKit */
|
||||
.xml .hljs-meta {
|
||||
color: #c0c0c0
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #007400
|
||||
}
|
||||
.hljs-tag,
|
||||
.hljs-attribute,
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-literal,
|
||||
.hljs-name {
|
||||
color: #aa0d91
|
||||
}
|
||||
.hljs-variable,
|
||||
.hljs-template-variable {
|
||||
color: #3F6E74
|
||||
}
|
||||
.hljs-code,
|
||||
.hljs-string,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #c41a16
|
||||
}
|
||||
.hljs-regexp,
|
||||
.hljs-link {
|
||||
color: #0E0EFF
|
||||
}
|
||||
.hljs-title,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-number {
|
||||
color: #1c00cf
|
||||
}
|
||||
.hljs-section,
|
||||
.hljs-meta {
|
||||
color: #643820
|
||||
}
|
||||
.hljs-title.class_,
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-type,
|
||||
.hljs-built_in,
|
||||
.hljs-params {
|
||||
color: #5c2699
|
||||
}
|
||||
.hljs-attr {
|
||||
color: #836C28
|
||||
}
|
||||
.hljs-subst {
|
||||
color: #000
|
||||
}
|
||||
.hljs-formula {
|
||||
background-color: #eee;
|
||||
font-style: italic
|
||||
}
|
||||
.hljs-addition {
|
||||
background-color: #baeeba
|
||||
}
|
||||
.hljs-deletion {
|
||||
background-color: #ffc8bd
|
||||
}
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #9b703f
|
||||
}
|
||||
.hljs-doctag,
|
||||
.hljs-strong {
|
||||
font-weight: bold
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic
|
||||
}
|
||||
`;
|
||||
349
src/default-theme.ts
Normal file
349
src/default-theme.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const css = `
|
||||
/* =========================================================== */
|
||||
/* Obsidian的默认样式 */
|
||||
/* =========================================================== */
|
||||
.note-to-mp {
|
||||
padding: 0;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
color: #222222;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.note-to-mp:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.note-to-mp .fancybox-img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.note-to-mp .fancybox-img:hover {
|
||||
opacity: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Heading
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp h1 {
|
||||
color: #222;
|
||||
font-weight: 700;
|
||||
font-size: 1.802em;
|
||||
line-height: 1.2;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.note-to-mp h2 {
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
font-size: 1.602em;
|
||||
line-height: 1.2;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.note-to-mp h3 {
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
font-size: 1.424em;
|
||||
line-height: 1.3;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.note-to-mp h4 {
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
font-size: 1.266em;
|
||||
line-height: 1.4;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.note-to-mp h5 {
|
||||
color: #222;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.note-to-mp h6 {
|
||||
color: #222;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Horizontal Rules
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp hr {
|
||||
border-color: #e0e0e0;
|
||||
margin-top: 3em;
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Paragraphs
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp p {
|
||||
line-height: 1.6em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Emphasis
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp strong {
|
||||
color: #222222;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.note-to-mp em {
|
||||
color: inherit;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.note-to-mp s {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Blockquotes
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp blockquote {
|
||||
font-size: 1rem;
|
||||
display: block;
|
||||
margin: 2em 0;
|
||||
padding: 0em 0.8em 0em 0.8em;
|
||||
position: relative;
|
||||
color: inherit;
|
||||
border-left: 0.15rem solid #7852ee;
|
||||
}
|
||||
|
||||
.note-to-mp blockquote blockquote {
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
.note-to-mp blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note-to-mp blockquote footer strong {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
List
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp ul {
|
||||
margin: 0;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.note-to-mp ul>li::marker {
|
||||
color: #ababab;
|
||||
/* font-size: 1.5em; */
|
||||
}
|
||||
|
||||
.note-to-mp li>p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note-to-mp ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 0em;
|
||||
list-style-type: decimal;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.note-to-mp ol>li {
|
||||
position: relative;
|
||||
padding-left: 0.1em;
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Link
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp a {
|
||||
color: #7852ee;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #7852ee;
|
||||
transition: border 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.note-to-mp a:hover {
|
||||
color: #7952eebb;
|
||||
border-bottom: 1px solid #7952eebb;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Table
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp table {
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
text-align: left;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.7142857;
|
||||
border-collapse: collapse;
|
||||
border-color: inherit;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.note-to-mp table thead {
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
border: #e0e0e0 1px solid;
|
||||
}
|
||||
|
||||
.note-to-mp table thead th {
|
||||
vertical-align: bottom;
|
||||
padding-right: 0.5714286em;
|
||||
padding-bottom: 0.5714286em;
|
||||
padding-left: 0.5714286em;
|
||||
border: #e0e0e0 1px solid;
|
||||
}
|
||||
|
||||
.note-to-mp table thead th:first-child {
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.note-to-mp table thead th:last-child {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.note-to-mp table tbody tr {
|
||||
border-style: solid;
|
||||
border: #e0e0e0 1px solid;
|
||||
}
|
||||
|
||||
.note-to-mp table tbody tr:last-child {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.note-to-mp table tbody td {
|
||||
vertical-align: top;
|
||||
padding-top: 0.5714286em;
|
||||
padding-right: 0.5714286em;
|
||||
padding-bottom: 0.5714286em;
|
||||
padding-left: 0.5714286em;
|
||||
border: #e0e0e0 1px solid;
|
||||
}
|
||||
|
||||
.note-to-mp table tbody td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.note-to-mp table tbody td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Images
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp img {
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
.note-to-mp .footnotes hr {
|
||||
margin-top: 4em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================
|
||||
Code
|
||||
==================================
|
||||
*/
|
||||
.note-to-mp .code-section {
|
||||
display: flex;
|
||||
border: rgb(240, 240, 240) 1px solid;
|
||||
line-height: 26px;
|
||||
font-size: 14px;
|
||||
margin: 1em 0;
|
||||
padding: 0.875em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.note-to-mp .code-section ul {
|
||||
width: fit-content;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
line-height: 26px;
|
||||
list-style-type: none;
|
||||
backgroud: transparent !important;
|
||||
}
|
||||
|
||||
.note-to-mp .code-section ul>li {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.note-to-mp .code-section pre {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
white-space: normal;
|
||||
overflow: auto;
|
||||
padding: 0 0 0 0.875em;
|
||||
}
|
||||
|
||||
.note-to-mp .code-section code {
|
||||
display: flex;
|
||||
text-wrap: nowrap;
|
||||
font-family: Consolas,Courier,monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default {name: '默认', className: 'obsidian-light', desc: '默认主题', author: 'SunBooshi', css:css};
|
||||
66
src/doc-modal.ts
Normal file
66
src/doc-modal.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 { App, Modal, sanitizeHTMLToDom } from "obsidian";
|
||||
|
||||
export class DocModal extends Modal {
|
||||
url: string = '';
|
||||
title: string = '提示';
|
||||
content: string = '';
|
||||
|
||||
constructor(app: App, title: string = "提示", content: string = "", url: string = "") {
|
||||
super(app);
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
let { contentEl, modalEl } = this;
|
||||
modalEl.style.width = '640px';
|
||||
modalEl.style.height = '720px';
|
||||
contentEl.style.display = 'flex';
|
||||
contentEl.style.flexDirection = 'column';
|
||||
|
||||
const titleEl = contentEl.createEl('h2', { text: this.title });
|
||||
titleEl.style.marginTop = '0.5em';
|
||||
const content = contentEl.createEl('div');
|
||||
content.setAttr('style', 'margin-bottom:1em;-webkit-user-select: text; user-select: text;');
|
||||
content.appendChild(sanitizeHTMLToDom(this.content));
|
||||
|
||||
const iframe = contentEl.createEl('iframe', {
|
||||
attr: {
|
||||
src: this.url,
|
||||
width: '100%',
|
||||
allow: 'clipboard-read; clipboard-write',
|
||||
},
|
||||
});
|
||||
|
||||
iframe.style.flex = '1';
|
||||
}
|
||||
|
||||
onClose() {
|
||||
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
80
src/expert-settings.ts
Normal file
80
src/expert-settings.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 { parseYaml } from "obsidian";
|
||||
|
||||
export interface ExpertSettings {
|
||||
render?: {
|
||||
h1?: string | number | object;
|
||||
h2?: string | number | object;
|
||||
h3?: string | number | object;
|
||||
code?: number;
|
||||
callout?: object | undefined;
|
||||
},
|
||||
frontmatter: {
|
||||
title: string;
|
||||
author: string;
|
||||
digest: string;
|
||||
content_source_url: string;
|
||||
cover: string;
|
||||
thumb_media_id: string
|
||||
need_open_comment: string;
|
||||
only_fans_can_comment: string;
|
||||
appid: string;
|
||||
theme: string;
|
||||
highlight: string;
|
||||
crop: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultExpertSettings: ExpertSettings = {
|
||||
render: undefined,
|
||||
frontmatter: {
|
||||
title: '标题',
|
||||
author: '作者',
|
||||
digest: '摘要',
|
||||
content_source_url: '原文地址',
|
||||
cover: '封面',
|
||||
thumb_media_id: '封面素材ID',
|
||||
need_open_comment: '打开评论',
|
||||
only_fans_can_comment: '仅粉丝可评论',
|
||||
appid: '公众号',
|
||||
theme: '样式',
|
||||
highlight: '代码高亮',
|
||||
crop: '封面裁剪',
|
||||
}
|
||||
};
|
||||
|
||||
export function expertSettingsFromString(content: string): ExpertSettings {
|
||||
content = content.replace(/```yaml/gi, '').replace(/```/g, '');
|
||||
let parsed = parseYaml(content) as Partial<ExpertSettings>;
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
parsed = {};
|
||||
}
|
||||
return {
|
||||
render: parsed.render,
|
||||
frontmatter: {
|
||||
...defaultExpertSettings.frontmatter,
|
||||
...(parsed.frontmatter || {})
|
||||
}
|
||||
};
|
||||
}
|
||||
47
src/gallery/index.ts
Normal file
47
src/gallery/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// [note-to-mp 重构] Gallery 模块
|
||||
import { App } from 'obsidian';
|
||||
|
||||
export interface GalleryTransformResult {
|
||||
content: string;
|
||||
replaced: boolean;
|
||||
}
|
||||
|
||||
// 单行 self-closing 形式: {{<gallery dir="/img/foo" figcaption="说明"/>}}{{<load-photoswipe>}}
|
||||
const GALLERY_INLINE_RE = /{{<gallery\s+dir="([^"]+)"(?:\s+figcaption="([^"]*)")?\s*\/?>}}\s*{{<load-photoswipe>}}/g;
|
||||
// 块级形式
|
||||
const GALLERY_BLOCK_RE = /{{<gallery>}}([\s\S]*?){{<\/gallery>}}/g;
|
||||
const FIGURE_RE = /{{<figure\s+src="([^"]+)"[^>]*>}}/g;
|
||||
|
||||
export function transformGalleryShortcodes(raw: string): GalleryTransformResult {
|
||||
let replaced = false;
|
||||
// 处理块级
|
||||
raw = raw.replace(GALLERY_BLOCK_RE, (_m, inner) => {
|
||||
const imgs: string[] = [];
|
||||
let fm: RegExpExecArray | null;
|
||||
while ((fm = FIGURE_RE.exec(inner)) !== null) {
|
||||
const src = fm[1];
|
||||
const base = src.split(/[?#]/)[0].split('/').pop();
|
||||
if (base) imgs.push(`![[${base}]]`);
|
||||
}
|
||||
if (imgs.length === 0) return _m; // 保留原文本
|
||||
replaced = true;
|
||||
return imgs.join('\n') + '\n';
|
||||
});
|
||||
|
||||
// 处理单行自闭合形式
|
||||
raw = raw.replace(GALLERY_INLINE_RE, (_m, dir, figcaption) => {
|
||||
replaced = true;
|
||||
const comment = figcaption ? `<!-- gallery: ${figcaption} -->\n` : '';
|
||||
// 暂不实际列目录;由后续 selectGalleryImages 扩展
|
||||
return comment + `<!-- gallery dir=${dir} -->`;
|
||||
});
|
||||
|
||||
return { content: raw, replaced };
|
||||
}
|
||||
|
||||
// 占位:真实实现可遍历 vault 目录
|
||||
export async function selectGalleryImages(app: App, dir: string, options?: { limit?: number }): Promise<string[]> {
|
||||
// TODO: 遍历 app.vault.getAbstractFileByPath(dir)
|
||||
// 返回文件名数组(不含路径)
|
||||
return [];
|
||||
}
|
||||
64
src/image/index.ts
Normal file
64
src/image/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// [note-to-mp 重构] 图片处理模块
|
||||
// 负责统一解析 wikilink 与 markdown 图片,并提供集中管理
|
||||
|
||||
export interface LocalImage {
|
||||
original: string; // 原始匹配串(包括语法标记)
|
||||
basename: string; // 文件基本名(不含路径)
|
||||
alt?: string; // alt 描述(若来自 markdown 语法)
|
||||
sourceType: 'wikilink' | 'markdown';
|
||||
index: number; // 在原文中的出现顺序
|
||||
}
|
||||
|
||||
export const LocalFileRegex = /^(?:!\[\[(.*?)\]\]|!\[[^\]]*\]\(([^\n\r\)]+)\))/;
|
||||
|
||||
export class LocalImageManager {
|
||||
private images: LocalImage[] = [];
|
||||
private byBasename: Map<string, LocalImage[]> = new Map();
|
||||
|
||||
add(image: LocalImage) {
|
||||
this.images.push(image);
|
||||
const list = this.byBasename.get(image.basename) || [];
|
||||
list.push(image);
|
||||
this.byBasename.set(image.basename, list);
|
||||
}
|
||||
|
||||
all(): LocalImage[] { return this.images.slice(); }
|
||||
|
||||
first(): LocalImage | undefined { return this.images[0]; }
|
||||
|
||||
findByBasename(name: string): LocalImage | undefined {
|
||||
const list = this.byBasename.get(name);
|
||||
return list && list[0];
|
||||
}
|
||||
|
||||
clear() { this.images = []; this.byBasename.clear(); }
|
||||
}
|
||||
|
||||
export function parseImagesFromMarkdown(markdown: string): LocalImage[] {
|
||||
// 扫描整篇,统一抽取,不做替换
|
||||
const result: LocalImage[] = [];
|
||||
const wikilinkRe = /!\[\[(.+?)\]\]/g; // 非贪婪
|
||||
const mdImgRe = /!\[([^\]]*)\]\(([^\n\r\)]+)\)/g;
|
||||
let index = 0;
|
||||
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = wikilinkRe.exec(markdown)) !== null) {
|
||||
const full = m[0];
|
||||
const inner = m[1].trim();
|
||||
const basename = inner.split('/').pop() || inner;
|
||||
result.push({ original: full, basename, sourceType: 'wikilink', index: index++ });
|
||||
}
|
||||
while ((m = mdImgRe.exec(markdown)) !== null) {
|
||||
const full = m[0];
|
||||
const alt = m[1].trim();
|
||||
const link = m[2].trim();
|
||||
const basename = link.split(/[?#]/)[0].split('/').pop() || link;
|
||||
result.push({ original: full, basename, alt, sourceType: 'markdown', index: index++ });
|
||||
}
|
||||
|
||||
// 按出现顺序(两个正则独立扫描会破坏顺序,重新排序 by 原始位置)
|
||||
result.sort((a, b) => markdown.indexOf(a.original) - markdown.indexOf(b.original));
|
||||
// 重排 index
|
||||
result.forEach((r, i) => r.index = i);
|
||||
return result;
|
||||
}
|
||||
68
src/imagelib.ts
Normal file
68
src/imagelib.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 { getBlobArrayBuffer } from "obsidian";
|
||||
import { wxUploadImage } from "./weixin-api";
|
||||
import { NMPSettings } from "./settings";
|
||||
import { IsWasmReady, LoadWasm } from "./wasm/wasm";
|
||||
import AssetsManager from "./assets";
|
||||
|
||||
declare function GoWebpToJPG(data: Uint8Array): Uint8Array;
|
||||
declare function GoWebpToPNG(data: Uint8Array): Uint8Array;
|
||||
declare function GoAddWatermark(img: Uint8Array, watermark: Uint8Array): Uint8Array;
|
||||
|
||||
export function IsImageLibReady() {
|
||||
return IsWasmReady();
|
||||
}
|
||||
|
||||
export async function PrepareImageLib() {
|
||||
await LoadWasm();
|
||||
}
|
||||
|
||||
export function WebpToJPG(data: ArrayBuffer): ArrayBuffer {
|
||||
return GoWebpToJPG(new Uint8Array(data));
|
||||
}
|
||||
|
||||
export function WebpToPNG(data: ArrayBuffer): ArrayBuffer {
|
||||
return GoWebpToPNG(new Uint8Array(data));
|
||||
}
|
||||
|
||||
export function AddWatermark(img: ArrayBuffer, watermark: ArrayBuffer): ArrayBuffer {
|
||||
return GoAddWatermark(new Uint8Array(img), new Uint8Array(watermark));
|
||||
}
|
||||
|
||||
export async function UploadImageToWx(data: Blob, filename: string, token: string, type?: string) {
|
||||
if (!IsImageLibReady()) {
|
||||
await PrepareImageLib();
|
||||
}
|
||||
|
||||
const watermark = NMPSettings.getInstance().watermark;
|
||||
if (watermark != null && watermark != '') {
|
||||
const watermarkData = await AssetsManager.getInstance().readFileBinary(watermark);
|
||||
if (watermarkData == null) {
|
||||
throw new Error('水印图片不存在: ' + watermark);
|
||||
}
|
||||
const watermarkImg = AddWatermark(await data.arrayBuffer(), watermarkData);
|
||||
data = new Blob([watermarkImg], { type: data.type });
|
||||
}
|
||||
return await wxUploadImage(data, filename, token, type);
|
||||
}
|
||||
207
src/inline-css.ts
Normal file
207
src/inline-css.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// 需要渲染进inline style的css样式
|
||||
export default `
|
||||
/* --------------------------------------- */
|
||||
/* callout */
|
||||
/* --------------------------------------- */
|
||||
section .note-callout {
|
||||
border: none;
|
||||
padding: 1em 1em 1em 1.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1em 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
section .note-callout-title-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.note-callout-icon {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.note-callout-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
section .note-callout-title {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
section .note-callout-content {
|
||||
color: rgb(34,34,34);
|
||||
}
|
||||
|
||||
/* note info todo */
|
||||
section .note-callout-note {
|
||||
color: rgb(8, 109, 221);
|
||||
background-color: rgba(8, 109, 221, 0.1);
|
||||
}
|
||||
/* abstract tip hint */
|
||||
section .note-callout-abstract {
|
||||
color: rgb(0, 191, 188);
|
||||
background-color: rgba(0, 191, 188, 0.1);
|
||||
}
|
||||
section .note-callout-success {
|
||||
color: rgb(8, 185, 78);
|
||||
background-color: rgba(8, 185, 78, 0.1);
|
||||
}
|
||||
/* question help, faq, warning, caution, attention */
|
||||
section .note-callout-question {
|
||||
color: rgb(236, 117, 0);
|
||||
background-color: rgba(236, 117, 0, 0.1);
|
||||
}
|
||||
/* failure, fail, missing, danger, error, bug */
|
||||
section .note-callout-failure {
|
||||
color: rgb(233, 49, 71);
|
||||
background-color: rgba(233, 49, 71, 0.1);
|
||||
}
|
||||
section .note-callout-example {
|
||||
color: rgb(120, 82, 238);
|
||||
background-color: rgba(120, 82, 238, 0.1);
|
||||
}
|
||||
section .note-callout-quote {
|
||||
color: rgb(158, 158, 158);
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
}
|
||||
/* custom icon callout */
|
||||
section .note-callout-custom {
|
||||
color: rgb(8, 109, 221);
|
||||
background-color: rgba(8, 109, 221, 0.1);
|
||||
}
|
||||
|
||||
/* --------------------------------------- */
|
||||
/* math */
|
||||
/* --------------------------------------- */
|
||||
.block-math-svg {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin:20px 0px;
|
||||
max-width: 300% !important;
|
||||
}
|
||||
|
||||
.block-math-section {
|
||||
text-align: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* --------------------------------------- */
|
||||
/* 高亮 */
|
||||
/* --------------------------------------- */
|
||||
.note-highlight {
|
||||
background-color: rgba(255,208,0, 0.4);
|
||||
}
|
||||
|
||||
/* --------------------------------------- */
|
||||
/* 列表需要强制设置样式*/
|
||||
/* --------------------------------------- */
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.note-svg-icon {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note-svg-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-embed-excalidraw-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-embed-excalidraw-center {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-embed-excalidraw-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-embed-excalidraw {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note-embed-excalidraw p {
|
||||
line-height: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/*
|
||||
.note-embed-excalidraw svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
*/
|
||||
|
||||
.note-embed-svg-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-embed-svg-center {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-embed-svg-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.note-embed-svg svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
`;
|
||||
153
src/main.ts
Normal file
153
src/main.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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 { Plugin, WorkspaceLeaf, App, PluginManifest, Menu, Notice, TAbstractFile, TFile, TFolder } from 'obsidian';
|
||||
import { NotePreview, VIEW_TYPE_NOTE_PREVIEW } from './note-preview';
|
||||
import { NMPSettings } from './settings';
|
||||
import { NoteToMpSettingTab } from './setting-tab';
|
||||
import AssetsManager from './assets';
|
||||
import { setVersion, uevent } from './utils';
|
||||
import { WidgetsModal } from './widgets-modal';
|
||||
|
||||
|
||||
export default class NoteToMpPlugin extends Plugin {
|
||||
settings: NMPSettings;
|
||||
assetsManager: AssetsManager;
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
AssetsManager.setup(app, manifest);
|
||||
this.assetsManager = AssetsManager.getInstance();
|
||||
}
|
||||
|
||||
async loadResource() {
|
||||
await this.loadSettings();
|
||||
await this.assetsManager.loadAssets();
|
||||
}
|
||||
|
||||
async onload() {
|
||||
console.log('Loading NoteToMP');
|
||||
setVersion(this.manifest.version);
|
||||
uevent('load');
|
||||
this.app.workspace.onLayoutReady(()=>{
|
||||
this.loadResource();
|
||||
})
|
||||
|
||||
this.registerView(
|
||||
VIEW_TYPE_NOTE_PREVIEW,
|
||||
(leaf) => new NotePreview(leaf, this)
|
||||
);
|
||||
|
||||
const ribbonIconEl = this.addRibbonIcon('clipboard-paste', '复制到公众号', (evt: MouseEvent) => {
|
||||
this.activateView();
|
||||
});
|
||||
ribbonIconEl.addClass('note-to-mp-plugin-ribbon-class');
|
||||
|
||||
this.addCommand({
|
||||
id: 'note-to-mp-preview',
|
||||
name: '复制到公众号',
|
||||
callback: () => {
|
||||
this.activateView();
|
||||
}
|
||||
});
|
||||
|
||||
this.addSettingTab(new NoteToMpSettingTab(this.app, this));
|
||||
|
||||
this.addCommand({
|
||||
id: 'note-to-mp-widget',
|
||||
name: '插入样式小部件',
|
||||
callback: () => {
|
||||
new WidgetsModal(this.app).open();
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'note-to-mp-pub',
|
||||
name: '发布公众号文章',
|
||||
callback: async () => {
|
||||
await this.activateView();
|
||||
this.getNotePreview()?.postArticle();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听右键菜单
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('file-menu', (menu, file) => {
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle('发布到公众号')
|
||||
.setIcon('lucide-send')
|
||||
.onClick(async () => {
|
||||
if (file instanceof TFile) {
|
||||
if (file.extension.toLowerCase() !== 'md') {
|
||||
new Notice('只能发布 Markdown 文件');
|
||||
return;
|
||||
}
|
||||
await this.activateView();
|
||||
await this.getNotePreview()?.renderMarkdown(file);
|
||||
await this.getNotePreview()?.postArticle();
|
||||
} else if (file instanceof TFolder) {
|
||||
await this.activateView();
|
||||
await this.getNotePreview()?.batchPost(file);
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
NMPSettings.loadSettings(await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(NMPSettings.allSettings());
|
||||
}
|
||||
|
||||
async activateView() {
|
||||
const { workspace } = this.app;
|
||||
|
||||
let leaf: WorkspaceLeaf | null = null;
|
||||
const leaves = workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
|
||||
|
||||
if (leaves.length > 0) {
|
||||
leaf = leaves[0];
|
||||
} else {
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
await leaf?.setViewState({ type: VIEW_TYPE_NOTE_PREVIEW, active: false });
|
||||
}
|
||||
|
||||
if (leaf) workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
getNotePreview(): NotePreview | null {
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
|
||||
if (leaves.length > 0) {
|
||||
const leaf = leaves[0];
|
||||
return leaf.view as NotePreview;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
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;
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/meta/index.ts
Normal file
76
src/meta/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
// [note-to-mp 重构] 元数据与封面模块
|
||||
import { LocalImage } from '../image';
|
||||
|
||||
export interface WeChatMetaRaw {
|
||||
title?: string;
|
||||
author?: string;
|
||||
coverLink?: string; // frontmatter 或行内指定的图片 basename 形式
|
||||
rawImage?: string; // 原 frontmatter 中的 image 字段原始值(可包含路径)
|
||||
hasFrontmatter: boolean;
|
||||
}
|
||||
|
||||
export interface FinalMeta {
|
||||
title: string;
|
||||
author?: string;
|
||||
coverImage?: LocalImage; // 解析到的封面图片对象
|
||||
coverLink?: string; // 决策后的封面 basename
|
||||
}
|
||||
|
||||
const FRONTMATTER_RE = /^---[\s\S]*?\n---/;
|
||||
|
||||
export function extractWeChatMeta(raw: string): { meta: WeChatMetaRaw; body: string } {
|
||||
const fmMatch = raw.match(FRONTMATTER_RE);
|
||||
if (!fmMatch) {
|
||||
return { meta: { hasFrontmatter: false }, body: raw };
|
||||
}
|
||||
const block = fmMatch[0];
|
||||
const lines = block.split(/\r?\n/).slice(1, -1); // 去除首尾 ---
|
||||
let title: string | undefined;
|
||||
let author: string | undefined;
|
||||
let image: string | undefined;
|
||||
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.*)$/);
|
||||
if (!m) continue;
|
||||
const key = m[1].toLowerCase();
|
||||
let val = m[2].trim();
|
||||
// 去除包裹引号
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
if (key === 'title') title = val;
|
||||
else if (key === 'author') author = val;
|
||||
else if (key === 'image' || key === 'cover') image = val;
|
||||
}
|
||||
|
||||
let coverLink: string | undefined;
|
||||
if (image) {
|
||||
const basename = image.split(/[?#]/)[0].split('/').pop() || image;
|
||||
coverLink = basename;
|
||||
}
|
||||
|
||||
const body = raw.slice(block.length).replace(/^\s+/, '');
|
||||
return { meta: { title, author, coverLink, rawImage: image, hasFrontmatter: true }, body };
|
||||
}
|
||||
|
||||
export function getMetadata(images: LocalImage[], rawMeta: WeChatMetaRaw): FinalMeta {
|
||||
// 标题回退策略:若无 frontmatter title,尝试第一行一级标题
|
||||
let title = rawMeta.title;
|
||||
if (!title) {
|
||||
// 简单取第一行 markdown 一级/二级标题
|
||||
// 实际调用方可传入 body 再做改进;这里保持接口简单
|
||||
title = '未命名文章';
|
||||
}
|
||||
|
||||
let coverLink = rawMeta.coverLink;
|
||||
let coverImage: LocalImage | undefined;
|
||||
if (coverLink) {
|
||||
coverImage = images.find(img => img.basename === coverLink);
|
||||
}
|
||||
if (!coverImage) {
|
||||
coverImage = images[0];
|
||||
coverLink = coverImage?.basename;
|
||||
}
|
||||
|
||||
return { title, author: rawMeta.author, coverImage, coverLink };
|
||||
}
|
||||
569
src/note-preview.ts
Normal file
569
src/note-preview.ts
Normal file
@@ -0,0 +1,569 @@
|
||||
/*
|
||||
* 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 { EventRef, ItemView, Workspace, WorkspaceLeaf, Notice, Platform, TFile, TFolder, TAbstractFile, Plugin } from 'obsidian';
|
||||
import { uevent, debounce, waitForLayoutReady } from './utils';
|
||||
// [note-to-mp 重构] 引入新渲染管线
|
||||
import { RenderService, RenderedArticle } from './render';
|
||||
import { NMPSettings } from './settings';
|
||||
import AssetsManager from './assets';
|
||||
import { MarkedParser } from './markdown/parser';
|
||||
import { LocalImageManager, LocalFile } from './markdown/local-file';
|
||||
import { CardDataManager } from './markdown/code';
|
||||
import { ArticleRender } from './article-render';
|
||||
|
||||
|
||||
export const VIEW_TYPE_NOTE_PREVIEW = 'note-preview';
|
||||
|
||||
export class NotePreview extends ItemView {
|
||||
workspace: Workspace;
|
||||
plugin: Plugin;
|
||||
mainDiv: HTMLDivElement;
|
||||
toolbar: HTMLDivElement;
|
||||
renderDiv: HTMLDivElement;
|
||||
articleDiv: HTMLDivElement;
|
||||
styleEl: HTMLElement;
|
||||
coverEl: HTMLInputElement;
|
||||
useDefaultCover: HTMLInputElement;
|
||||
useLocalCover: HTMLInputElement;
|
||||
msgView: HTMLDivElement;
|
||||
wechatSelect: HTMLSelectElement;
|
||||
themeSelect: HTMLSelectElement;
|
||||
highlightSelect: HTMLSelectElement;
|
||||
listeners?: EventRef[];
|
||||
container: Element;
|
||||
settings: NMPSettings;
|
||||
assetsManager: AssetsManager;
|
||||
articleHTML: string;
|
||||
title: string;
|
||||
currentFile?: TFile;
|
||||
currentTheme: string;
|
||||
currentHighlight: string;
|
||||
currentAppId: string;
|
||||
markedParser: MarkedParser;
|
||||
cachedElements: Map<string, string> = new Map();
|
||||
_articleRender: ArticleRender | null = null;
|
||||
isCancelUpload: boolean = false;
|
||||
isBatchRuning: boolean = false;
|
||||
// [note-to-mp 重构] 新渲染服务实例与最近一次渲染结果
|
||||
newRenderService: RenderService | null = null;
|
||||
lastArticle?: RenderedArticle;
|
||||
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: Plugin) {
|
||||
super(leaf);
|
||||
this.workspace = this.app.workspace;
|
||||
this.plugin = plugin;
|
||||
this.settings = NMPSettings.getInstance();
|
||||
this.assetsManager = AssetsManager.getInstance();
|
||||
this.currentTheme = this.settings.defaultStyle;
|
||||
this.currentHighlight = this.settings.defaultHighlight;
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return VIEW_TYPE_NOTE_PREVIEW;
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return 'clipboard-paste';
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return '笔记预览';
|
||||
}
|
||||
|
||||
get render() {
|
||||
if (!this._articleRender) {
|
||||
this._articleRender = new ArticleRender(this.app, this, this.styleEl, this.articleDiv);
|
||||
this._articleRender.currentTheme = this.currentTheme;
|
||||
this._articleRender.currentHighlight = this.currentHighlight;
|
||||
}
|
||||
return this._articleRender;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.viewLoading();
|
||||
this.setup();
|
||||
uevent('open');
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await waitForLayoutReady(this.app);
|
||||
|
||||
if (!this.settings.isLoaded) {
|
||||
const data = await this.plugin.loadData();
|
||||
NMPSettings.loadSettings(data);
|
||||
}
|
||||
if (!this.assetsManager.isLoaded) {
|
||||
await this.assetsManager.loadAssets();
|
||||
}
|
||||
|
||||
this.buildUI();
|
||||
// [note-to-mp 重构] 初始化新渲染服务
|
||||
this.newRenderService = new RenderService(this.app);
|
||||
this.listeners = [
|
||||
this.workspace.on('file-open', () => {
|
||||
this.update();
|
||||
}),
|
||||
this.app.vault.on("modify", (file) => {
|
||||
if (this.currentFile?.path == file.path) {
|
||||
this.renderMarkdown();
|
||||
}
|
||||
} )
|
||||
];
|
||||
|
||||
this.renderMarkdown();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.listeners?.forEach(listener => this.workspace.offref(listener));
|
||||
LocalFile.fileCache.clear();
|
||||
uevent('close');
|
||||
}
|
||||
|
||||
onAppIdChanged() {
|
||||
// 清理上传过的图片
|
||||
this.cleanArticleData();
|
||||
}
|
||||
|
||||
async update() {
|
||||
if (this.isBatchRuning) {
|
||||
return;
|
||||
}
|
||||
this.cleanArticleData();
|
||||
this.renderMarkdown();
|
||||
}
|
||||
|
||||
cleanArticleData() {
|
||||
LocalImageManager.getInstance().cleanup();
|
||||
CardDataManager.getInstance().cleanup();
|
||||
}
|
||||
|
||||
buildMsgView(parent: HTMLDivElement) {
|
||||
this.msgView = parent.createDiv({ cls: 'msg-view' });
|
||||
const title = this.msgView.createDiv({ cls: 'msg-title' });
|
||||
title.id = 'msg-title';
|
||||
title.innerText = '加载中...';
|
||||
const okBtn = this.msgView.createEl('button', { cls: 'msg-ok-btn' }, async (button) => {
|
||||
|
||||
});
|
||||
okBtn.id = 'msg-ok-btn';
|
||||
okBtn.innerText = '确定';
|
||||
okBtn.onclick = async () => {
|
||||
this.msgView.setAttr('style', 'display: none;');
|
||||
}
|
||||
const cancelBtn = this.msgView.createEl('button', { cls: 'msg-ok-btn' }, async (button) => {
|
||||
});
|
||||
cancelBtn.id = 'msg-cancel-btn';
|
||||
cancelBtn.innerText = '取消';
|
||||
cancelBtn.onclick = async () => {
|
||||
this.isCancelUpload = true;
|
||||
this.msgView.setAttr('style', 'display: none;');
|
||||
}
|
||||
}
|
||||
|
||||
showLoading(msg: string, cancelable: boolean = false) {
|
||||
const title = this.msgView.querySelector('#msg-title') as HTMLElement;
|
||||
title!.innerText = msg;
|
||||
const btn = this.msgView.querySelector('#msg-ok-btn') as HTMLElement;
|
||||
btn.setAttr('style', 'display: none;');
|
||||
this.msgView.setAttr('style', 'display: flex;');
|
||||
const cancelBtn = this.msgView.querySelector('#msg-cancel-btn') as HTMLElement;
|
||||
cancelBtn.setAttr('style', cancelable ? 'display: block;': 'display: none;');
|
||||
this.msgView.setAttr('style', 'display: flex;');
|
||||
}
|
||||
|
||||
showMsg(msg: string) {
|
||||
const title = this.msgView.querySelector('#msg-title') as HTMLElement;
|
||||
title!.innerText = msg;
|
||||
const btn = this.msgView.querySelector('#msg-ok-btn') as HTMLElement;
|
||||
btn.setAttr('style', 'display: block;');
|
||||
this.msgView.setAttr('style', 'display: flex;');
|
||||
const cancelBtn = this.msgView.querySelector('#msg-cancel-btn') as HTMLElement;
|
||||
cancelBtn.setAttr('style', 'display: none;');
|
||||
this.msgView.setAttr('style', 'display: flex;');
|
||||
}
|
||||
|
||||
buildToolbar(parent: HTMLDivElement) {
|
||||
this.toolbar = parent.createDiv({ cls: 'preview-toolbar' });
|
||||
let lineDiv;
|
||||
|
||||
// 公众号
|
||||
if (this.settings.wxInfo.length > 1 || Platform.isDesktop) {
|
||||
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line' });
|
||||
lineDiv.createDiv({ cls: 'style-label' }).innerText = '公众号:';
|
||||
const wxSelect = lineDiv.createEl('select', { cls: 'style-select' })
|
||||
wxSelect.setAttr('style', 'width: 200px');
|
||||
wxSelect.onchange = async () => {
|
||||
this.currentAppId = wxSelect.value;
|
||||
this.onAppIdChanged();
|
||||
}
|
||||
const defautlOp =wxSelect.createEl('option');
|
||||
defautlOp.value = '';
|
||||
defautlOp.text = '请在设置里配置公众号';
|
||||
for (let i = 0; i < this.settings.wxInfo.length; i++) {
|
||||
const op = wxSelect.createEl('option');
|
||||
const wx = this.settings.wxInfo[i];
|
||||
op.value = wx.appid;
|
||||
op.text = wx.name;
|
||||
if (i== 0) {
|
||||
op.selected = true
|
||||
this.currentAppId = wx.appid;
|
||||
}
|
||||
}
|
||||
this.wechatSelect = wxSelect;
|
||||
|
||||
if (Platform.isDesktop) {
|
||||
const openBtn = lineDiv.createEl('button', { cls: 'refresh-button' }, async (button) => {
|
||||
button.setText('去公众号后台');
|
||||
})
|
||||
|
||||
openBtn.onclick = async () => {
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal('https://mp.weixin.qq.com')
|
||||
uevent('open-mp');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.settings.wxInfo.length > 0) {
|
||||
this.currentAppId = this.settings.wxInfo[0].appid;
|
||||
}
|
||||
|
||||
// 复制,刷新,带图片复制,发草稿箱
|
||||
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line' });
|
||||
const refreshBtn = lineDiv.createEl('button', { cls: 'refresh-button' }, async (button) => {
|
||||
button.setText('刷新');
|
||||
})
|
||||
|
||||
refreshBtn.onclick = async () => {
|
||||
await this.assetsManager.loadCustomCSS();
|
||||
await this.assetsManager.loadExpertSettings();
|
||||
this.render.reloadStyle();
|
||||
await this.renderMarkdown();
|
||||
uevent('refresh');
|
||||
}
|
||||
if (Platform.isDesktop) {
|
||||
const copyBtn = lineDiv.createEl('button', { cls: 'copy-button' }, async (button) => {
|
||||
button.setText('复制');
|
||||
})
|
||||
|
||||
copyBtn.onclick = async() => {
|
||||
try {
|
||||
await this.render.copyArticle();
|
||||
new Notice('复制成功,请到公众号编辑器粘贴。');
|
||||
uevent('copy');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
new Notice('复制失败: ' + error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uploadImgBtn = lineDiv.createEl('button', { cls: 'copy-button' }, async (button) => {
|
||||
button.setText('上传图片');
|
||||
})
|
||||
|
||||
uploadImgBtn.onclick = async() => {
|
||||
await this.uploadImages();
|
||||
uevent('upload');
|
||||
}
|
||||
|
||||
const postBtn = lineDiv.createEl('button', { cls: 'copy-button' }, async (button) => {
|
||||
button.setText('发草稿');
|
||||
})
|
||||
|
||||
postBtn.onclick = async() => {
|
||||
await this.postArticle();
|
||||
uevent('pub');
|
||||
}
|
||||
|
||||
const imagesBtn = lineDiv.createEl('button', { cls: 'copy-button' }, async (button) => {
|
||||
button.setText('图片/文字');
|
||||
})
|
||||
|
||||
imagesBtn.onclick = async() => {
|
||||
await this.postImages();
|
||||
uevent('pub-images');
|
||||
}
|
||||
|
||||
if (Platform.isDesktop && this.settings.isAuthKeyVaild()) {
|
||||
const htmlBtn = lineDiv.createEl('button', { cls: 'copy-button' }, async (button) => {
|
||||
button.setText('导出HTML');
|
||||
})
|
||||
|
||||
htmlBtn.onclick = async() => {
|
||||
await this.exportHTML();
|
||||
uevent('export-html');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 封面
|
||||
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line' });
|
||||
|
||||
const coverTitle = lineDiv.createDiv({ cls: 'style-label' });
|
||||
coverTitle.innerText = '封面:';
|
||||
|
||||
this.useDefaultCover = lineDiv.createEl('input', { cls: 'input-style' });
|
||||
this.useDefaultCover.setAttr('type', 'radio');
|
||||
this.useDefaultCover.setAttr('name', 'cover');
|
||||
this.useDefaultCover.setAttr('value', 'default');
|
||||
this.useDefaultCover.setAttr('checked', true);
|
||||
this.useDefaultCover.id = 'default-cover';
|
||||
this.useDefaultCover.onchange = async () => {
|
||||
if (this.useDefaultCover.checked) {
|
||||
this.coverEl.setAttr('style', 'visibility:hidden;width:0px;');
|
||||
}
|
||||
else {
|
||||
this.coverEl.setAttr('style', 'visibility:visible;width:180px;');
|
||||
}
|
||||
}
|
||||
const defaultLable = lineDiv.createEl('label');
|
||||
defaultLable.innerText = '默认';
|
||||
defaultLable.setAttr('for', 'default-cover');
|
||||
|
||||
this.useLocalCover = lineDiv.createEl('input', { cls: 'input-style' });
|
||||
this.useLocalCover.setAttr('type', 'radio');
|
||||
this.useLocalCover.setAttr('name', 'cover');
|
||||
this.useLocalCover.setAttr('value', 'local');
|
||||
this.useLocalCover.id = 'local-cover';
|
||||
this.useLocalCover.setAttr('style', 'margin-left:20px;');
|
||||
this.useLocalCover.onchange = async () => {
|
||||
if (this.useLocalCover.checked) {
|
||||
this.coverEl.setAttr('style', 'visibility:visible;width:180px;');
|
||||
}
|
||||
else {
|
||||
this.coverEl.setAttr('style', 'visibility:hidden;width:0px;');
|
||||
}
|
||||
}
|
||||
|
||||
const localLabel = lineDiv.createEl('label');
|
||||
localLabel.setAttr('for', 'local-cover');
|
||||
localLabel.innerText = '上传';
|
||||
|
||||
this.coverEl = lineDiv.createEl('input', { cls: 'upload-input' });
|
||||
this.coverEl.setAttr('type', 'file');
|
||||
this.coverEl.setAttr('placeholder', '封面图片');
|
||||
this.coverEl.setAttr('accept', '.png, .jpg, .jpeg');
|
||||
this.coverEl.setAttr('name', 'cover');
|
||||
this.coverEl.id = 'cover-input';
|
||||
|
||||
// 样式
|
||||
if (this.settings.showStyleUI) {
|
||||
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line' });
|
||||
const cssStyle = lineDiv.createDiv({ cls: 'style-label' });
|
||||
cssStyle.innerText = '样式:';
|
||||
|
||||
const selectBtn = lineDiv.createEl('select', { cls: 'style-select' }, async (sel) => {
|
||||
|
||||
})
|
||||
|
||||
selectBtn.onchange = async () => {
|
||||
this.currentTheme = selectBtn.value;
|
||||
this.render.updateStyle(selectBtn.value);
|
||||
}
|
||||
|
||||
for (let s of this.assetsManager.themes) {
|
||||
const op = selectBtn.createEl('option');
|
||||
op.value = s.className;
|
||||
op.text = s.name;
|
||||
op.selected = s.className == this.settings.defaultStyle;
|
||||
}
|
||||
|
||||
this.themeSelect = selectBtn;
|
||||
|
||||
const highlightStyle = lineDiv.createDiv({ cls: 'style-label' });
|
||||
highlightStyle.innerText = '代码高亮:';
|
||||
|
||||
const highlightStyleBtn = lineDiv.createEl('select', { cls: 'style-select' }, async (sel) => {
|
||||
|
||||
})
|
||||
|
||||
highlightStyleBtn.onchange = async () => {
|
||||
this.currentHighlight = highlightStyleBtn.value;
|
||||
this.render.updateHighLight(highlightStyleBtn.value);
|
||||
}
|
||||
|
||||
for (let s of this.assetsManager.highlights) {
|
||||
const op = highlightStyleBtn.createEl('option');
|
||||
op.value = s.name;
|
||||
op.text = s.name;
|
||||
op.selected = s.name == this.settings.defaultHighlight;
|
||||
}
|
||||
|
||||
this.highlightSelect = highlightStyleBtn;
|
||||
}
|
||||
|
||||
this.buildMsgView(this.toolbar);
|
||||
}
|
||||
|
||||
async buildUI() {
|
||||
this.container = this.containerEl.children[1];
|
||||
this.container.empty();
|
||||
|
||||
this.mainDiv = this.container.createDiv({ cls: 'note-preview' });
|
||||
|
||||
this.buildToolbar(this.mainDiv);
|
||||
|
||||
this.renderDiv = this.mainDiv.createDiv({cls: 'render-div'});
|
||||
this.renderDiv.id = 'render-div';
|
||||
this.renderDiv.setAttribute('style', '-webkit-user-select: text; user-select: text;');
|
||||
this.styleEl = this.renderDiv.createEl('style');
|
||||
this.styleEl.setAttr('title', 'note-to-mp-style');
|
||||
this.articleDiv = this.renderDiv.createEl('div');
|
||||
}
|
||||
|
||||
async viewLoading() {
|
||||
const container = this.containerEl.children[1]
|
||||
container.empty();
|
||||
const loading = container.createDiv({cls: 'loading-wrapper'})
|
||||
loading.createDiv({cls: 'loading-spinner'})
|
||||
}
|
||||
|
||||
async renderMarkdown(af: TFile | null = null) {
|
||||
if (!af) {
|
||||
af = this.app.workspace.getActiveFile();
|
||||
}
|
||||
if (!af || af.extension.toLocaleLowerCase() !== 'md') {
|
||||
return;
|
||||
}
|
||||
this.currentFile = af;
|
||||
// [note-to-mp 重构] 使用新渲染服务进行渲染
|
||||
if (this.newRenderService) {
|
||||
try {
|
||||
const article = await this.newRenderService.renderFile(af);
|
||||
this.lastArticle = article;
|
||||
if (this.articleDiv) {
|
||||
this.articleDiv.empty();
|
||||
const wrap = this.articleDiv.createDiv();
|
||||
wrap.innerHTML = article.html;
|
||||
}
|
||||
// 元数据适配(当前新 meta 不含 appid/theme/highlight,保持现有选择状态)
|
||||
if (this.wechatSelect) {
|
||||
this.wechatSelect.value = this.currentAppId || '';
|
||||
}
|
||||
if (this.themeSelect) {
|
||||
this.themeSelect.value = this.currentTheme;
|
||||
}
|
||||
if (this.highlightSelect) {
|
||||
this.highlightSelect.value = this.currentHighlight;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[note-to-mp 重构] 渲染失败', e);
|
||||
new Notice('渲染失败: ' + e.message);
|
||||
}
|
||||
} else {
|
||||
// 兜底:仍使用旧渲染
|
||||
await this.render.renderMarkdown(af);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadImages() {
|
||||
this.showLoading('图片上传中...');
|
||||
try {
|
||||
await this.render.uploadImages(this.currentAppId);
|
||||
this.showMsg('图片上传成功,并且文章内容已复制,请到公众号编辑器粘贴。');
|
||||
} catch (error) {
|
||||
this.showMsg('图片上传失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async postArticle() {
|
||||
let localCover = null;
|
||||
if (this.useLocalCover.checked) {
|
||||
const fileInput = this.coverEl;
|
||||
if (!fileInput.files || fileInput.files.length === 0) {
|
||||
this.showMsg('请选择封面文件');
|
||||
return;
|
||||
}
|
||||
localCover = fileInput.files[0];
|
||||
if (!localCover) {
|
||||
this.showMsg('请选择封面文件');
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.showLoading('发布中...');
|
||||
try {
|
||||
await this.render.postArticle(this.currentAppId, localCover);
|
||||
this.showMsg('发布成功');
|
||||
}
|
||||
catch (error) {
|
||||
this.showMsg('发布失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async postImages() {
|
||||
this.showLoading('发布图片中...');
|
||||
try {
|
||||
await this.render.postImages(this.currentAppId);
|
||||
this.showMsg('图片发布成功');
|
||||
} catch (error) {
|
||||
this.showMsg('图片发布失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async exportHTML() {
|
||||
this.showLoading('导出HTML中...');
|
||||
try {
|
||||
await this.render.exportHTML();
|
||||
this.showMsg('HTML导出成功');
|
||||
} catch (error) {
|
||||
this.showMsg('HTML导出失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async batchPost(folder: TFolder) {
|
||||
const files = folder.children.filter((child: TAbstractFile) => child.path.toLocaleLowerCase().endsWith('.md'));
|
||||
if (!files) {
|
||||
new Notice('没有可渲染的笔记或文件不支持渲染');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCancelUpload = false;
|
||||
this.isBatchRuning = true;
|
||||
|
||||
try {
|
||||
for (let file of files) {
|
||||
this.showLoading(`即将发布: ${file.name}`, true);
|
||||
await sleep(5000);
|
||||
if (this.isCancelUpload) {
|
||||
break;
|
||||
}
|
||||
this.cleanArticleData();
|
||||
await this.renderMarkdown(file as TFile);
|
||||
await this.postArticle();
|
||||
}
|
||||
|
||||
if (!this.isCancelUpload) {
|
||||
this.showMsg(`批量发布完成:成功发布 ${files.length} 篇笔记`);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
new Notice('批量发布失败: ' + e.message);
|
||||
}
|
||||
finally {
|
||||
this.isBatchRuning = false;
|
||||
this.isCancelUpload = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
src/postcss/at-rule.d.ts
vendored
Normal file
140
src/postcss/at-rule.d.ts
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
import Container, {
|
||||
ContainerProps,
|
||||
ContainerWithChildren
|
||||
} from './container.js'
|
||||
|
||||
declare namespace AtRule {
|
||||
export interface AtRuleRaws extends Record<string, unknown> {
|
||||
/**
|
||||
* The space symbols after the last child of the node to the end of the node.
|
||||
*/
|
||||
after?: string
|
||||
|
||||
/**
|
||||
* The space between the at-rule name and its parameters.
|
||||
*/
|
||||
afterName?: string
|
||||
|
||||
/**
|
||||
* The space symbols before the node. It also stores `*`
|
||||
* and `_` symbols before the declaration (IE hack).
|
||||
*/
|
||||
before?: string
|
||||
|
||||
/**
|
||||
* The symbols between the last parameter and `{` for rules.
|
||||
*/
|
||||
between?: string
|
||||
|
||||
/**
|
||||
* The rule’s selector with comments.
|
||||
*/
|
||||
params?: {
|
||||
raw: string
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains `true` if the last child has an (optional) semicolon.
|
||||
*/
|
||||
semicolon?: boolean
|
||||
}
|
||||
|
||||
export interface AtRuleProps extends ContainerProps {
|
||||
/** Name of the at-rule. */
|
||||
name: string
|
||||
/** Parameters following the name of the at-rule. */
|
||||
params?: number | string
|
||||
/** Information used to generate byte-to-byte equal node string as it was in the origin input. */
|
||||
raws?: AtRuleRaws
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { AtRule_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an at-rule.
|
||||
*
|
||||
* ```js
|
||||
* Once (root, { AtRule }) {
|
||||
* let media = new AtRule({ name: 'media', params: 'print' })
|
||||
* media.append(…)
|
||||
* root.append(media)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If it’s followed in the CSS by a `{}` block, this node will have
|
||||
* a nodes property representing its children.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('@charset "UTF-8"; @media print {}')
|
||||
*
|
||||
* const charset = root.first
|
||||
* charset.type //=> 'atrule'
|
||||
* charset.nodes //=> undefined
|
||||
*
|
||||
* const media = root.last
|
||||
* media.nodes //=> []
|
||||
* ```
|
||||
*/
|
||||
declare class AtRule_ extends Container {
|
||||
/**
|
||||
* The at-rule’s name immediately follows the `@`.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('@media print {}')
|
||||
* const media = root.first
|
||||
* media.name //=> 'media'
|
||||
* ```
|
||||
*/
|
||||
get name(): string
|
||||
set name(value: string)
|
||||
|
||||
/**
|
||||
* An array containing the layer’s children.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('@layer example { a { color: black } }')
|
||||
* const layer = root.first
|
||||
* layer.nodes.length //=> 1
|
||||
* layer.nodes[0].selector //=> 'a'
|
||||
* ```
|
||||
*
|
||||
* Can be `undefinded` if the at-rule has no body.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('@layer a, b, c;')
|
||||
* const layer = root.first
|
||||
* layer.nodes //=> undefined
|
||||
* ```
|
||||
*/
|
||||
nodes: Container['nodes']
|
||||
/**
|
||||
* The at-rule’s parameters, the values that follow the at-rule’s name
|
||||
* but precede any `{}` block.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('@media print, screen {}')
|
||||
* const media = root.first
|
||||
* media.params //=> 'print, screen'
|
||||
* ```
|
||||
*/
|
||||
get params(): string
|
||||
set params(value: string)
|
||||
parent: ContainerWithChildren | undefined
|
||||
|
||||
raws: AtRule.AtRuleRaws
|
||||
|
||||
type: 'atrule'
|
||||
|
||||
constructor(defaults?: AtRule.AtRuleProps)
|
||||
assign(overrides: AtRule.AtRuleProps | object): this
|
||||
clone(overrides?: Partial<AtRule.AtRuleProps>): AtRule
|
||||
cloneAfter(overrides?: Partial<AtRule.AtRuleProps>): AtRule
|
||||
cloneBefore(overrides?: Partial<AtRule.AtRuleProps>): AtRule
|
||||
}
|
||||
|
||||
declare class AtRule extends AtRule_ {}
|
||||
|
||||
export = AtRule
|
||||
68
src/postcss/comment.d.ts
vendored
Normal file
68
src/postcss/comment.d.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
import Container from './container.js'
|
||||
import Node, { NodeProps } from './node.js'
|
||||
|
||||
declare namespace Comment {
|
||||
export interface CommentRaws extends Record<string, unknown> {
|
||||
/**
|
||||
* The space symbols before the node.
|
||||
*/
|
||||
before?: string
|
||||
|
||||
/**
|
||||
* The space symbols between `/*` and the comment’s text.
|
||||
*/
|
||||
left?: string
|
||||
|
||||
/**
|
||||
* The space symbols between the comment’s text.
|
||||
*/
|
||||
right?: string
|
||||
}
|
||||
|
||||
export interface CommentProps extends NodeProps {
|
||||
/** Information used to generate byte-to-byte equal node string as it was in the origin input. */
|
||||
raws?: CommentRaws
|
||||
/** Content of the comment. */
|
||||
text: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Comment_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* It represents a class that handles
|
||||
* [CSS comments](https://developer.mozilla.org/en-US/docs/Web/CSS/Comments)
|
||||
*
|
||||
* ```js
|
||||
* Once (root, { Comment }) {
|
||||
* const note = new Comment({ text: 'Note: …' })
|
||||
* root.append(note)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Remember that CSS comments inside selectors, at-rule parameters,
|
||||
* or declaration values will be stored in the `raws` properties
|
||||
* explained above.
|
||||
*/
|
||||
declare class Comment_ extends Node {
|
||||
parent: Container | undefined
|
||||
raws: Comment.CommentRaws
|
||||
/**
|
||||
* The comment's text.
|
||||
*/
|
||||
get text(): string
|
||||
set text(value: string)
|
||||
|
||||
type: 'comment'
|
||||
|
||||
constructor(defaults?: Comment.CommentProps)
|
||||
assign(overrides: Comment.CommentProps | object): this
|
||||
clone(overrides?: Partial<Comment.CommentProps>): Comment
|
||||
cloneAfter(overrides?: Partial<Comment.CommentProps>): Comment
|
||||
cloneBefore(overrides?: Partial<Comment.CommentProps>): Comment
|
||||
}
|
||||
|
||||
declare class Comment extends Comment_ {}
|
||||
|
||||
export = Comment
|
||||
490
src/postcss/container.d.ts
vendored
Normal file
490
src/postcss/container.d.ts
vendored
Normal file
@@ -0,0 +1,490 @@
|
||||
import AtRule from './at-rule.js'
|
||||
import Comment from './comment.js'
|
||||
import Declaration from './declaration.js'
|
||||
import Node, { ChildNode, ChildProps, NodeProps } from './node.js'
|
||||
import Rule from './rule.js'
|
||||
|
||||
declare namespace Container {
|
||||
export class ContainerWithChildren<
|
||||
Child extends Node = ChildNode
|
||||
> extends Container_<Child> {
|
||||
nodes: Child[]
|
||||
}
|
||||
|
||||
export interface ValueOptions {
|
||||
/**
|
||||
* String that’s used to narrow down values and speed up the regexp search.
|
||||
*/
|
||||
fast?: string
|
||||
|
||||
/**
|
||||
* An array of property names.
|
||||
*/
|
||||
props?: string[]
|
||||
}
|
||||
|
||||
export interface ContainerProps extends NodeProps {
|
||||
nodes?: (ChildNode | ChildProps)[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Container_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Root`, `AtRule`, and `Rule` container nodes
|
||||
* inherit some common methods to help work with their children.
|
||||
*
|
||||
* Note that all containers can store any content. If you write a rule inside
|
||||
* a rule, PostCSS will parse it.
|
||||
*/
|
||||
declare abstract class Container_<Child extends Node = ChildNode> extends Node {
|
||||
/**
|
||||
* An array containing the container’s children.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a { color: black }')
|
||||
* root.nodes.length //=> 1
|
||||
* root.nodes[0].selector //=> 'a'
|
||||
* root.nodes[0].nodes[0].prop //=> 'color'
|
||||
* ```
|
||||
*/
|
||||
nodes: Child[] | undefined
|
||||
|
||||
/**
|
||||
* Inserts new nodes to the end of the container.
|
||||
*
|
||||
* ```js
|
||||
* const decl1 = new Declaration({ prop: 'color', value: 'black' })
|
||||
* const decl2 = new Declaration({ prop: 'background-color', value: 'white' })
|
||||
* rule.append(decl1, decl2)
|
||||
*
|
||||
* root.append({ name: 'charset', params: '"UTF-8"' }) // at-rule
|
||||
* root.append({ selector: 'a' }) // rule
|
||||
* rule.append({ prop: 'color', value: 'black' }) // declaration
|
||||
* rule.append({ text: 'Comment' }) // comment
|
||||
*
|
||||
* root.append('a {}')
|
||||
* root.first.append('color: black; z-index: 1')
|
||||
* ```
|
||||
*
|
||||
* @param nodes New nodes.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
append(
|
||||
...nodes: (
|
||||
| ChildProps
|
||||
| ChildProps[]
|
||||
| Node
|
||||
| Node[]
|
||||
| string
|
||||
| string[]
|
||||
| undefined
|
||||
)[]
|
||||
): this
|
||||
|
||||
assign(overrides: Container.ContainerProps | object): this
|
||||
clone(overrides?: Partial<Container.ContainerProps>): Container<Child>
|
||||
cloneAfter(overrides?: Partial<Container.ContainerProps>): Container<Child>
|
||||
cloneBefore(overrides?: Partial<Container.ContainerProps>): Container<Child>
|
||||
|
||||
/**
|
||||
* Iterates through the container’s immediate children,
|
||||
* calling `callback` for each child.
|
||||
*
|
||||
* Returning `false` in the callback will break iteration.
|
||||
*
|
||||
* This method only iterates through the container’s immediate children.
|
||||
* If you need to recursively iterate through all the container’s descendant
|
||||
* nodes, use `Container#walk`.
|
||||
*
|
||||
* Unlike the for `{}`-cycle or `Array#forEach` this iterator is safe
|
||||
* if you are mutating the array of child nodes during iteration.
|
||||
* PostCSS will adjust the current index to match the mutations.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a { color: black; z-index: 1 }')
|
||||
* const rule = root.first
|
||||
*
|
||||
* for (const decl of rule.nodes) {
|
||||
* decl.cloneBefore({ prop: '-webkit-' + decl.prop })
|
||||
* // Cycle will be infinite, because cloneBefore moves the current node
|
||||
* // to the next index
|
||||
* }
|
||||
*
|
||||
* rule.each(decl => {
|
||||
* decl.cloneBefore({ prop: '-webkit-' + decl.prop })
|
||||
* // Will be executed only for color and z-index
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param callback Iterator receives each node and index.
|
||||
* @return Returns `false` if iteration was broke.
|
||||
*/
|
||||
each(
|
||||
callback: (node: Child, index: number) => false | void
|
||||
): false | undefined
|
||||
|
||||
/**
|
||||
* Returns `true` if callback returns `true`
|
||||
* for all of the container’s children.
|
||||
*
|
||||
* ```js
|
||||
* const noPrefixes = rule.every(i => i.prop[0] !== '-')
|
||||
* ```
|
||||
*
|
||||
* @param condition Iterator returns true or false.
|
||||
* @return Is every child pass condition.
|
||||
*/
|
||||
every(
|
||||
condition: (node: Child, index: number, nodes: Child[]) => boolean
|
||||
): boolean
|
||||
/**
|
||||
* Returns a `child`’s index within the `Container#nodes` array.
|
||||
*
|
||||
* ```js
|
||||
* rule.index( rule.nodes[2] ) //=> 2
|
||||
* ```
|
||||
*
|
||||
* @param child Child of the current container.
|
||||
* @return Child index.
|
||||
*/
|
||||
index(child: Child | number): number
|
||||
|
||||
/**
|
||||
* Insert new node after old node within the container.
|
||||
*
|
||||
* @param oldNode Child or child’s index.
|
||||
* @param newNode New node.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
insertAfter(
|
||||
oldNode: Child | number,
|
||||
newNode:
|
||||
| Child
|
||||
| Child[]
|
||||
| ChildProps
|
||||
| ChildProps[]
|
||||
| string
|
||||
| string[]
|
||||
| undefined
|
||||
): this
|
||||
/**
|
||||
* Insert new node before old node within the container.
|
||||
*
|
||||
* ```js
|
||||
* rule.insertBefore(decl, decl.clone({ prop: '-webkit-' + decl.prop }))
|
||||
* ```
|
||||
*
|
||||
* @param oldNode Child or child’s index.
|
||||
* @param newNode New node.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
insertBefore(
|
||||
oldNode: Child | number,
|
||||
newNode:
|
||||
| Child
|
||||
| Child[]
|
||||
| ChildProps
|
||||
| ChildProps[]
|
||||
| string
|
||||
| string[]
|
||||
| undefined
|
||||
): this
|
||||
|
||||
/**
|
||||
* Traverses the container’s descendant nodes, calling callback
|
||||
* for each comment node.
|
||||
*
|
||||
* Like `Container#each`, this method is safe
|
||||
* to use if you are mutating arrays during iteration.
|
||||
*
|
||||
* ```js
|
||||
* root.walkComments(comment => {
|
||||
* comment.remove()
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param callback Iterator receives each node and index.
|
||||
* @return Returns `false` if iteration was broke.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Inserts new nodes to the start of the container.
|
||||
*
|
||||
* ```js
|
||||
* const decl1 = new Declaration({ prop: 'color', value: 'black' })
|
||||
* const decl2 = new Declaration({ prop: 'background-color', value: 'white' })
|
||||
* rule.prepend(decl1, decl2)
|
||||
*
|
||||
* root.append({ name: 'charset', params: '"UTF-8"' }) // at-rule
|
||||
* root.append({ selector: 'a' }) // rule
|
||||
* rule.append({ prop: 'color', value: 'black' }) // declaration
|
||||
* rule.append({ text: 'Comment' }) // comment
|
||||
*
|
||||
* root.append('a {}')
|
||||
* root.first.append('color: black; z-index: 1')
|
||||
* ```
|
||||
*
|
||||
* @param nodes New nodes.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
prepend(
|
||||
...nodes: (
|
||||
| ChildProps
|
||||
| ChildProps[]
|
||||
| Node
|
||||
| Node[]
|
||||
| string
|
||||
| string[]
|
||||
| undefined
|
||||
)[]
|
||||
): this
|
||||
/**
|
||||
* Add child to the end of the node.
|
||||
*
|
||||
* ```js
|
||||
* rule.push(new Declaration({ prop: 'color', value: 'black' }))
|
||||
* ```
|
||||
*
|
||||
* @param child New node.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
push(child: Child): this
|
||||
|
||||
/**
|
||||
* Removes all children from the container
|
||||
* and cleans their parent properties.
|
||||
*
|
||||
* ```js
|
||||
* rule.removeAll()
|
||||
* rule.nodes.length //=> 0
|
||||
* ```
|
||||
*
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
removeAll(): this
|
||||
|
||||
/**
|
||||
* Removes node from the container and cleans the parent properties
|
||||
* from the node and its children.
|
||||
*
|
||||
* ```js
|
||||
* rule.nodes.length //=> 5
|
||||
* rule.removeChild(decl)
|
||||
* rule.nodes.length //=> 4
|
||||
* decl.parent //=> undefined
|
||||
* ```
|
||||
*
|
||||
* @param child Child or child’s index.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
removeChild(child: Child | number): this
|
||||
|
||||
replaceValues(
|
||||
pattern: RegExp | string,
|
||||
replaced: { (substring: string, ...args: any[]): string } | string
|
||||
): this
|
||||
|
||||
/**
|
||||
* Passes all declaration values within the container that match pattern
|
||||
* through callback, replacing those values with the returned result
|
||||
* of callback.
|
||||
*
|
||||
* This method is useful if you are using a custom unit or function
|
||||
* and need to iterate through all values.
|
||||
*
|
||||
* ```js
|
||||
* root.replaceValues(/\d+rem/, { fast: 'rem' }, string => {
|
||||
* return 15 * parseInt(string) + 'px'
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param pattern Replace pattern.
|
||||
* @param {object} opts Options to speed up the search.
|
||||
* @param callback String to replace pattern or callback
|
||||
* that returns a new value. The callback
|
||||
* will receive the same arguments
|
||||
* as those passed to a function parameter
|
||||
* of `String#replace`.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
replaceValues(
|
||||
pattern: RegExp | string,
|
||||
options: Container.ValueOptions,
|
||||
replaced: { (substring: string, ...args: any[]): string } | string
|
||||
): this
|
||||
|
||||
/**
|
||||
* Returns `true` if callback returns `true` for (at least) one
|
||||
* of the container’s children.
|
||||
*
|
||||
* ```js
|
||||
* const hasPrefix = rule.some(i => i.prop[0] === '-')
|
||||
* ```
|
||||
*
|
||||
* @param condition Iterator returns true or false.
|
||||
* @return Is some child pass condition.
|
||||
*/
|
||||
some(
|
||||
condition: (node: Child, index: number, nodes: Child[]) => boolean
|
||||
): boolean
|
||||
|
||||
/**
|
||||
* Traverses the container’s descendant nodes, calling callback
|
||||
* for each node.
|
||||
*
|
||||
* Like container.each(), this method is safe to use
|
||||
* if you are mutating arrays during iteration.
|
||||
*
|
||||
* If you only need to iterate through the container’s immediate children,
|
||||
* use `Container#each`.
|
||||
*
|
||||
* ```js
|
||||
* root.walk(node => {
|
||||
* // Traverses all descendant nodes.
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param callback Iterator receives each node and index.
|
||||
* @return Returns `false` if iteration was broke.
|
||||
*/
|
||||
walk(
|
||||
callback: (node: ChildNode, index: number) => false | void
|
||||
): false | undefined
|
||||
|
||||
/**
|
||||
* Traverses the container’s descendant nodes, calling callback
|
||||
* for each at-rule node.
|
||||
*
|
||||
* If you pass a filter, iteration will only happen over at-rules
|
||||
* that have matching names.
|
||||
*
|
||||
* Like `Container#each`, this method is safe
|
||||
* to use if you are mutating arrays during iteration.
|
||||
*
|
||||
* ```js
|
||||
* root.walkAtRules(rule => {
|
||||
* if (isOld(rule.name)) rule.remove()
|
||||
* })
|
||||
*
|
||||
* let first = false
|
||||
* root.walkAtRules('charset', rule => {
|
||||
* if (!first) {
|
||||
* first = true
|
||||
* } else {
|
||||
* rule.remove()
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param name String or regular expression to filter at-rules by name.
|
||||
* @param callback Iterator receives each node and index.
|
||||
* @return Returns `false` if iteration was broke.
|
||||
*/
|
||||
walkAtRules(
|
||||
nameFilter: RegExp | string,
|
||||
callback: (atRule: AtRule, index: number) => false | void
|
||||
): false | undefined
|
||||
|
||||
walkAtRules(
|
||||
callback: (atRule: AtRule, index: number) => false | void
|
||||
): false | undefined
|
||||
walkComments(
|
||||
callback: (comment: Comment, indexed: number) => false | void
|
||||
): false | undefined
|
||||
|
||||
walkComments(
|
||||
callback: (comment: Comment, indexed: number) => false | void
|
||||
): false | undefined
|
||||
|
||||
/**
|
||||
* Traverses the container’s descendant nodes, calling callback
|
||||
* for each declaration node.
|
||||
*
|
||||
* If you pass a filter, iteration will only happen over declarations
|
||||
* with matching properties.
|
||||
*
|
||||
* ```js
|
||||
* root.walkDecls(decl => {
|
||||
* checkPropertySupport(decl.prop)
|
||||
* })
|
||||
*
|
||||
* root.walkDecls('border-radius', decl => {
|
||||
* decl.remove()
|
||||
* })
|
||||
*
|
||||
* root.walkDecls(/^background/, decl => {
|
||||
* decl.value = takeFirstColorFromGradient(decl.value)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Like `Container#each`, this method is safe
|
||||
* to use if you are mutating arrays during iteration.
|
||||
*
|
||||
* @param prop String or regular expression to filter declarations
|
||||
* by property name.
|
||||
* @param callback Iterator receives each node and index.
|
||||
* @return Returns `false` if iteration was broke.
|
||||
*/
|
||||
walkDecls(
|
||||
propFilter: RegExp | string,
|
||||
callback: (decl: Declaration, index: number) => false | void
|
||||
): false | undefined
|
||||
|
||||
walkDecls(
|
||||
callback: (decl: Declaration, index: number) => false | void
|
||||
): false | undefined
|
||||
|
||||
/**
|
||||
* Traverses the container’s descendant nodes, calling callback
|
||||
* for each rule node.
|
||||
*
|
||||
* If you pass a filter, iteration will only happen over rules
|
||||
* with matching selectors.
|
||||
*
|
||||
* Like `Container#each`, this method is safe
|
||||
* to use if you are mutating arrays during iteration.
|
||||
*
|
||||
* ```js
|
||||
* const selectors = []
|
||||
* root.walkRules(rule => {
|
||||
* selectors.push(rule.selector)
|
||||
* })
|
||||
* console.log(`Your CSS uses ${ selectors.length } selectors`)
|
||||
* ```
|
||||
*
|
||||
* @param selector String or regular expression to filter rules by selector.
|
||||
* @param callback Iterator receives each node and index.
|
||||
* @return Returns `false` if iteration was broke.
|
||||
*/
|
||||
walkRules(
|
||||
selectorFilter: RegExp | string,
|
||||
callback: (rule: Rule, index: number) => false | void
|
||||
): false | undefined
|
||||
walkRules(
|
||||
callback: (rule: Rule, index: number) => false | void
|
||||
): false | undefined
|
||||
/**
|
||||
* The container’s first child.
|
||||
*
|
||||
* ```js
|
||||
* rule.first === rules.nodes[0]
|
||||
* ```
|
||||
*/
|
||||
get first(): Child | undefined
|
||||
/**
|
||||
* The container’s last child.
|
||||
*
|
||||
* ```js
|
||||
* rule.last === rule.nodes[rule.nodes.length - 1]
|
||||
* ```
|
||||
*/
|
||||
get last(): Child | undefined
|
||||
}
|
||||
|
||||
declare class Container<
|
||||
Child extends Node = ChildNode
|
||||
> extends Container_<Child> {}
|
||||
|
||||
export = Container
|
||||
248
src/postcss/css-syntax-error.d.ts
vendored
Normal file
248
src/postcss/css-syntax-error.d.ts
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
import { FilePosition } from './input.js'
|
||||
|
||||
declare namespace CssSyntaxError {
|
||||
/**
|
||||
* A position that is part of a range.
|
||||
*/
|
||||
export interface RangePosition {
|
||||
/**
|
||||
* The column number in the input.
|
||||
*/
|
||||
column: number
|
||||
|
||||
/**
|
||||
* The line number in the input.
|
||||
*/
|
||||
line: number
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { CssSyntaxError_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* The CSS parser throws this error for broken CSS.
|
||||
*
|
||||
* Custom parsers can throw this error for broken custom syntax using
|
||||
* the `Node#error` method.
|
||||
*
|
||||
* PostCSS will use the input source map to detect the original error location.
|
||||
* If you wrote a Sass file, compiled it to CSS and then parsed it with PostCSS,
|
||||
* PostCSS will show the original position in the Sass file.
|
||||
*
|
||||
* If you need the position in the PostCSS input
|
||||
* (e.g., to debug the previous compiler), use `error.input.file`.
|
||||
*
|
||||
* ```js
|
||||
* // Raising error from plugin
|
||||
* throw node.error('Unknown variable', { plugin: 'postcss-vars' })
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* // Catching and checking syntax error
|
||||
* try {
|
||||
* postcss.parse('a{')
|
||||
* } catch (error) {
|
||||
* if (error.name === 'CssSyntaxError') {
|
||||
* error //=> CssSyntaxError
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
declare class CssSyntaxError_ extends Error {
|
||||
/**
|
||||
* Source column of the error.
|
||||
*
|
||||
* ```js
|
||||
* error.column //=> 1
|
||||
* error.input.column //=> 4
|
||||
* ```
|
||||
*
|
||||
* PostCSS will use the input source map to detect the original location.
|
||||
* If you need the position in the PostCSS input, use `error.input.column`.
|
||||
*/
|
||||
column?: number
|
||||
|
||||
/**
|
||||
* Source column of the error's end, exclusive. Provided if the error pertains
|
||||
* to a range.
|
||||
*
|
||||
* ```js
|
||||
* error.endColumn //=> 1
|
||||
* error.input.endColumn //=> 4
|
||||
* ```
|
||||
*
|
||||
* PostCSS will use the input source map to detect the original location.
|
||||
* If you need the position in the PostCSS input, use `error.input.endColumn`.
|
||||
*/
|
||||
endColumn?: number
|
||||
|
||||
/**
|
||||
* Source line of the error's end, exclusive. Provided if the error pertains
|
||||
* to a range.
|
||||
*
|
||||
* ```js
|
||||
* error.endLine //=> 3
|
||||
* error.input.endLine //=> 4
|
||||
* ```
|
||||
*
|
||||
* PostCSS will use the input source map to detect the original location.
|
||||
* If you need the position in the PostCSS input, use `error.input.endLine`.
|
||||
*/
|
||||
endLine?: number
|
||||
|
||||
/**
|
||||
* Absolute path to the broken file.
|
||||
*
|
||||
* ```js
|
||||
* error.file //=> 'a.sass'
|
||||
* error.input.file //=> 'a.css'
|
||||
* ```
|
||||
*
|
||||
* PostCSS will use the input source map to detect the original location.
|
||||
* If you need the position in the PostCSS input, use `error.input.file`.
|
||||
*/
|
||||
file?: string
|
||||
|
||||
/**
|
||||
* Input object with PostCSS internal information
|
||||
* about input file. If input has source map
|
||||
* from previous tool, PostCSS will use origin
|
||||
* (for example, Sass) source. You can use this
|
||||
* object to get PostCSS input source.
|
||||
*
|
||||
* ```js
|
||||
* error.input.file //=> 'a.css'
|
||||
* error.file //=> 'a.sass'
|
||||
* ```
|
||||
*/
|
||||
input?: FilePosition
|
||||
|
||||
/**
|
||||
* Source line of the error.
|
||||
*
|
||||
* ```js
|
||||
* error.line //=> 2
|
||||
* error.input.line //=> 4
|
||||
* ```
|
||||
*
|
||||
* PostCSS will use the input source map to detect the original location.
|
||||
* If you need the position in the PostCSS input, use `error.input.line`.
|
||||
*/
|
||||
line?: number
|
||||
|
||||
/**
|
||||
* Full error text in the GNU error format
|
||||
* with plugin, file, line and column.
|
||||
*
|
||||
* ```js
|
||||
* error.message //=> 'a.css:1:1: Unclosed block'
|
||||
* ```
|
||||
*/
|
||||
message: string
|
||||
|
||||
/**
|
||||
* Always equal to `'CssSyntaxError'`. You should always check error type
|
||||
* by `error.name === 'CssSyntaxError'`
|
||||
* instead of `error instanceof CssSyntaxError`,
|
||||
* because npm could have several PostCSS versions.
|
||||
*
|
||||
* ```js
|
||||
* if (error.name === 'CssSyntaxError') {
|
||||
* error //=> CssSyntaxError
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
name: 'CssSyntaxError'
|
||||
|
||||
/**
|
||||
* Plugin name, if error came from plugin.
|
||||
*
|
||||
* ```js
|
||||
* error.plugin //=> 'postcss-vars'
|
||||
* ```
|
||||
*/
|
||||
plugin?: string
|
||||
|
||||
/**
|
||||
* Error message.
|
||||
*
|
||||
* ```js
|
||||
* error.message //=> 'Unclosed block'
|
||||
* ```
|
||||
*/
|
||||
reason: string
|
||||
|
||||
/**
|
||||
* Source code of the broken file.
|
||||
*
|
||||
* ```js
|
||||
* error.source //=> 'a { b {} }'
|
||||
* error.input.source //=> 'a b { }'
|
||||
* ```
|
||||
*/
|
||||
source?: string
|
||||
|
||||
stack: string
|
||||
|
||||
/**
|
||||
* Instantiates a CSS syntax error. Can be instantiated for a single position
|
||||
* or for a range.
|
||||
* @param message Error message.
|
||||
* @param lineOrStartPos If for a single position, the line number, or if for
|
||||
* a range, the inclusive start position of the error.
|
||||
* @param columnOrEndPos If for a single position, the column number, or if for
|
||||
* a range, the exclusive end position of the error.
|
||||
* @param source Source code of the broken file.
|
||||
* @param file Absolute path to the broken file.
|
||||
* @param plugin PostCSS plugin name, if error came from plugin.
|
||||
*/
|
||||
constructor(
|
||||
message: string,
|
||||
lineOrStartPos?: CssSyntaxError.RangePosition | number,
|
||||
columnOrEndPos?: CssSyntaxError.RangePosition | number,
|
||||
source?: string,
|
||||
file?: string,
|
||||
plugin?: string
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a few lines of CSS source that caused the error.
|
||||
*
|
||||
* If the CSS has an input source map without `sourceContent`,
|
||||
* this method will return an empty string.
|
||||
*
|
||||
* ```js
|
||||
* error.showSourceCode() //=> " 4 | }
|
||||
* // 5 | a {
|
||||
* // > 6 | bad
|
||||
* // | ^
|
||||
* // 7 | }
|
||||
* // 8 | b {"
|
||||
* ```
|
||||
*
|
||||
* @param color Whether arrow will be colored red by terminal
|
||||
* color codes. By default, PostCSS will detect
|
||||
* color support by `process.stdout.isTTY`
|
||||
* and `process.env.NODE_DISABLE_COLORS`.
|
||||
* @return Few lines of CSS source that caused the error.
|
||||
*/
|
||||
showSourceCode(color?: boolean): string
|
||||
|
||||
/**
|
||||
* Returns error position, message and source code of the broken part.
|
||||
*
|
||||
* ```js
|
||||
* error.toString() //=> "CssSyntaxError: app.css:1:1: Unclosed block
|
||||
* // > 1 | a {
|
||||
* // | ^"
|
||||
* ```
|
||||
*
|
||||
* @return Error position, message and source code.
|
||||
*/
|
||||
toString(): string
|
||||
}
|
||||
|
||||
declare class CssSyntaxError extends CssSyntaxError_ {}
|
||||
|
||||
export = CssSyntaxError
|
||||
152
src/postcss/declaration.d.ts
vendored
Normal file
152
src/postcss/declaration.d.ts
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
import { ContainerWithChildren } from './container.js'
|
||||
import Node from './node.js'
|
||||
|
||||
declare namespace Declaration {
|
||||
export interface DeclarationRaws extends Record<string, unknown> {
|
||||
/**
|
||||
* The space symbols before the node. It also stores `*`
|
||||
* and `_` symbols before the declaration (IE hack).
|
||||
*/
|
||||
before?: string
|
||||
|
||||
/**
|
||||
* The symbols between the property and value for declarations.
|
||||
*/
|
||||
between?: string
|
||||
|
||||
/**
|
||||
* The content of the important statement, if it is not just `!important`.
|
||||
*/
|
||||
important?: string
|
||||
|
||||
/**
|
||||
* Declaration value with comments.
|
||||
*/
|
||||
value?: {
|
||||
raw: string
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeclarationProps {
|
||||
/** Whether the declaration has an `!important` annotation. */
|
||||
important?: boolean
|
||||
/** Name of the declaration. */
|
||||
prop: string
|
||||
/** Information used to generate byte-to-byte equal node string as it was in the origin input. */
|
||||
raws?: DeclarationRaws
|
||||
/** Value of the declaration. */
|
||||
value: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Declaration_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* It represents a class that handles
|
||||
* [CSS declarations](https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax#css_declarations)
|
||||
*
|
||||
* ```js
|
||||
* Once (root, { Declaration }) {
|
||||
* const color = new Declaration({ prop: 'color', value: 'black' })
|
||||
* root.append(color)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a { color: black }')
|
||||
* const decl = root.first?.first
|
||||
*
|
||||
* decl.type //=> 'decl'
|
||||
* decl.toString() //=> ' color: black'
|
||||
* ```
|
||||
*/
|
||||
declare class Declaration_ extends Node {
|
||||
/**
|
||||
* It represents a specificity of the declaration.
|
||||
*
|
||||
* If true, the CSS declaration will have an
|
||||
* [important](https://developer.mozilla.org/en-US/docs/Web/CSS/important)
|
||||
* specifier.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a { color: black !important; color: red }')
|
||||
*
|
||||
* root.first.first.important //=> true
|
||||
* root.first.last.important //=> undefined
|
||||
* ```
|
||||
*/
|
||||
get important(): boolean
|
||||
set important(value: boolean)
|
||||
|
||||
parent: ContainerWithChildren | undefined
|
||||
|
||||
/**
|
||||
* The property name for a CSS declaration.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a { color: black }')
|
||||
* const decl = root.first.first
|
||||
*
|
||||
* decl.prop //=> 'color'
|
||||
* ```
|
||||
*/
|
||||
get prop(): string
|
||||
set prop(value: string)
|
||||
|
||||
raws: Declaration.DeclarationRaws
|
||||
|
||||
type: 'decl'
|
||||
|
||||
/**
|
||||
* The property value for a CSS declaration.
|
||||
*
|
||||
* Any CSS comments inside the value string will be filtered out.
|
||||
* CSS comments present in the source value will be available in
|
||||
* the `raws` property.
|
||||
*
|
||||
* Assigning new `value` would ignore the comments in `raws`
|
||||
* property while compiling node to string.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a { color: black }')
|
||||
* const decl = root.first.first
|
||||
*
|
||||
* decl.value //=> 'black'
|
||||
* ```
|
||||
*/
|
||||
get value(): string
|
||||
set value(value: string)
|
||||
|
||||
/**
|
||||
* It represents a getter that returns `true` if a declaration starts with
|
||||
* `--` or `$`, which are used to declare variables in CSS and SASS/SCSS.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse(':root { --one: 1 }')
|
||||
* const one = root.first.first
|
||||
*
|
||||
* one.variable //=> true
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('$one: 1')
|
||||
* const one = root.first
|
||||
*
|
||||
* one.variable //=> true
|
||||
* ```
|
||||
*/
|
||||
get variable(): boolean
|
||||
set varaible(value: string)
|
||||
|
||||
constructor(defaults?: Declaration.DeclarationProps)
|
||||
assign(overrides: Declaration.DeclarationProps | object): this
|
||||
clone(overrides?: Partial<Declaration.DeclarationProps>): Declaration
|
||||
cloneAfter(overrides?: Partial<Declaration.DeclarationProps>): Declaration
|
||||
cloneBefore(overrides?: Partial<Declaration.DeclarationProps>): Declaration
|
||||
}
|
||||
|
||||
declare class Declaration extends Declaration_ {}
|
||||
|
||||
export = Declaration
|
||||
69
src/postcss/document.d.ts
vendored
Normal file
69
src/postcss/document.d.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
import Container, { ContainerProps } from './container.js'
|
||||
import { ProcessOptions } from './postcss.js'
|
||||
import Result from './result.js'
|
||||
import Root from './root.js'
|
||||
|
||||
declare namespace Document {
|
||||
export interface DocumentProps extends ContainerProps {
|
||||
nodes?: Root[]
|
||||
|
||||
/**
|
||||
* Information to generate byte-to-byte equal node string as it was
|
||||
* in the origin input.
|
||||
*
|
||||
* Every parser saves its own properties.
|
||||
*/
|
||||
raws?: Record<string, any>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Document_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a file and contains all its parsed nodes.
|
||||
*
|
||||
* **Experimental:** some aspects of this node could change within minor
|
||||
* or patch version releases.
|
||||
*
|
||||
* ```js
|
||||
* const document = htmlParser(
|
||||
* '<html><style>a{color:black}</style><style>b{z-index:2}</style>'
|
||||
* )
|
||||
* document.type //=> 'document'
|
||||
* document.nodes.length //=> 2
|
||||
* ```
|
||||
*/
|
||||
declare class Document_ extends Container<Root> {
|
||||
nodes: Root[]
|
||||
parent: undefined
|
||||
type: 'document'
|
||||
|
||||
constructor(defaults?: Document.DocumentProps)
|
||||
|
||||
assign(overrides: Document.DocumentProps | object): this
|
||||
clone(overrides?: Partial<Document.DocumentProps>): Document
|
||||
cloneAfter(overrides?: Partial<Document.DocumentProps>): Document
|
||||
cloneBefore(overrides?: Partial<Document.DocumentProps>): Document
|
||||
|
||||
/**
|
||||
* Returns a `Result` instance representing the document’s CSS roots.
|
||||
*
|
||||
* ```js
|
||||
* const root1 = postcss.parse(css1, { from: 'a.css' })
|
||||
* const root2 = postcss.parse(css2, { from: 'b.css' })
|
||||
* const document = postcss.document()
|
||||
* document.append(root1)
|
||||
* document.append(root2)
|
||||
* const result = document.toResult({ to: 'all.css', map: true })
|
||||
* ```
|
||||
*
|
||||
* @param opts Options.
|
||||
* @return Result with current document’s CSS.
|
||||
*/
|
||||
toResult(options?: ProcessOptions): Result
|
||||
}
|
||||
|
||||
declare class Document extends Document_ {}
|
||||
|
||||
export = Document
|
||||
9
src/postcss/fromJSON.d.ts
vendored
Normal file
9
src/postcss/fromJSON.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import { JSONHydrator } from './postcss.js'
|
||||
|
||||
interface FromJSON extends JSONHydrator {
|
||||
default: FromJSON
|
||||
}
|
||||
|
||||
declare const fromJSON: FromJSON
|
||||
|
||||
export = fromJSON
|
||||
194
src/postcss/input.d.ts
vendored
Normal file
194
src/postcss/input.d.ts
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
import { CssSyntaxError, ProcessOptions } from './postcss.js'
|
||||
import PreviousMap from './previous-map.js'
|
||||
|
||||
declare namespace Input {
|
||||
export interface FilePosition {
|
||||
/**
|
||||
* Column of inclusive start position in source file.
|
||||
*/
|
||||
column: number
|
||||
|
||||
/**
|
||||
* Column of exclusive end position in source file.
|
||||
*/
|
||||
endColumn?: number
|
||||
|
||||
/**
|
||||
* Line of exclusive end position in source file.
|
||||
*/
|
||||
endLine?: number
|
||||
|
||||
/**
|
||||
* Absolute path to the source file.
|
||||
*/
|
||||
file?: string
|
||||
|
||||
/**
|
||||
* Line of inclusive start position in source file.
|
||||
*/
|
||||
line: number
|
||||
|
||||
/**
|
||||
* Source code.
|
||||
*/
|
||||
source?: string
|
||||
|
||||
/**
|
||||
* URL for the source file.
|
||||
*/
|
||||
url: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Input_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the source CSS.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse(css, { from: file })
|
||||
* const input = root.source.input
|
||||
* ```
|
||||
*/
|
||||
declare class Input_ {
|
||||
/**
|
||||
* Input CSS source.
|
||||
*
|
||||
* ```js
|
||||
* const input = postcss.parse('a{}', { from: file }).input
|
||||
* input.css //=> "a{}"
|
||||
* ```
|
||||
*/
|
||||
css: string
|
||||
|
||||
/**
|
||||
* The absolute path to the CSS source file defined
|
||||
* with the `from` option.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse(css, { from: 'a.css' })
|
||||
* root.source.input.file //=> '/home/ai/a.css'
|
||||
* ```
|
||||
*/
|
||||
file?: string
|
||||
|
||||
/**
|
||||
* The flag to indicate whether or not the source code has Unicode BOM.
|
||||
*/
|
||||
hasBOM: boolean
|
||||
|
||||
/**
|
||||
* The unique ID of the CSS source. It will be created if `from` option
|
||||
* is not provided (because PostCSS does not know the file path).
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse(css)
|
||||
* root.source.input.file //=> undefined
|
||||
* root.source.input.id //=> "<input css 8LZeVF>"
|
||||
* ```
|
||||
*/
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* The input source map passed from a compilation step before PostCSS
|
||||
* (for example, from Sass compiler).
|
||||
*
|
||||
* ```js
|
||||
* root.source.input.map.consumer().sources //=> ['a.sass']
|
||||
* ```
|
||||
*/
|
||||
map: PreviousMap
|
||||
|
||||
/**
|
||||
* @param css Input CSS source.
|
||||
* @param opts Process options.
|
||||
*/
|
||||
constructor(css: string, opts?: ProcessOptions)
|
||||
|
||||
error(
|
||||
message: string,
|
||||
start:
|
||||
| {
|
||||
column: number
|
||||
line: number
|
||||
}
|
||||
| {
|
||||
offset: number
|
||||
},
|
||||
end:
|
||||
| {
|
||||
column: number
|
||||
line: number
|
||||
}
|
||||
| {
|
||||
offset: number
|
||||
},
|
||||
opts?: { plugin?: CssSyntaxError['plugin'] }
|
||||
): CssSyntaxError
|
||||
|
||||
/**
|
||||
* Returns `CssSyntaxError` with information about the error and its position.
|
||||
*/
|
||||
error(
|
||||
message: string,
|
||||
line: number,
|
||||
column: number,
|
||||
opts?: { plugin?: CssSyntaxError['plugin'] }
|
||||
): CssSyntaxError
|
||||
|
||||
error(
|
||||
message: string,
|
||||
offset: number,
|
||||
opts?: { plugin?: CssSyntaxError['plugin'] }
|
||||
): CssSyntaxError
|
||||
|
||||
/**
|
||||
* Converts source offset to line and column.
|
||||
*
|
||||
* @param offset Source offset.
|
||||
*/
|
||||
fromOffset(offset: number): { col: number; line: number } | null
|
||||
/**
|
||||
* Reads the input source map and returns a symbol position
|
||||
* in the input source (e.g., in a Sass file that was compiled
|
||||
* to CSS before being passed to PostCSS). Optionally takes an
|
||||
* end position, exclusive.
|
||||
*
|
||||
* ```js
|
||||
* root.source.input.origin(1, 1) //=> { file: 'a.css', line: 3, column: 1 }
|
||||
* root.source.input.origin(1, 1, 1, 4)
|
||||
* //=> { file: 'a.css', line: 3, column: 1, endLine: 3, endColumn: 4 }
|
||||
* ```
|
||||
*
|
||||
* @param line Line for inclusive start position in input CSS.
|
||||
* @param column Column for inclusive start position in input CSS.
|
||||
* @param endLine Line for exclusive end position in input CSS.
|
||||
* @param endColumn Column for exclusive end position in input CSS.
|
||||
*
|
||||
* @return Position in input source.
|
||||
*/
|
||||
origin(
|
||||
line: number,
|
||||
column: number,
|
||||
endLine?: number,
|
||||
endColumn?: number
|
||||
): false | Input.FilePosition
|
||||
/**
|
||||
* The CSS source identifier. Contains `Input#file` if the user
|
||||
* set the `from` option, or `Input#id` if they did not.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse(css, { from: 'a.css' })
|
||||
* root.source.input.from //=> "/home/ai/a.css"
|
||||
*
|
||||
* const root = postcss.parse(css)
|
||||
* root.source.input.from //=> "<input css 1>"
|
||||
* ```
|
||||
*/
|
||||
get from(): string
|
||||
}
|
||||
|
||||
declare class Input extends Input_ {}
|
||||
|
||||
export = Input
|
||||
190
src/postcss/lazy-result.d.ts
vendored
Normal file
190
src/postcss/lazy-result.d.ts
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
import Document from './document.js'
|
||||
import { SourceMap } from './postcss.js'
|
||||
import Processor from './processor.js'
|
||||
import Result, { Message, ResultOptions } from './result.js'
|
||||
import Root from './root.js'
|
||||
import Warning from './warning.js'
|
||||
|
||||
declare namespace LazyResult {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { LazyResult_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Promise proxy for the result of PostCSS transformations.
|
||||
*
|
||||
* A `LazyResult` instance is returned by `Processor#process`.
|
||||
*
|
||||
* ```js
|
||||
* const lazy = postcss([autoprefixer]).process(css)
|
||||
* ```
|
||||
*/
|
||||
declare class LazyResult_<RootNode = Document | Root>
|
||||
implements PromiseLike<Result<RootNode>>
|
||||
{
|
||||
/**
|
||||
* Processes input CSS through synchronous and asynchronous plugins
|
||||
* and calls onRejected for each error thrown in any plugin.
|
||||
*
|
||||
* It implements standard Promise API.
|
||||
*
|
||||
* ```js
|
||||
* postcss([autoprefixer]).process(css).then(result => {
|
||||
* console.log(result.css)
|
||||
* }).catch(error => {
|
||||
* console.error(error)
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
catch: Promise<Result<RootNode>>['catch']
|
||||
|
||||
/**
|
||||
* Processes input CSS through synchronous and asynchronous plugins
|
||||
* and calls onFinally on any error or when all plugins will finish work.
|
||||
*
|
||||
* It implements standard Promise API.
|
||||
*
|
||||
* ```js
|
||||
* postcss([autoprefixer]).process(css).finally(() => {
|
||||
* console.log('processing ended')
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
finally: Promise<Result<RootNode>>['finally']
|
||||
|
||||
/**
|
||||
* Processes input CSS through synchronous and asynchronous plugins
|
||||
* and calls `onFulfilled` with a Result instance. If a plugin throws
|
||||
* an error, the `onRejected` callback will be executed.
|
||||
*
|
||||
* It implements standard Promise API.
|
||||
*
|
||||
* ```js
|
||||
* postcss([autoprefixer]).process(css, { from: cssPath }).then(result => {
|
||||
* console.log(result.css)
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
then: Promise<Result<RootNode>>['then']
|
||||
|
||||
/**
|
||||
* @param processor Processor used for this transformation.
|
||||
* @param css CSS to parse and transform.
|
||||
* @param opts Options from the `Processor#process` or `Root#toResult`.
|
||||
*/
|
||||
constructor(processor: Processor, css: string, opts: ResultOptions)
|
||||
|
||||
/**
|
||||
* Run plugin in async way and return `Result`.
|
||||
*
|
||||
* @return Result with output content.
|
||||
*/
|
||||
async(): Promise<Result<RootNode>>
|
||||
|
||||
/**
|
||||
* Run plugin in sync way and return `Result`.
|
||||
*
|
||||
* @return Result with output content.
|
||||
*/
|
||||
sync(): Result<RootNode>
|
||||
|
||||
/**
|
||||
* Alias for the `LazyResult#css` property.
|
||||
*
|
||||
* ```js
|
||||
* lazy + '' === lazy.css
|
||||
* ```
|
||||
*
|
||||
* @return Output CSS.
|
||||
*/
|
||||
toString(): string
|
||||
|
||||
/**
|
||||
* Processes input CSS through synchronous plugins
|
||||
* and calls `Result#warnings`.
|
||||
*
|
||||
* @return Warnings from plugins.
|
||||
*/
|
||||
warnings(): Warning[]
|
||||
|
||||
/**
|
||||
* An alias for the `css` property. Use it with syntaxes
|
||||
* that generate non-CSS output.
|
||||
*
|
||||
* This property will only work with synchronous plugins.
|
||||
* If the processor contains any asynchronous plugins
|
||||
* it will throw an error.
|
||||
*
|
||||
* PostCSS runners should always use `LazyResult#then`.
|
||||
*/
|
||||
get content(): string
|
||||
|
||||
/**
|
||||
* Processes input CSS through synchronous plugins, converts `Root`
|
||||
* to a CSS string and returns `Result#css`.
|
||||
*
|
||||
* This property will only work with synchronous plugins.
|
||||
* If the processor contains any asynchronous plugins
|
||||
* it will throw an error.
|
||||
*
|
||||
* PostCSS runners should always use `LazyResult#then`.
|
||||
*/
|
||||
get css(): string
|
||||
|
||||
/**
|
||||
* Processes input CSS through synchronous plugins
|
||||
* and returns `Result#map`.
|
||||
*
|
||||
* This property will only work with synchronous plugins.
|
||||
* If the processor contains any asynchronous plugins
|
||||
* it will throw an error.
|
||||
*
|
||||
* PostCSS runners should always use `LazyResult#then`.
|
||||
*/
|
||||
get map(): SourceMap
|
||||
|
||||
/**
|
||||
* Processes input CSS through synchronous plugins
|
||||
* and returns `Result#messages`.
|
||||
*
|
||||
* This property will only work with synchronous plugins. If the processor
|
||||
* contains any asynchronous plugins it will throw an error.
|
||||
*
|
||||
* PostCSS runners should always use `LazyResult#then`.
|
||||
*/
|
||||
get messages(): Message[]
|
||||
|
||||
/**
|
||||
* Options from the `Processor#process` call.
|
||||
*/
|
||||
get opts(): ResultOptions
|
||||
|
||||
/**
|
||||
* Returns a `Processor` instance, which will be used
|
||||
* for CSS transformations.
|
||||
*/
|
||||
get processor(): Processor
|
||||
|
||||
/**
|
||||
* Processes input CSS through synchronous plugins
|
||||
* and returns `Result#root`.
|
||||
*
|
||||
* This property will only work with synchronous plugins. If the processor
|
||||
* contains any asynchronous plugins it will throw an error.
|
||||
*
|
||||
* PostCSS runners should always use `LazyResult#then`.
|
||||
*/
|
||||
get root(): RootNode
|
||||
|
||||
/**
|
||||
* Returns the default string description of an object.
|
||||
* Required to implement the Promise interface.
|
||||
*/
|
||||
get [Symbol.toStringTag](): string
|
||||
}
|
||||
|
||||
declare class LazyResult<
|
||||
RootNode = Document | Root
|
||||
> extends LazyResult_<RootNode> {}
|
||||
|
||||
export = LazyResult
|
||||
57
src/postcss/list.d.ts
vendored
Normal file
57
src/postcss/list.d.ts
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
declare namespace list {
|
||||
type List = {
|
||||
/**
|
||||
* Safely splits comma-separated values (such as those for `transition-*`
|
||||
* and `background` properties).
|
||||
*
|
||||
* ```js
|
||||
* Once (root, { list }) {
|
||||
* list.comma('black, linear-gradient(white, black)')
|
||||
* //=> ['black', 'linear-gradient(white, black)']
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param str Comma-separated values.
|
||||
* @return Split values.
|
||||
*/
|
||||
comma(str: string): string[]
|
||||
|
||||
default: List
|
||||
|
||||
/**
|
||||
* Safely splits space-separated values (such as those for `background`,
|
||||
* `border-radius`, and other shorthand properties).
|
||||
*
|
||||
* ```js
|
||||
* Once (root, { list }) {
|
||||
* list.space('1px calc(10% + 1px)') //=> ['1px', 'calc(10% + 1px)']
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param str Space-separated values.
|
||||
* @return Split values.
|
||||
*/
|
||||
space(str: string): string[]
|
||||
|
||||
/**
|
||||
* Safely splits values.
|
||||
*
|
||||
* ```js
|
||||
* Once (root, { list }) {
|
||||
* list.split('1px calc(10% + 1px)', [' ', '\n', '\t']) //=> ['1px', 'calc(10% + 1px)']
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param string separated values.
|
||||
* @param separators array of separators.
|
||||
* @param last boolean indicator.
|
||||
* @return Split values.
|
||||
*/
|
||||
split(string: string, separators: string[], last: boolean): string[]
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
declare const list: list.List
|
||||
|
||||
export = list
|
||||
46
src/postcss/no-work-result.d.ts
vendored
Normal file
46
src/postcss/no-work-result.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import LazyResult from './lazy-result.js'
|
||||
import { SourceMap } from './postcss.js'
|
||||
import Processor from './processor.js'
|
||||
import Result, { Message, ResultOptions } from './result.js'
|
||||
import Root from './root.js'
|
||||
import Warning from './warning.js'
|
||||
|
||||
declare namespace NoWorkResult {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { NoWorkResult_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Promise proxy for the result of PostCSS transformations.
|
||||
* This lazy result instance doesn't parse css unless `NoWorkResult#root` or `Result#root`
|
||||
* are accessed. See the example below for details.
|
||||
* A `NoWork` instance is returned by `Processor#process` ONLY when no plugins defined.
|
||||
*
|
||||
* ```js
|
||||
* const noWorkResult = postcss().process(css) // No plugins are defined.
|
||||
* // CSS is not parsed
|
||||
* let root = noWorkResult.root // now css is parsed because we accessed the root
|
||||
* ```
|
||||
*/
|
||||
declare class NoWorkResult_ implements LazyResult<Root> {
|
||||
catch: Promise<Result<Root>>['catch']
|
||||
finally: Promise<Result<Root>>['finally']
|
||||
then: Promise<Result<Root>>['then']
|
||||
constructor(processor: Processor, css: string, opts: ResultOptions)
|
||||
async(): Promise<Result<Root>>
|
||||
sync(): Result<Root>
|
||||
toString(): string
|
||||
warnings(): Warning[]
|
||||
get content(): string
|
||||
get css(): string
|
||||
get map(): SourceMap
|
||||
get messages(): Message[]
|
||||
get opts(): ResultOptions
|
||||
get processor(): Processor
|
||||
get root(): Root
|
||||
get [Symbol.toStringTag](): string
|
||||
}
|
||||
|
||||
declare class NoWorkResult extends NoWorkResult_ {}
|
||||
|
||||
export = NoWorkResult
|
||||
536
src/postcss/node.d.ts
vendored
Normal file
536
src/postcss/node.d.ts
vendored
Normal file
@@ -0,0 +1,536 @@
|
||||
import AtRule = require('./at-rule.js')
|
||||
|
||||
import { AtRuleProps } from './at-rule.js'
|
||||
import Comment, { CommentProps } from './comment.js'
|
||||
import Container from './container.js'
|
||||
import CssSyntaxError from './css-syntax-error.js'
|
||||
import Declaration, { DeclarationProps } from './declaration.js'
|
||||
import Document from './document.js'
|
||||
import Input from './input.js'
|
||||
import { Stringifier, Syntax } from './postcss.js'
|
||||
import Result from './result.js'
|
||||
import Root from './root.js'
|
||||
import Rule, { RuleProps } from './rule.js'
|
||||
import Warning, { WarningOptions } from './warning.js'
|
||||
|
||||
declare namespace Node {
|
||||
export type ChildNode = AtRule.default | Comment | Declaration | Rule
|
||||
|
||||
export type AnyNode =
|
||||
| AtRule.default
|
||||
| Comment
|
||||
| Declaration
|
||||
| Document
|
||||
| Root
|
||||
| Rule
|
||||
|
||||
export type ChildProps =
|
||||
| AtRuleProps
|
||||
| CommentProps
|
||||
| DeclarationProps
|
||||
| RuleProps
|
||||
|
||||
export interface Position {
|
||||
/**
|
||||
* Source line in file. In contrast to `offset` it starts from 1.
|
||||
*/
|
||||
column: number
|
||||
|
||||
/**
|
||||
* Source column in file.
|
||||
*/
|
||||
line: number
|
||||
|
||||
/**
|
||||
* Source offset in file. It starts from 0.
|
||||
*/
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
/**
|
||||
* End position, exclusive.
|
||||
*/
|
||||
end: Position
|
||||
|
||||
/**
|
||||
* Start position, inclusive.
|
||||
*/
|
||||
start: Position
|
||||
}
|
||||
|
||||
/**
|
||||
* Source represents an interface for the {@link Node.source} property.
|
||||
*/
|
||||
export interface Source {
|
||||
/**
|
||||
* The inclusive ending position for the source
|
||||
* code of a node.
|
||||
*/
|
||||
end?: Position
|
||||
|
||||
/**
|
||||
* The source file from where a node has originated.
|
||||
*/
|
||||
input: Input
|
||||
|
||||
/**
|
||||
* The inclusive starting position for the source
|
||||
* code of a node.
|
||||
*/
|
||||
start?: Position
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface represents an interface for an object received
|
||||
* as parameter by Node class constructor.
|
||||
*/
|
||||
export interface NodeProps {
|
||||
source?: Source
|
||||
}
|
||||
|
||||
export interface NodeErrorOptions {
|
||||
/**
|
||||
* An ending index inside a node's string that should be highlighted as
|
||||
* source of error.
|
||||
*/
|
||||
endIndex?: number
|
||||
/**
|
||||
* An index inside a node's string that should be highlighted as source
|
||||
* of error.
|
||||
*/
|
||||
index?: number
|
||||
/**
|
||||
* Plugin name that created this error. PostCSS will set it automatically.
|
||||
*/
|
||||
plugin?: string
|
||||
/**
|
||||
* A word inside a node's string, that should be highlighted as source
|
||||
* of error.
|
||||
*/
|
||||
word?: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
class Node extends Node_ {}
|
||||
export { Node as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* It represents an abstract class that handles common
|
||||
* methods for other CSS abstract syntax tree nodes.
|
||||
*
|
||||
* Any node that represents CSS selector or value should
|
||||
* not extend the `Node` class.
|
||||
*/
|
||||
declare abstract class Node_ {
|
||||
/**
|
||||
* It represents parent of the current node.
|
||||
*
|
||||
* ```js
|
||||
* root.nodes[0].parent === root //=> true
|
||||
* ```
|
||||
*/
|
||||
parent: Container | Document | undefined
|
||||
|
||||
/**
|
||||
* It represents unnecessary whitespace and characters present
|
||||
* in the css source code.
|
||||
*
|
||||
* Information to generate byte-to-byte equal node string as it was
|
||||
* in the origin input.
|
||||
*
|
||||
* The properties of the raws object are decided by parser,
|
||||
* the default parser uses the following properties:
|
||||
*
|
||||
* * `before`: the space symbols before the node. It also stores `*`
|
||||
* and `_` symbols before the declaration (IE hack).
|
||||
* * `after`: the space symbols after the last child of the node
|
||||
* to the end of the node.
|
||||
* * `between`: the symbols between the property and value
|
||||
* for declarations, selector and `{` for rules, or last parameter
|
||||
* and `{` for at-rules.
|
||||
* * `semicolon`: contains true if the last child has
|
||||
* an (optional) semicolon.
|
||||
* * `afterName`: the space between the at-rule name and its parameters.
|
||||
* * `left`: the space symbols between `/*` and the comment’s text.
|
||||
* * `right`: the space symbols between the comment’s text
|
||||
* and <code>*/</code>.
|
||||
* - `important`: the content of the important statement,
|
||||
* if it is not just `!important`.
|
||||
*
|
||||
* PostCSS filters out the comments inside selectors, declaration values
|
||||
* and at-rule parameters but it stores the origin content in raws.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a {\n color:black\n}')
|
||||
* root.first.first.raws //=> { before: '\n ', between: ':' }
|
||||
* ```
|
||||
*/
|
||||
raws: any
|
||||
|
||||
/**
|
||||
* It represents information related to origin of a node and is required
|
||||
* for generating source maps.
|
||||
*
|
||||
* The nodes that are created manually using the public APIs
|
||||
* provided by PostCSS will have `source` undefined and
|
||||
* will be absent in the source map.
|
||||
*
|
||||
* For this reason, the plugin developer should consider
|
||||
* duplicating nodes as the duplicate node will have the
|
||||
* same source as the original node by default or assign
|
||||
* source to a node created manually.
|
||||
*
|
||||
* ```js
|
||||
* decl.source.input.from //=> '/home/ai/source.css'
|
||||
* decl.source.start //=> { line: 10, column: 2 }
|
||||
* decl.source.end //=> { line: 10, column: 12 }
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* // Incorrect method, source not specified!
|
||||
* const prefixed = postcss.decl({
|
||||
* prop: '-moz-' + decl.prop,
|
||||
* value: decl.value
|
||||
* })
|
||||
*
|
||||
* // Correct method, source is inherited when duplicating.
|
||||
* const prefixed = decl.clone({
|
||||
* prop: '-moz-' + decl.prop
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* if (atrule.name === 'add-link') {
|
||||
* const rule = postcss.rule({
|
||||
* selector: 'a',
|
||||
* source: atrule.source
|
||||
* })
|
||||
*
|
||||
* atrule.parent.insertBefore(atrule, rule)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
source?: Node.Source
|
||||
|
||||
/**
|
||||
* It represents type of a node in
|
||||
* an abstract syntax tree.
|
||||
*
|
||||
* A type of node helps in identification of a node
|
||||
* and perform operation based on it's type.
|
||||
*
|
||||
* ```js
|
||||
* const declaration = new Declaration({
|
||||
* prop: 'color',
|
||||
* value: 'black'
|
||||
* })
|
||||
*
|
||||
* declaration.type //=> 'decl'
|
||||
* ```
|
||||
*/
|
||||
type: string
|
||||
|
||||
constructor(defaults?: object)
|
||||
|
||||
/**
|
||||
* Insert new node after current node to current node’s parent.
|
||||
*
|
||||
* Just alias for `node.parent.insertAfter(node, add)`.
|
||||
*
|
||||
* ```js
|
||||
* decl.after('color: black')
|
||||
* ```
|
||||
*
|
||||
* @param newNode New node.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
after(newNode: Node | Node.ChildProps | Node[] | string | undefined): this
|
||||
|
||||
/**
|
||||
* It assigns properties to an existing node instance.
|
||||
*
|
||||
* ```js
|
||||
* decl.assign({ prop: 'word-wrap', value: 'break-word' })
|
||||
* ```
|
||||
*
|
||||
* @param overrides New properties to override the node.
|
||||
*
|
||||
* @return `this` for method chaining.
|
||||
*/
|
||||
assign(overrides: object): this
|
||||
|
||||
/**
|
||||
* Insert new node before current node to current node’s parent.
|
||||
*
|
||||
* Just alias for `node.parent.insertBefore(node, add)`.
|
||||
*
|
||||
* ```js
|
||||
* decl.before('content: ""')
|
||||
* ```
|
||||
*
|
||||
* @param newNode New node.
|
||||
* @return This node for methods chain.
|
||||
*/
|
||||
before(newNode: Node | Node.ChildProps | Node[] | string | undefined): this
|
||||
|
||||
/**
|
||||
* Clear the code style properties for the node and its children.
|
||||
*
|
||||
* ```js
|
||||
* node.raws.before //=> ' '
|
||||
* node.cleanRaws()
|
||||
* node.raws.before //=> undefined
|
||||
* ```
|
||||
*
|
||||
* @param keepBetween Keep the `raws.between` symbols.
|
||||
*/
|
||||
cleanRaws(keepBetween?: boolean): void
|
||||
|
||||
/**
|
||||
* It creates clone of an existing node, which includes all the properties
|
||||
* and their values, that includes `raws` but not `type`.
|
||||
*
|
||||
* ```js
|
||||
* decl.raws.before //=> "\n "
|
||||
* const cloned = decl.clone({ prop: '-moz-' + decl.prop })
|
||||
* cloned.raws.before //=> "\n "
|
||||
* cloned.toString() //=> -moz-transform: scale(0)
|
||||
* ```
|
||||
*
|
||||
* @param overrides New properties to override in the clone.
|
||||
*
|
||||
* @return Duplicate of the node instance.
|
||||
*/
|
||||
clone(overrides?: object): Node
|
||||
|
||||
/**
|
||||
* Shortcut to clone the node and insert the resulting cloned node
|
||||
* after the current node.
|
||||
*
|
||||
* @param overrides New properties to override in the clone.
|
||||
* @return New node.
|
||||
*/
|
||||
cloneAfter(overrides?: object): Node
|
||||
|
||||
/**
|
||||
* Shortcut to clone the node and insert the resulting cloned node
|
||||
* before the current node.
|
||||
*
|
||||
* ```js
|
||||
* decl.cloneBefore({ prop: '-moz-' + decl.prop })
|
||||
* ```
|
||||
*
|
||||
* @param overrides Mew properties to override in the clone.
|
||||
*
|
||||
* @return New node
|
||||
*/
|
||||
cloneBefore(overrides?: object): Node
|
||||
|
||||
/**
|
||||
* It creates an instance of the class `CssSyntaxError` and parameters passed
|
||||
* to this method are assigned to the error instance.
|
||||
*
|
||||
* The error instance will have description for the
|
||||
* error, original position of the node in the
|
||||
* source, showing line and column number.
|
||||
*
|
||||
* If any previous map is present, it would be used
|
||||
* to get original position of the source.
|
||||
*
|
||||
* The Previous Map here is referred to the source map
|
||||
* generated by previous compilation, example: Less,
|
||||
* Stylus and Sass.
|
||||
*
|
||||
* This method returns the error instance instead of
|
||||
* throwing it.
|
||||
*
|
||||
* ```js
|
||||
* if (!variables[name]) {
|
||||
* throw decl.error(`Unknown variable ${name}`, { word: name })
|
||||
* // CssSyntaxError: postcss-vars:a.sass:4:3: Unknown variable $black
|
||||
* // color: $black
|
||||
* // a
|
||||
* // ^
|
||||
* // background: white
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param message Description for the error instance.
|
||||
* @param options Options for the error instance.
|
||||
*
|
||||
* @return Error instance is returned.
|
||||
*/
|
||||
error(message: string, options?: Node.NodeErrorOptions): CssSyntaxError
|
||||
|
||||
/**
|
||||
* Returns the next child of the node’s parent.
|
||||
* Returns `undefined` if the current node is the last child.
|
||||
*
|
||||
* ```js
|
||||
* if (comment.text === 'delete next') {
|
||||
* const next = comment.next()
|
||||
* if (next) {
|
||||
* next.remove()
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return Next node.
|
||||
*/
|
||||
next(): Node.ChildNode | undefined
|
||||
|
||||
/**
|
||||
* Get the position for a word or an index inside the node.
|
||||
*
|
||||
* @param opts Options.
|
||||
* @return Position.
|
||||
*/
|
||||
positionBy(opts?: Pick<WarningOptions, 'index' | 'word'>): Node.Position
|
||||
|
||||
/**
|
||||
* Convert string index to line/column.
|
||||
*
|
||||
* @param index The symbol number in the node’s string.
|
||||
* @return Symbol position in file.
|
||||
*/
|
||||
positionInside(index: number): Node.Position
|
||||
|
||||
/**
|
||||
* Returns the previous child of the node’s parent.
|
||||
* Returns `undefined` if the current node is the first child.
|
||||
*
|
||||
* ```js
|
||||
* const annotation = decl.prev()
|
||||
* if (annotation.type === 'comment') {
|
||||
* readAnnotation(annotation.text)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return Previous node.
|
||||
*/
|
||||
prev(): Node.ChildNode | undefined
|
||||
|
||||
/**
|
||||
* Get the range for a word or start and end index inside the node.
|
||||
* The start index is inclusive; the end index is exclusive.
|
||||
*
|
||||
* @param opts Options.
|
||||
* @return Range.
|
||||
*/
|
||||
rangeBy(
|
||||
opts?: Pick<WarningOptions, 'endIndex' | 'index' | 'word'>
|
||||
): Node.Range
|
||||
|
||||
/**
|
||||
* Returns a `raws` value. If the node is missing
|
||||
* the code style property (because the node was manually built or cloned),
|
||||
* PostCSS will try to autodetect the code style property by looking
|
||||
* at other nodes in the tree.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a { background: white }')
|
||||
* root.nodes[0].append({ prop: 'color', value: 'black' })
|
||||
* root.nodes[0].nodes[1].raws.before //=> undefined
|
||||
* root.nodes[0].nodes[1].raw('before') //=> ' '
|
||||
* ```
|
||||
*
|
||||
* @param prop Name of code style property.
|
||||
* @param defaultType Name of default value, it can be missed
|
||||
* if the value is the same as prop.
|
||||
* @return {string} Code style value.
|
||||
*/
|
||||
raw(prop: string, defaultType?: string): string
|
||||
|
||||
/**
|
||||
* It removes the node from its parent and deletes its parent property.
|
||||
*
|
||||
* ```js
|
||||
* if (decl.prop.match(/^-webkit-/)) {
|
||||
* decl.remove()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return `this` for method chaining.
|
||||
*/
|
||||
remove(): this
|
||||
|
||||
/**
|
||||
* Inserts node(s) before the current node and removes the current node.
|
||||
*
|
||||
* ```js
|
||||
* AtRule: {
|
||||
* mixin: atrule => {
|
||||
* atrule.replaceWith(mixinRules[atrule.params])
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param nodes Mode(s) to replace current one.
|
||||
* @return Current node to methods chain.
|
||||
*/
|
||||
replaceWith(
|
||||
...nodes: (
|
||||
| Node.ChildNode
|
||||
| Node.ChildNode[]
|
||||
| Node.ChildProps
|
||||
| Node.ChildProps[]
|
||||
)[]
|
||||
): this
|
||||
|
||||
/**
|
||||
* Finds the Root instance of the node’s tree.
|
||||
*
|
||||
* ```js
|
||||
* root.nodes[0].nodes[0].root() === root
|
||||
* ```
|
||||
*
|
||||
* @return Root parent.
|
||||
*/
|
||||
root(): Root
|
||||
|
||||
/**
|
||||
* Fix circular links on `JSON.stringify()`.
|
||||
*
|
||||
* @return Cleaned object.
|
||||
*/
|
||||
toJSON(): object
|
||||
|
||||
/**
|
||||
* It compiles the node to browser readable cascading style sheets string
|
||||
* depending on it's type.
|
||||
*
|
||||
* ```js
|
||||
* new Rule({ selector: 'a' }).toString() //=> "a {}"
|
||||
* ```
|
||||
*
|
||||
* @param stringifier A syntax to use in string generation.
|
||||
* @return CSS string of this node.
|
||||
*/
|
||||
toString(stringifier?: Stringifier | Syntax): string
|
||||
|
||||
/**
|
||||
* It is a wrapper for {@link Result#warn}, providing convenient
|
||||
* way of generating warnings.
|
||||
*
|
||||
* ```js
|
||||
* Declaration: {
|
||||
* bad: (decl, { result }) => {
|
||||
* decl.warn(result, 'Deprecated property: bad')
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param result The `Result` instance that will receive the warning.
|
||||
* @param message Description for the warning.
|
||||
* @param options Options for the warning.
|
||||
*
|
||||
* @return `Warning` instance is returned
|
||||
*/
|
||||
warn(result: Result, message: string, options?: WarningOptions): Warning
|
||||
}
|
||||
|
||||
declare class Node extends Node_ {}
|
||||
|
||||
export = Node
|
||||
9
src/postcss/parse.d.ts
vendored
Normal file
9
src/postcss/parse.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Parser } from './postcss.js'
|
||||
|
||||
interface Parse extends Parser {
|
||||
default: Parse
|
||||
}
|
||||
|
||||
declare const parse: Parse
|
||||
|
||||
export = parse
|
||||
436
src/postcss/postcss.d.ts
vendored
Normal file
436
src/postcss/postcss.d.ts
vendored
Normal file
@@ -0,0 +1,436 @@
|
||||
import AtRule, { AtRuleProps } from './at-rule.js'
|
||||
import Comment, { CommentProps } from './comment.js'
|
||||
import Container, { ContainerProps } from './container.js'
|
||||
import CssSyntaxError from './css-syntax-error.js'
|
||||
import Declaration, { DeclarationProps } from './declaration.js'
|
||||
import Document, { DocumentProps } from './document.js'
|
||||
import Input, { FilePosition } from './input.js'
|
||||
import LazyResult from './lazy-result.js'
|
||||
import list from './list.js'
|
||||
import Node, {
|
||||
AnyNode,
|
||||
ChildNode,
|
||||
ChildProps,
|
||||
NodeErrorOptions,
|
||||
NodeProps,
|
||||
Position,
|
||||
Source
|
||||
} from './node.js'
|
||||
import Processor from './processor.js'
|
||||
import Result, { Message } from './result.js'
|
||||
import Root, { RootProps } from './root.js'
|
||||
import Rule, { RuleProps } from './rule.js'
|
||||
import Warning, { WarningOptions } from './warning.js'
|
||||
|
||||
type DocumentProcessor = (
|
||||
document: Document,
|
||||
helper: postcss.Helpers
|
||||
) => Promise<void> | void
|
||||
type RootProcessor = (root: Root, helper: postcss.Helpers) => Promise<void> | void
|
||||
type DeclarationProcessor = (
|
||||
decl: Declaration,
|
||||
helper: postcss.Helpers
|
||||
) => Promise<void> | void
|
||||
type RuleProcessor = (rule: Rule, helper: postcss.Helpers) => Promise<void> | void
|
||||
type AtRuleProcessor = (atRule: AtRule, helper: postcss.Helpers) => Promise<void> | void
|
||||
type CommentProcessor = (
|
||||
comment: Comment,
|
||||
helper: postcss.Helpers
|
||||
) => Promise<void> | void
|
||||
|
||||
interface Processors {
|
||||
/**
|
||||
* Will be called on all`AtRule` nodes.
|
||||
*
|
||||
* Will be called again on node or children changes.
|
||||
*/
|
||||
AtRule?: { [name: string]: AtRuleProcessor } | AtRuleProcessor
|
||||
|
||||
/**
|
||||
* Will be called on all `AtRule` nodes, when all children will be processed.
|
||||
*
|
||||
* Will be called again on node or children changes.
|
||||
*/
|
||||
AtRuleExit?: { [name: string]: AtRuleProcessor } | AtRuleProcessor
|
||||
|
||||
/**
|
||||
* Will be called on all `Comment` nodes.
|
||||
*
|
||||
* Will be called again on node or children changes.
|
||||
*/
|
||||
Comment?: CommentProcessor
|
||||
|
||||
/**
|
||||
* Will be called on all `Comment` nodes after listeners
|
||||
* for `Comment` event.
|
||||
*
|
||||
* Will be called again on node or children changes.
|
||||
*/
|
||||
CommentExit?: CommentProcessor
|
||||
|
||||
/**
|
||||
* Will be called on all `Declaration` nodes after listeners
|
||||
* for `Declaration` event.
|
||||
*
|
||||
* Will be called again on node or children changes.
|
||||
*/
|
||||
Declaration?: { [prop: string]: DeclarationProcessor } | DeclarationProcessor
|
||||
|
||||
/**
|
||||
* Will be called on all `Declaration` nodes.
|
||||
*
|
||||
* Will be called again on node or children changes.
|
||||
*/
|
||||
DeclarationExit?:
|
||||
| { [prop: string]: DeclarationProcessor }
|
||||
| DeclarationProcessor
|
||||
|
||||
/**
|
||||
* Will be called on `Document` node.
|
||||
*
|
||||
* Will be called again on children changes.
|
||||
*/
|
||||
Document?: DocumentProcessor
|
||||
|
||||
/**
|
||||
* Will be called on `Document` node, when all children will be processed.
|
||||
*
|
||||
* Will be called again on children changes.
|
||||
*/
|
||||
DocumentExit?: DocumentProcessor
|
||||
|
||||
/**
|
||||
* Will be called on `Root` node once.
|
||||
*/
|
||||
Once?: RootProcessor
|
||||
|
||||
/**
|
||||
* Will be called on `Root` node once, when all children will be processed.
|
||||
*/
|
||||
OnceExit?: RootProcessor
|
||||
|
||||
/**
|
||||
* Will be called on `Root` node.
|
||||
*
|
||||
* Will be called again on children changes.
|
||||
*/
|
||||
Root?: RootProcessor
|
||||
|
||||
/**
|
||||
* Will be called on `Root` node, when all children will be processed.
|
||||
*
|
||||
* Will be called again on children changes.
|
||||
*/
|
||||
RootExit?: RootProcessor
|
||||
|
||||
/**
|
||||
* Will be called on all `Rule` nodes.
|
||||
*
|
||||
* Will be called again on node or children changes.
|
||||
*/
|
||||
Rule?: RuleProcessor
|
||||
|
||||
/**
|
||||
* Will be called on all `Rule` nodes, when all children will be processed.
|
||||
*
|
||||
* Will be called again on node or children changes.
|
||||
*/
|
||||
RuleExit?: RuleProcessor
|
||||
}
|
||||
|
||||
declare namespace postcss {
|
||||
export {
|
||||
AnyNode,
|
||||
AtRule,
|
||||
AtRuleProps,
|
||||
ChildNode,
|
||||
ChildProps,
|
||||
Comment,
|
||||
CommentProps,
|
||||
Container,
|
||||
ContainerProps,
|
||||
CssSyntaxError,
|
||||
Declaration,
|
||||
DeclarationProps,
|
||||
Document,
|
||||
DocumentProps,
|
||||
FilePosition,
|
||||
Input,
|
||||
LazyResult,
|
||||
list,
|
||||
Message,
|
||||
Node,
|
||||
NodeErrorOptions,
|
||||
NodeProps,
|
||||
Position,
|
||||
Processor,
|
||||
Result,
|
||||
Root,
|
||||
RootProps,
|
||||
Rule,
|
||||
RuleProps,
|
||||
Source,
|
||||
Warning,
|
||||
WarningOptions
|
||||
}
|
||||
|
||||
|
||||
export type Helpers = { postcss: Postcss; result: Result } & Postcss
|
||||
|
||||
export interface Plugin extends Processors {
|
||||
postcssPlugin: string
|
||||
prepare?: (result: Result) => Processors
|
||||
}
|
||||
|
||||
export interface PluginCreator<PluginOptions> {
|
||||
(opts?: PluginOptions): Plugin | Processor
|
||||
postcss: true
|
||||
}
|
||||
|
||||
export interface Transformer extends TransformCallback {
|
||||
postcssPlugin: string
|
||||
postcssVersion: string
|
||||
}
|
||||
|
||||
export interface TransformCallback {
|
||||
(root: Root, result: Result): Promise<void> | void
|
||||
}
|
||||
|
||||
export interface OldPlugin<T> extends Transformer {
|
||||
(opts?: T): Transformer
|
||||
postcss: Transformer
|
||||
}
|
||||
|
||||
export type AcceptedPlugin =
|
||||
| {
|
||||
postcss: Processor | TransformCallback
|
||||
}
|
||||
| OldPlugin<any>
|
||||
| Plugin
|
||||
| PluginCreator<any>
|
||||
| Processor
|
||||
| TransformCallback
|
||||
|
||||
export interface Parser<RootNode = Document | Root> {
|
||||
(
|
||||
css: { toString(): string } | string,
|
||||
opts?: Pick<ProcessOptions, 'from' | 'map'>
|
||||
): RootNode
|
||||
}
|
||||
|
||||
export interface Builder {
|
||||
(part: string, node?: AnyNode, type?: 'end' | 'start'): void
|
||||
}
|
||||
|
||||
export interface Stringifier {
|
||||
(node: AnyNode, builder: Builder): void
|
||||
}
|
||||
|
||||
export interface JSONHydrator {
|
||||
(data: object): Node
|
||||
(data: object[]): Node[]
|
||||
}
|
||||
|
||||
export interface Syntax<RootNode = Document | Root> {
|
||||
/**
|
||||
* Function to generate AST by string.
|
||||
*/
|
||||
parse?: Parser<RootNode>
|
||||
|
||||
/**
|
||||
* Class to generate string by AST.
|
||||
*/
|
||||
stringify?: Stringifier
|
||||
}
|
||||
|
||||
export interface SourceMapOptions {
|
||||
/**
|
||||
* Use absolute path in generated source map.
|
||||
*/
|
||||
absolute?: boolean
|
||||
|
||||
/**
|
||||
* Indicates that PostCSS should add annotation comments to the CSS.
|
||||
* By default, PostCSS will always add a comment with a path
|
||||
* to the source map. PostCSS will not add annotations to CSS files
|
||||
* that do not contain any comments.
|
||||
*
|
||||
* By default, PostCSS presumes that you want to save the source map as
|
||||
* `opts.to + '.map'` and will use this path in the annotation comment.
|
||||
* A different path can be set by providing a string value for annotation.
|
||||
*
|
||||
* If you have set `inline: true`, annotation cannot be disabled.
|
||||
*/
|
||||
annotation?: ((file: string, root: Root) => string) | boolean | string
|
||||
|
||||
/**
|
||||
* Override `from` in map’s sources.
|
||||
*/
|
||||
from?: string
|
||||
|
||||
/**
|
||||
* Indicates that the source map should be embedded in the output CSS
|
||||
* as a Base64-encoded comment. By default, it is `true`.
|
||||
* But if all previous maps are external, not inline, PostCSS will not embed
|
||||
* the map even if you do not set this option.
|
||||
*
|
||||
* If you have an inline source map, the result.map property will be empty,
|
||||
* as the source map will be contained within the text of `result.css`.
|
||||
*/
|
||||
inline?: boolean
|
||||
|
||||
/**
|
||||
* Source map content from a previous processing step (e.g., Sass).
|
||||
*
|
||||
* PostCSS will try to read the previous source map
|
||||
* automatically (based on comments within the source CSS), but you can use
|
||||
* this option to identify it manually.
|
||||
*
|
||||
* If desired, you can omit the previous map with prev: `false`.
|
||||
*/
|
||||
prev?: ((file: string) => string) | boolean | object | string
|
||||
|
||||
/**
|
||||
* Indicates that PostCSS should set the origin content (e.g., Sass source)
|
||||
* of the source map. By default, it is true. But if all previous maps do not
|
||||
* contain sources content, PostCSS will also leave it out even if you
|
||||
* do not set this option.
|
||||
*/
|
||||
sourcesContent?: boolean
|
||||
}
|
||||
|
||||
export interface ProcessOptions<RootNode = Document | Root> {
|
||||
/**
|
||||
* The path of the CSS source file. You should always set `from`,
|
||||
* because it is used in source map generation and syntax error messages.
|
||||
*/
|
||||
from?: string | undefined
|
||||
|
||||
/**
|
||||
* Source map options
|
||||
*/
|
||||
map?: boolean | SourceMapOptions
|
||||
|
||||
/**
|
||||
* Function to generate AST by string.
|
||||
*/
|
||||
parser?: Parser<RootNode> | Syntax<RootNode>
|
||||
|
||||
/**
|
||||
* Class to generate string by AST.
|
||||
*/
|
||||
stringifier?: Stringifier | Syntax<RootNode>
|
||||
|
||||
/**
|
||||
* Object with parse and stringify.
|
||||
*/
|
||||
syntax?: Syntax<RootNode>
|
||||
|
||||
/**
|
||||
* The path where you'll put the output CSS file. You should always set `to`
|
||||
* to generate correct source maps.
|
||||
*/
|
||||
to?: string
|
||||
}
|
||||
|
||||
export type Postcss = typeof postcss
|
||||
|
||||
/**
|
||||
* Default function to convert a node tree into a CSS string.
|
||||
*/
|
||||
export let stringify: Stringifier
|
||||
|
||||
/**
|
||||
* Parses source css and returns a new `Root` or `Document` node,
|
||||
* which contains the source CSS nodes.
|
||||
*
|
||||
* ```js
|
||||
* // Simple CSS concatenation with source map support
|
||||
* const root1 = postcss.parse(css1, { from: file1 })
|
||||
* const root2 = postcss.parse(css2, { from: file2 })
|
||||
* root1.append(root2).toResult().css
|
||||
* ```
|
||||
*/
|
||||
export let parse: Parser<Root>
|
||||
|
||||
/**
|
||||
* Rehydrate a JSON AST (from `Node#toJSON`) back into the AST classes.
|
||||
*
|
||||
* ```js
|
||||
* const json = root.toJSON()
|
||||
* // save to file, send by network, etc
|
||||
* const root2 = postcss.fromJSON(json)
|
||||
* ```
|
||||
*/
|
||||
export let fromJSON: JSONHydrator
|
||||
|
||||
/**
|
||||
* Creates a new `Comment` node.
|
||||
*
|
||||
* @param defaults Properties for the new node.
|
||||
* @return New comment node
|
||||
*/
|
||||
export function comment(defaults?: CommentProps): Comment
|
||||
|
||||
/**
|
||||
* Creates a new `AtRule` node.
|
||||
*
|
||||
* @param defaults Properties for the new node.
|
||||
* @return New at-rule node.
|
||||
*/
|
||||
export function atRule(defaults?: AtRuleProps): AtRule
|
||||
|
||||
/**
|
||||
* Creates a new `Declaration` node.
|
||||
*
|
||||
* @param defaults Properties for the new node.
|
||||
* @return New declaration node.
|
||||
*/
|
||||
export function decl(defaults?: DeclarationProps): Declaration
|
||||
|
||||
/**
|
||||
* Creates a new `Rule` node.
|
||||
*
|
||||
* @param default Properties for the new node.
|
||||
* @return New rule node.
|
||||
*/
|
||||
export function rule(defaults?: RuleProps): Rule
|
||||
|
||||
/**
|
||||
* Creates a new `Root` node.
|
||||
*
|
||||
* @param defaults Properties for the new node.
|
||||
* @return New root node.
|
||||
*/
|
||||
export function root(defaults?: RootProps): Root
|
||||
|
||||
/**
|
||||
* Creates a new `Document` node.
|
||||
*
|
||||
* @param defaults Properties for the new node.
|
||||
* @return New document node.
|
||||
*/
|
||||
export function document(defaults?: DocumentProps): Document
|
||||
|
||||
export { postcss as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new `Processor` instance that will apply `plugins`
|
||||
* as CSS processors.
|
||||
*
|
||||
* ```js
|
||||
* let postcss = require('postcss')
|
||||
*
|
||||
* postcss(plugins).process(css, { from, to }).then(result => {
|
||||
* console.log(result.css)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param plugins PostCSS plugins.
|
||||
* @return Processor to process multiple CSS.
|
||||
*/
|
||||
declare function postcss(plugins?: postcss.AcceptedPlugin[]): Processor
|
||||
declare function postcss(...plugins: postcss.AcceptedPlugin[]): Processor
|
||||
|
||||
export = postcss
|
||||
6520
src/postcss/postcss.js
Normal file
6520
src/postcss/postcss.js
Normal file
File diff suppressed because it is too large
Load Diff
81
src/postcss/previous-map.d.ts
vendored
Normal file
81
src/postcss/previous-map.d.ts
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
import { SourceMapConsumer } from 'source-map-js'
|
||||
|
||||
import { ProcessOptions } from './postcss.js'
|
||||
|
||||
declare namespace PreviousMap {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { PreviousMap_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Source map information from input CSS.
|
||||
* For example, source map after Sass compiler.
|
||||
*
|
||||
* This class will automatically find source map in input CSS or in file system
|
||||
* near input file (according `from` option).
|
||||
*
|
||||
* ```js
|
||||
* const root = parse(css, { from: 'a.sass.css' })
|
||||
* root.input.map //=> PreviousMap
|
||||
* ```
|
||||
*/
|
||||
declare class PreviousMap_ {
|
||||
/**
|
||||
* `sourceMappingURL` content.
|
||||
*/
|
||||
annotation?: string
|
||||
|
||||
/**
|
||||
* The CSS source identifier. Contains `Input#file` if the user
|
||||
* set the `from` option, or `Input#id` if they did not.
|
||||
*/
|
||||
file?: string
|
||||
|
||||
/**
|
||||
* Was source map inlined by data-uri to input CSS.
|
||||
*/
|
||||
inline: boolean
|
||||
|
||||
/**
|
||||
* Path to source map file.
|
||||
*/
|
||||
mapFile?: string
|
||||
|
||||
/**
|
||||
* The directory with source map file, if source map is in separated file.
|
||||
*/
|
||||
root?: string
|
||||
|
||||
/**
|
||||
* Source map file content.
|
||||
*/
|
||||
text?: string
|
||||
|
||||
/**
|
||||
* @param css Input CSS source.
|
||||
* @param opts Process options.
|
||||
*/
|
||||
constructor(css: string, opts?: ProcessOptions)
|
||||
|
||||
/**
|
||||
* Create a instance of `SourceMapGenerator` class
|
||||
* from the `source-map` library to work with source map information.
|
||||
*
|
||||
* It is lazy method, so it will create object only on first call
|
||||
* and then it will use cache.
|
||||
*
|
||||
* @return Object with source map information.
|
||||
*/
|
||||
consumer(): SourceMapConsumer
|
||||
|
||||
/**
|
||||
* Does source map contains `sourcesContent` with input source text.
|
||||
*
|
||||
* @return Is `sourcesContent` present.
|
||||
*/
|
||||
withContent(): boolean
|
||||
}
|
||||
|
||||
declare class PreviousMap extends PreviousMap_ {}
|
||||
|
||||
export = PreviousMap
|
||||
115
src/postcss/processor.d.ts
vendored
Normal file
115
src/postcss/processor.d.ts
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
import Document from './document.js'
|
||||
import LazyResult from './lazy-result.js'
|
||||
import NoWorkResult from './no-work-result.js'
|
||||
import {
|
||||
AcceptedPlugin,
|
||||
Plugin,
|
||||
ProcessOptions,
|
||||
TransformCallback,
|
||||
Transformer
|
||||
} from './postcss.js'
|
||||
import Result from './result.js'
|
||||
import Root from './root.js'
|
||||
|
||||
declare namespace Processor {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Processor_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains plugins to process CSS. Create one `Processor` instance,
|
||||
* initialize its plugins, and then use that instance on numerous CSS files.
|
||||
*
|
||||
* ```js
|
||||
* const processor = postcss([autoprefixer, postcssNested])
|
||||
* processor.process(css1).then(result => console.log(result.css))
|
||||
* processor.process(css2).then(result => console.log(result.css))
|
||||
* ```
|
||||
*/
|
||||
declare class Processor_ {
|
||||
/**
|
||||
* Plugins added to this processor.
|
||||
*
|
||||
* ```js
|
||||
* const processor = postcss([autoprefixer, postcssNested])
|
||||
* processor.plugins.length //=> 2
|
||||
* ```
|
||||
*/
|
||||
plugins: (Plugin | TransformCallback | Transformer)[]
|
||||
|
||||
/**
|
||||
* Current PostCSS version.
|
||||
*
|
||||
* ```js
|
||||
* if (result.processor.version.split('.')[0] !== '6') {
|
||||
* throw new Error('This plugin works only with PostCSS 6')
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
version: string
|
||||
|
||||
/**
|
||||
* @param plugins PostCSS plugins
|
||||
*/
|
||||
constructor(plugins?: AcceptedPlugin[])
|
||||
|
||||
/**
|
||||
* Parses source CSS and returns a `LazyResult` Promise proxy.
|
||||
* Because some plugins can be asynchronous it doesn’t make
|
||||
* any transformations. Transformations will be applied
|
||||
* in the `LazyResult` methods.
|
||||
*
|
||||
* ```js
|
||||
* processor.process(css, { from: 'a.css', to: 'a.out.css' })
|
||||
* .then(result => {
|
||||
* console.log(result.css)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param css String with input CSS or any object with a `toString()` method,
|
||||
* like a Buffer. Optionally, send a `Result` instance
|
||||
* and the processor will take the `Root` from it.
|
||||
* @param opts Options.
|
||||
* @return Promise proxy.
|
||||
*/
|
||||
process(
|
||||
css: { toString(): string } | LazyResult | Result | Root | string
|
||||
): LazyResult | NoWorkResult
|
||||
process<RootNode extends Document | Root = Root>(
|
||||
css: { toString(): string } | LazyResult | Result | Root | string,
|
||||
options: ProcessOptions<RootNode>
|
||||
): LazyResult<RootNode>
|
||||
|
||||
/**
|
||||
* Adds a plugin to be used as a CSS processor.
|
||||
*
|
||||
* PostCSS plugin can be in 4 formats:
|
||||
* * A plugin in `Plugin` format.
|
||||
* * A plugin creator function with `pluginCreator.postcss = true`.
|
||||
* PostCSS will call this function without argument to get plugin.
|
||||
* * A function. PostCSS will pass the function a {@link Root}
|
||||
* as the first argument and current `Result` instance
|
||||
* as the second.
|
||||
* * Another `Processor` instance. PostCSS will copy plugins
|
||||
* from that instance into this one.
|
||||
*
|
||||
* Plugins can also be added by passing them as arguments when creating
|
||||
* a `postcss` instance (see [`postcss(plugins)`]).
|
||||
*
|
||||
* Asynchronous plugins should return a `Promise` instance.
|
||||
*
|
||||
* ```js
|
||||
* const processor = postcss()
|
||||
* .use(autoprefixer)
|
||||
* .use(postcssNested)
|
||||
* ```
|
||||
*
|
||||
* @param plugin PostCSS plugin or `Processor` with plugins.
|
||||
* @return Current processor to make methods chain.
|
||||
*/
|
||||
use(plugin: AcceptedPlugin): this
|
||||
}
|
||||
|
||||
declare class Processor extends Processor_ {}
|
||||
|
||||
export = Processor
|
||||
206
src/postcss/result.d.ts
vendored
Normal file
206
src/postcss/result.d.ts
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
import {
|
||||
Document,
|
||||
Node,
|
||||
Plugin,
|
||||
ProcessOptions,
|
||||
Root,
|
||||
SourceMap,
|
||||
TransformCallback,
|
||||
Warning,
|
||||
WarningOptions
|
||||
} from './postcss.js'
|
||||
import Processor from './processor.js'
|
||||
|
||||
declare namespace Result {
|
||||
export interface Message {
|
||||
[others: string]: any
|
||||
|
||||
/**
|
||||
* Source PostCSS plugin name.
|
||||
*/
|
||||
plugin?: string
|
||||
|
||||
/**
|
||||
* Message type.
|
||||
*/
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface ResultOptions extends ProcessOptions {
|
||||
/**
|
||||
* The CSS node that was the source of the warning.
|
||||
*/
|
||||
node?: Node
|
||||
|
||||
/**
|
||||
* Name of plugin that created this warning. `Result#warn` will fill it
|
||||
* automatically with `Plugin#postcssPlugin` value.
|
||||
*/
|
||||
plugin?: string
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Result_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the result of the PostCSS transformations.
|
||||
*
|
||||
* A Result instance is returned by `LazyResult#then`
|
||||
* or `Root#toResult` methods.
|
||||
*
|
||||
* ```js
|
||||
* postcss([autoprefixer]).process(css).then(result => {
|
||||
* console.log(result.css)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const result2 = postcss.parse(css).toResult()
|
||||
* ```
|
||||
*/
|
||||
declare class Result_<RootNode = Document | Root> {
|
||||
/**
|
||||
* A CSS string representing of `Result#root`.
|
||||
*
|
||||
* ```js
|
||||
* postcss.parse('a{}').toResult().css //=> "a{}"
|
||||
* ```
|
||||
*/
|
||||
css: string
|
||||
|
||||
/**
|
||||
* Last runned PostCSS plugin.
|
||||
*/
|
||||
lastPlugin: Plugin | TransformCallback
|
||||
|
||||
/**
|
||||
* An instance of `SourceMapGenerator` class from the `source-map` library,
|
||||
* representing changes to the `Result#root` instance.
|
||||
*
|
||||
* ```js
|
||||
* result.map.toJSON() //=> { version: 3, file: 'a.css', … }
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* if (result.map) {
|
||||
* fs.writeFileSync(result.opts.to + '.map', result.map.toString())
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
map: SourceMap
|
||||
|
||||
/**
|
||||
* Contains messages from plugins (e.g., warnings or custom messages).
|
||||
* Each message should have type and plugin properties.
|
||||
*
|
||||
* ```js
|
||||
* AtRule: {
|
||||
* import: (atRule, { result }) {
|
||||
* const importedFile = parseImport(atRule)
|
||||
* result.messages.push({
|
||||
* type: 'dependency',
|
||||
* plugin: 'postcss-import',
|
||||
* file: importedFile,
|
||||
* parent: result.opts.from
|
||||
* })
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
messages: Result.Message[]
|
||||
|
||||
/**
|
||||
* Options from the `Processor#process` or `Root#toResult` call
|
||||
* that produced this Result instance.]
|
||||
*
|
||||
* ```js
|
||||
* root.toResult(opts).opts === opts
|
||||
* ```
|
||||
*/
|
||||
opts: Result.ResultOptions
|
||||
|
||||
/**
|
||||
* The Processor instance used for this transformation.
|
||||
*
|
||||
* ```js
|
||||
* for (const plugin of result.processor.plugins) {
|
||||
* if (plugin.postcssPlugin === 'postcss-bad') {
|
||||
* throw 'postcss-good is incompatible with postcss-bad'
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
processor: Processor
|
||||
|
||||
/**
|
||||
* Root node after all transformations.
|
||||
*
|
||||
* ```js
|
||||
* root.toResult().root === root
|
||||
* ```
|
||||
*/
|
||||
root: RootNode
|
||||
|
||||
/**
|
||||
* @param processor Processor used for this transformation.
|
||||
* @param root Root node after all transformations.
|
||||
* @param opts Options from the `Processor#process` or `Root#toResult`.
|
||||
*/
|
||||
constructor(processor: Processor, root: RootNode, opts: Result.ResultOptions)
|
||||
|
||||
/**
|
||||
* Returns for `Result#css` content.
|
||||
*
|
||||
* ```js
|
||||
* result + '' === result.css
|
||||
* ```
|
||||
*
|
||||
* @return String representing of `Result#root`.
|
||||
*/
|
||||
toString(): string
|
||||
|
||||
/**
|
||||
* Creates an instance of `Warning` and adds it to `Result#messages`.
|
||||
*
|
||||
* ```js
|
||||
* if (decl.important) {
|
||||
* result.warn('Avoid !important', { node: decl, word: '!important' })
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param text Warning message.
|
||||
* @param opts Warning options.
|
||||
* @return Created warning.
|
||||
*/
|
||||
warn(message: string, options?: WarningOptions): Warning
|
||||
|
||||
/**
|
||||
* Returns warnings from plugins. Filters `Warning` instances
|
||||
* from `Result#messages`.
|
||||
*
|
||||
* ```js
|
||||
* result.warnings().forEach(warn => {
|
||||
* console.warn(warn.toString())
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @return Warnings from plugins.
|
||||
*/
|
||||
warnings(): Warning[]
|
||||
|
||||
/**
|
||||
* An alias for the `Result#css` property.
|
||||
* Use it with syntaxes that generate non-CSS output.
|
||||
*
|
||||
* ```js
|
||||
* result.css === result.content
|
||||
* ```
|
||||
*/
|
||||
get content(): string
|
||||
}
|
||||
|
||||
declare class Result<RootNode = Document | Root> extends Result_<RootNode> {}
|
||||
|
||||
export = Result
|
||||
87
src/postcss/root.d.ts
vendored
Normal file
87
src/postcss/root.d.ts
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
import Container, { ContainerProps } from './container.js'
|
||||
import Document from './document.js'
|
||||
import { ProcessOptions } from './postcss.js'
|
||||
import Result from './result.js'
|
||||
|
||||
declare namespace Root {
|
||||
export interface RootRaws extends Record<string, any> {
|
||||
/**
|
||||
* The space symbols after the last child to the end of file.
|
||||
*/
|
||||
after?: string
|
||||
|
||||
/**
|
||||
* Non-CSS code after `Root`, when `Root` is inside `Document`.
|
||||
*
|
||||
* **Experimental:** some aspects of this node could change within minor
|
||||
* or patch version releases.
|
||||
*/
|
||||
codeAfter?: string
|
||||
|
||||
/**
|
||||
* Non-CSS code before `Root`, when `Root` is inside `Document`.
|
||||
*
|
||||
* **Experimental:** some aspects of this node could change within minor
|
||||
* or patch version releases.
|
||||
*/
|
||||
codeBefore?: string
|
||||
|
||||
/**
|
||||
* Is the last child has an (optional) semicolon.
|
||||
*/
|
||||
semicolon?: boolean
|
||||
}
|
||||
|
||||
export interface RootProps extends ContainerProps {
|
||||
/**
|
||||
* Information used to generate byte-to-byte equal node string
|
||||
* as it was in the origin input.
|
||||
* */
|
||||
raws?: RootRaws
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Root_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a CSS file and contains all its parsed nodes.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a{color:black} b{z-index:2}')
|
||||
* root.type //=> 'root'
|
||||
* root.nodes.length //=> 2
|
||||
* ```
|
||||
*/
|
||||
declare class Root_ extends Container {
|
||||
nodes: NonNullable<Container['nodes']>
|
||||
parent: Document | undefined
|
||||
raws: Root.RootRaws
|
||||
type: 'root'
|
||||
|
||||
constructor(defaults?: Root.RootProps)
|
||||
|
||||
assign(overrides: object | Root.RootProps): this
|
||||
clone(overrides?: Partial<Root.RootProps>): Root
|
||||
cloneAfter(overrides?: Partial<Root.RootProps>): Root
|
||||
cloneBefore(overrides?: Partial<Root.RootProps>): Root
|
||||
|
||||
/**
|
||||
* Returns a `Result` instance representing the root’s CSS.
|
||||
*
|
||||
* ```js
|
||||
* const root1 = postcss.parse(css1, { from: 'a.css' })
|
||||
* const root2 = postcss.parse(css2, { from: 'b.css' })
|
||||
* root1.append(root2)
|
||||
* const result = root1.toResult({ to: 'all.css', map: true })
|
||||
* ```
|
||||
*
|
||||
* @param opts Options.
|
||||
* @return Result with current root’s CSS.
|
||||
*/
|
||||
toResult(options?: ProcessOptions): Result
|
||||
}
|
||||
|
||||
declare class Root extends Root_ {}
|
||||
|
||||
export = Root
|
||||
119
src/postcss/rule.d.ts
vendored
Normal file
119
src/postcss/rule.d.ts
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
import Container, {
|
||||
ContainerProps,
|
||||
ContainerWithChildren
|
||||
} from './container.js'
|
||||
|
||||
declare namespace Rule {
|
||||
export interface RuleRaws extends Record<string, unknown> {
|
||||
/**
|
||||
* The space symbols after the last child of the node to the end of the node.
|
||||
*/
|
||||
after?: string
|
||||
|
||||
/**
|
||||
* The space symbols before the node. It also stores `*`
|
||||
* and `_` symbols before the declaration (IE hack).
|
||||
*/
|
||||
before?: string
|
||||
|
||||
/**
|
||||
* The symbols between the selector and `{` for rules.
|
||||
*/
|
||||
between?: string
|
||||
|
||||
/**
|
||||
* Contains `true` if there is semicolon after rule.
|
||||
*/
|
||||
ownSemicolon?: string
|
||||
|
||||
/**
|
||||
* The rule’s selector with comments.
|
||||
*/
|
||||
selector?: {
|
||||
raw: string
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains `true` if the last child has an (optional) semicolon.
|
||||
*/
|
||||
semicolon?: boolean
|
||||
}
|
||||
|
||||
export interface RuleProps extends ContainerProps {
|
||||
/** Information used to generate byte-to-byte equal node string as it was in the origin input. */
|
||||
raws?: RuleRaws
|
||||
/** Selector or selectors of the rule. */
|
||||
selector?: string
|
||||
/** Selectors of the rule represented as an array of strings. */
|
||||
selectors?: string[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Rule_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a CSS rule: a selector followed by a declaration block.
|
||||
*
|
||||
* ```js
|
||||
* Once (root, { Rule }) {
|
||||
* let a = new Rule({ selector: 'a' })
|
||||
* a.append(…)
|
||||
* root.append(a)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a{}')
|
||||
* const rule = root.first
|
||||
* rule.type //=> 'rule'
|
||||
* rule.toString() //=> 'a{}'
|
||||
* ```
|
||||
*/
|
||||
declare class Rule_ extends Container {
|
||||
nodes: NonNullable<Container['nodes']>
|
||||
parent: ContainerWithChildren | undefined
|
||||
raws: Rule.RuleRaws
|
||||
/**
|
||||
* The rule’s full selector represented as a string.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a, b { }')
|
||||
* const rule = root.first
|
||||
* rule.selector //=> 'a, b'
|
||||
* ```
|
||||
*/
|
||||
get selector(): string
|
||||
set selector(value: string);
|
||||
|
||||
/**
|
||||
* An array containing the rule’s individual selectors.
|
||||
* Groups of selectors are split at commas.
|
||||
*
|
||||
* ```js
|
||||
* const root = postcss.parse('a, b { }')
|
||||
* const rule = root.first
|
||||
*
|
||||
* rule.selector //=> 'a, b'
|
||||
* rule.selectors //=> ['a', 'b']
|
||||
*
|
||||
* rule.selectors = ['a', 'strong']
|
||||
* rule.selector //=> 'a, strong'
|
||||
* ```
|
||||
*/
|
||||
get selectors(): string[]
|
||||
set selectors(values: string[]);
|
||||
|
||||
type: 'rule'
|
||||
|
||||
constructor(defaults?: Rule.RuleProps)
|
||||
assign(overrides: object | Rule.RuleProps): this
|
||||
clone(overrides?: Partial<Rule.RuleProps>): Rule
|
||||
cloneAfter(overrides?: Partial<Rule.RuleProps>): Rule
|
||||
cloneBefore(overrides?: Partial<Rule.RuleProps>): Rule
|
||||
}
|
||||
|
||||
declare class Rule extends Rule_ {}
|
||||
|
||||
export = Rule
|
||||
46
src/postcss/stringifier.d.ts
vendored
Normal file
46
src/postcss/stringifier.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
AnyNode,
|
||||
AtRule,
|
||||
Builder,
|
||||
Comment,
|
||||
Container,
|
||||
Declaration,
|
||||
Document,
|
||||
Root,
|
||||
Rule
|
||||
} from './postcss.js'
|
||||
|
||||
declare namespace Stringifier {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Stringifier_ as default }
|
||||
}
|
||||
|
||||
declare class Stringifier_ {
|
||||
builder: Builder
|
||||
constructor(builder: Builder)
|
||||
atrule(node: AtRule, semicolon?: boolean): void
|
||||
beforeAfter(node: AnyNode, detect: 'after' | 'before'): string
|
||||
block(node: AnyNode, start: string): void
|
||||
body(node: Container): void
|
||||
comment(node: Comment): void
|
||||
decl(node: Declaration, semicolon?: boolean): void
|
||||
document(node: Document): void
|
||||
raw(node: AnyNode, own: null | string, detect?: string): string
|
||||
rawBeforeClose(root: Root): string | undefined
|
||||
rawBeforeComment(root: Root, node: Comment): string | undefined
|
||||
rawBeforeDecl(root: Root, node: Declaration): string | undefined
|
||||
rawBeforeOpen(root: Root): string | undefined
|
||||
rawBeforeRule(root: Root): string | undefined
|
||||
rawColon(root: Root): string | undefined
|
||||
rawEmptyBody(root: Root): string | undefined
|
||||
rawIndent(root: Root): string | undefined
|
||||
rawSemicolon(root: Root): boolean | undefined
|
||||
rawValue(node: AnyNode, prop: string): string
|
||||
root(node: Root): void
|
||||
rule(node: Rule): void
|
||||
stringify(node: AnyNode, semicolon?: boolean): void
|
||||
}
|
||||
|
||||
declare class Stringifier extends Stringifier_ {}
|
||||
|
||||
export = Stringifier
|
||||
9
src/postcss/stringify.d.ts
vendored
Normal file
9
src/postcss/stringify.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Stringifier } from './postcss.js'
|
||||
|
||||
interface Stringify extends Stringifier {
|
||||
default: Stringify
|
||||
}
|
||||
|
||||
declare const stringify: Stringify
|
||||
|
||||
export = stringify
|
||||
147
src/postcss/warning.d.ts
vendored
Normal file
147
src/postcss/warning.d.ts
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
import { RangePosition } from './css-syntax-error.js'
|
||||
import Node from './node.js'
|
||||
|
||||
declare namespace Warning {
|
||||
export interface WarningOptions {
|
||||
/**
|
||||
* End position, exclusive, in CSS node string that caused the warning.
|
||||
*/
|
||||
end?: RangePosition
|
||||
|
||||
/**
|
||||
* End index, exclusive, in CSS node string that caused the warning.
|
||||
*/
|
||||
endIndex?: number
|
||||
|
||||
/**
|
||||
* Start index, inclusive, in CSS node string that caused the warning.
|
||||
*/
|
||||
index?: number
|
||||
|
||||
/**
|
||||
* CSS node that caused the warning.
|
||||
*/
|
||||
node?: Node
|
||||
|
||||
/**
|
||||
* Name of the plugin that created this warning. `Result#warn` fills
|
||||
* this property automatically.
|
||||
*/
|
||||
plugin?: string
|
||||
|
||||
/**
|
||||
* Start position, inclusive, in CSS node string that caused the warning.
|
||||
*/
|
||||
start?: RangePosition
|
||||
|
||||
/**
|
||||
* Word in CSS source that caused the warning.
|
||||
*/
|
||||
word?: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
export { Warning_ as default }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a plugin’s warning. It can be created using `Node#warn`.
|
||||
*
|
||||
* ```js
|
||||
* if (decl.important) {
|
||||
* decl.warn(result, 'Avoid !important', { word: '!important' })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
declare class Warning_ {
|
||||
/**
|
||||
* Column for inclusive start position in the input file with this warning’s source.
|
||||
*
|
||||
* ```js
|
||||
* warning.column //=> 6
|
||||
* ```
|
||||
*/
|
||||
column: number
|
||||
|
||||
/**
|
||||
* Column for exclusive end position in the input file with this warning’s source.
|
||||
*
|
||||
* ```js
|
||||
* warning.endColumn //=> 4
|
||||
* ```
|
||||
*/
|
||||
endColumn?: number
|
||||
|
||||
/**
|
||||
* Line for exclusive end position in the input file with this warning’s source.
|
||||
*
|
||||
* ```js
|
||||
* warning.endLine //=> 6
|
||||
* ```
|
||||
*/
|
||||
endLine?: number
|
||||
|
||||
/**
|
||||
* Line for inclusive start position in the input file with this warning’s source.
|
||||
*
|
||||
* ```js
|
||||
* warning.line //=> 5
|
||||
* ```
|
||||
*/
|
||||
line: number
|
||||
|
||||
/**
|
||||
* Contains the CSS node that caused the warning.
|
||||
*
|
||||
* ```js
|
||||
* warning.node.toString() //=> 'color: white !important'
|
||||
* ```
|
||||
*/
|
||||
node: Node
|
||||
|
||||
/**
|
||||
* The name of the plugin that created this warning.
|
||||
* When you call `Node#warn` it will fill this property automatically.
|
||||
*
|
||||
* ```js
|
||||
* warning.plugin //=> 'postcss-important'
|
||||
* ```
|
||||
*/
|
||||
plugin: string
|
||||
|
||||
/**
|
||||
* The warning message.
|
||||
*
|
||||
* ```js
|
||||
* warning.text //=> 'Try to avoid !important'
|
||||
* ```
|
||||
*/
|
||||
text: string
|
||||
|
||||
/**
|
||||
* Type to filter warnings from `Result#messages`.
|
||||
* Always equal to `"warning"`.
|
||||
*/
|
||||
type: 'warning'
|
||||
|
||||
/**
|
||||
* @param text Warning message.
|
||||
* @param opts Warning options.
|
||||
*/
|
||||
constructor(text: string, opts?: Warning.WarningOptions)
|
||||
|
||||
/**
|
||||
* Returns a warning position and message.
|
||||
*
|
||||
* ```js
|
||||
* warning.toString() //=> 'postcss-lint:a.css:10:14: Avoid !important'
|
||||
* ```
|
||||
*
|
||||
* @return Warning position and message.
|
||||
*/
|
||||
toString(): string
|
||||
}
|
||||
|
||||
declare class Warning extends Warning_ {}
|
||||
|
||||
export = Warning
|
||||
36
src/preprocess/index.ts
Normal file
36
src/preprocess/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// [note-to-mp 重构] 内容预处理模块
|
||||
|
||||
// 行级颜色块语法:||r text / ||g text / ||b text / ||y text / || text
|
||||
const LINE_COLOR_RE = /^\|\|(r|g|b|y)?\s+(.*)$/;
|
||||
const FIG_RE = /\[fig([^\n]*?)\/_?]/g; // 简单题注
|
||||
|
||||
function wrapColorLine(code: string | undefined, text: string): string {
|
||||
const colorMap: Record<string, string> = {
|
||||
r: '#ffe5e5',
|
||||
g: '#e5ffe9',
|
||||
b: '#e5f1ff',
|
||||
y: '#fff7d6',
|
||||
'': '#f2f2f2'
|
||||
};
|
||||
const c = (code && colorMap[code]) || colorMap[''];
|
||||
return `<p style="background:${c};padding:4px 8px;border-radius:4px;">${text}</p>`;
|
||||
}
|
||||
|
||||
export function preprocessContent(markdown: string): string {
|
||||
const lines = markdown.split(/\r?\n/);
|
||||
const out: string[] = [];
|
||||
for (const line of lines) {
|
||||
const m = line.match(LINE_COLOR_RE);
|
||||
if (m) {
|
||||
out.push(wrapColorLine(m[1], m[2]));
|
||||
} else {
|
||||
out.push(line);
|
||||
}
|
||||
}
|
||||
let joined = out.join('\n');
|
||||
joined = joined.replace(FIG_RE, (_m, g1) => {
|
||||
const text = g1.trim();
|
||||
return `<span class="nmp-fig" style="display:block;text-align:center;color:#666;font-size:12px;margin:4px 0;">${text}</span>`;
|
||||
});
|
||||
return joined;
|
||||
}
|
||||
69
src/refactor-plan.md
Normal file
69
src/refactor-plan.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# note-to-mp 重构规划 (模块与接口草案)
|
||||
|
||||
> 标记格式: // [note-to-mp 重构]
|
||||
|
||||
## 目标概要
|
||||
- 模块化:图片处理、元数据、Gallery、内容预处理、渲染管线分离。
|
||||
- 清晰接口:对外暴露统一渲染与数据提取 API。
|
||||
- 可测试:核心逻辑函数纯函数化,最小化对 Obsidian 运行时依赖。
|
||||
|
||||
## 模块划分
|
||||
1. 图片处理模块 (image/)
|
||||
- 统一识别 wikilink 与 markdown 图片语法
|
||||
- LocalImage 结构: { original: string; basename: string; alt?: string; sourceType: 'wikilink'|'markdown'; index: number; }
|
||||
- LocalImageManager: 收集、查询、封面候选、上传占位接口
|
||||
- 正则常量: LocalFileRegex
|
||||
|
||||
2. 元数据与封面 (meta/)
|
||||
- extractWeChatMeta(raw: string): WeChatMetaRaw
|
||||
- getWeChatArticleMeta(): 返回最近一次渲染缓存的 meta
|
||||
- getMetadata(images: LocalImage[], metaRaw: WeChatMetaRaw): FinalMeta
|
||||
- 回退策略: frontmatter cover > metaRaw.coverLink > images[0]
|
||||
|
||||
3. Gallery 支持 (gallery/)
|
||||
- transformGalleryShortcodes(content: string): { content: string; extracted?: GalleryInfo }
|
||||
- selectGalleryImages(dir: string, options): Promise<string[]>
|
||||
- 语法: 单行 self-closing 与 块级形式
|
||||
|
||||
4. 内容预处理 (preprocess/)
|
||||
- preprocessContent(markdown: string): string
|
||||
- 行级语法: ||r / ||g / ||b / ||y / || (默认灰)
|
||||
- figure 语法: [fig text/]
|
||||
|
||||
5. 渲染管线 (render/)
|
||||
- renderMarkdown(file: TFile): Promise<RenderedArticle>
|
||||
- 内部阶段:
|
||||
Raw -> extractWeChatMeta -> strip frontmatter -> transformGalleryShortcodes -> preprocessContent -> markdown parse (自定义 tokenizer) -> HTML + 样式注入 -> metadata 汇总
|
||||
|
||||
6. 上传/微信接口 (weixin/)
|
||||
- 包装现有 weixin-api.ts 函数 + 错误封装
|
||||
|
||||
## 数据结构
|
||||
```ts
|
||||
interface LocalImage { original: string; basename: string; alt?: string; sourceType: 'wikilink'|'markdown'; index: number; }
|
||||
interface WeChatMetaRaw { title?: string; author?: string; coverLink?: string; rawImage?: string; hasFrontmatter: boolean; }
|
||||
interface FinalMeta { title: string; author?: string; coverImage?: LocalImage; coverLink?: string; }
|
||||
interface RenderedArticle { html: string; css?: string; meta: FinalMeta; images: LocalImage[]; raw: string; }
|
||||
```
|
||||
|
||||
## 关键正则
|
||||
- frontmatter: ^---[\s\S]*?\n---
|
||||
- wikilink image: !\[\[(.+?)\]\]
|
||||
- markdown image: !\[[^\]]*\]\(([^\n\r\)]+)\)
|
||||
- gallery block: /{{<gallery>}}([\s\S]*?){{<\/gallery>}}/g
|
||||
- gallery figure: /{{<figure\s+src="([^"]+)"[^>]*>}}/g
|
||||
|
||||
## 风险点
|
||||
- 正则误判 frontmatter
|
||||
- 图片在预处理阶段被破坏索引
|
||||
- 多次渲染缓存污染
|
||||
|
||||
## 缓解
|
||||
- 提取后不修改原文本副本
|
||||
- 维护渲染上下文对象 (RenderContext)
|
||||
|
||||
## 后续实现顺序
|
||||
图片处理 -> 元数据 -> Gallery -> 预处理 -> 渲染组装 -> 接口对接现有 NotePreview
|
||||
|
||||
---
|
||||
(实现过程中该文档可增补)
|
||||
50
src/render/index.ts
Normal file
50
src/render/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// [note-to-mp 重构] 渲染管线模块
|
||||
import { App, TFile, MarkdownRenderer } from 'obsidian';
|
||||
import { parseImagesFromMarkdown, LocalImage, LocalImageManager } from '../image';
|
||||
import { extractWeChatMeta, getMetadata, FinalMeta } from '../meta';
|
||||
import { transformGalleryShortcodes } from '../gallery';
|
||||
import { preprocessContent } from '../preprocess';
|
||||
|
||||
export interface RenderedArticle {
|
||||
html: string;
|
||||
meta: FinalMeta;
|
||||
images: LocalImage[];
|
||||
raw: string;
|
||||
}
|
||||
|
||||
export class RenderService {
|
||||
private app: App;
|
||||
private imageManager = new LocalImageManager();
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async renderFile(file: TFile): Promise<RenderedArticle> {
|
||||
const raw = await this.app.vault.read(file);
|
||||
return this.renderRaw(raw, file.path);
|
||||
}
|
||||
|
||||
async renderRaw(raw: string, path?: string): Promise<RenderedArticle> {
|
||||
this.imageManager.clear();
|
||||
// 1. frontmatter + 基础元数据
|
||||
const { meta: rawMeta, body } = extractWeChatMeta(raw);
|
||||
// 2. gallery 转换
|
||||
const galleryRes = transformGalleryShortcodes(body);
|
||||
// 3. 预处理行级语法
|
||||
const preprocessed = preprocessContent(galleryRes.content);
|
||||
// 4. 图片解析
|
||||
const images = parseImagesFromMarkdown(preprocessed);
|
||||
images.forEach(i => this.imageManager.add(i));
|
||||
// 5. 获取最终 meta(封面回退)
|
||||
const finalMeta = getMetadata(images, rawMeta);
|
||||
// 6. markdown -> HTML (使用 Obsidian 内部渲染管线)
|
||||
const el = document.createElement('div');
|
||||
// NOTE: 这里简化,实际应考虑自定义 tokenizer;后续可补充
|
||||
await MarkdownRenderer.renderMarkdown(preprocessed, el, path || '', this.app as any);
|
||||
// 7. 注入简单样式 (可外置)
|
||||
const style = `<style>.nmp-fig{font-style:italic}</style>`;
|
||||
|
||||
return { html: style + el.innerHTML, meta: finalMeta, images, raw };
|
||||
}
|
||||
}
|
||||
459
src/setting-tab.ts
Normal file
459
src/setting-tab.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
* 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 { App, TextAreaComponent, PluginSettingTab, Setting, Notice, sanitizeHTMLToDom } from 'obsidian';
|
||||
import NoteToMpPlugin from './main';
|
||||
import { wxGetToken,wxEncrypt } from './weixin-api';
|
||||
import { cleanMathCache } from './markdown/math';
|
||||
import { NMPSettings } from './settings';
|
||||
import { DocModal } from './doc-modal';
|
||||
|
||||
export class NoteToMpSettingTab extends PluginSettingTab {
|
||||
plugin: NoteToMpPlugin;
|
||||
wxInfo: string;
|
||||
wxTextArea: TextAreaComponent|null;
|
||||
settings: NMPSettings;
|
||||
|
||||
constructor(app: App, plugin: NoteToMpPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.settings = NMPSettings.getInstance();
|
||||
this.wxInfo = this.parseWXInfo();
|
||||
}
|
||||
|
||||
displayWXInfo(txt:string) {
|
||||
this.wxTextArea?.setValue(txt);
|
||||
}
|
||||
|
||||
parseWXInfo() {
|
||||
const wxInfo = this.settings.wxInfo;
|
||||
if (wxInfo.length == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let res = '';
|
||||
for (let wx of wxInfo) {
|
||||
res += `${wx.name}|${wx.appid}|********\n`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async testWXInfo() {
|
||||
const authKey = this.settings.authKey;
|
||||
if (authKey.length == 0) {
|
||||
new Notice('请先设置authKey');
|
||||
return;
|
||||
}
|
||||
const wxInfo = this.settings.wxInfo;
|
||||
if (wxInfo.length == 0) {
|
||||
new Notice('请先设置公众号信息');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const docUrl = 'https://mp.weixin.qq.com/s/rk5CTPGr5ftly8PtYgSjCQ';
|
||||
for (let wx of wxInfo) {
|
||||
const res = await wxGetToken(authKey, wx.appid, wx.secret.replace('SECRET', ''));
|
||||
if (res.status != 200) {
|
||||
const data = res.json;
|
||||
const { code, message } = data;
|
||||
let content = message;
|
||||
if (code === 50002) {
|
||||
content = '用户受限,可能是您的公众号被冻结或注销,请联系微信客服处理';
|
||||
}
|
||||
else if (code === 40125) {
|
||||
content = 'AppSecret错误,请检查或者重置,详细操作步骤请参考下方文档';
|
||||
}
|
||||
else if (code === 40164) {
|
||||
content = 'IP地址不在白名单中,请将如下地址添加到白名单:<br>59.110.112.211<br>154.8.198.218<br>详细步骤请参考下方文档';
|
||||
}
|
||||
const modal = new DocModal(this.app, `${wx.name} 测试失败`, content, docUrl);
|
||||
modal.open();
|
||||
break
|
||||
}
|
||||
|
||||
const data = res.json;
|
||||
if (data.token.length == 0) {
|
||||
new Notice(`${wx.name}|${wx.appid} 测试失败`);
|
||||
break
|
||||
}
|
||||
new Notice(`${wx.name} 测试通过`);
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(`测试失败:${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async encrypt() {
|
||||
if (this.wxInfo.length == 0) {
|
||||
new Notice('请输入内容');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.settings.wxInfo.length > 0) {
|
||||
new Notice('已经保存过了,请先清除!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const wechat = [];
|
||||
const lines = this.wxInfo.split('\n');
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (line.length == 0) {
|
||||
continue;
|
||||
}
|
||||
const items = line.split('|');
|
||||
if (items.length != 3) {
|
||||
new Notice('格式错误,请检查');
|
||||
return false;
|
||||
}
|
||||
const name = items[0];
|
||||
const appid = items[1].trim();
|
||||
const secret = items[2].trim();
|
||||
wechat.push({name, appid, secret});
|
||||
}
|
||||
|
||||
if (wechat.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await wxEncrypt(this.settings.authKey, wechat);
|
||||
if (res.status != 200) {
|
||||
const data = res.json;
|
||||
new Notice(`${data.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = res.json;
|
||||
for (let wx of wechat) {
|
||||
wx.secret = data[wx.appid];
|
||||
}
|
||||
|
||||
this.settings.wxInfo = wechat;
|
||||
await this.plugin.saveSettings();
|
||||
this.wxInfo = this.parseWXInfo();
|
||||
this.displayWXInfo(this.wxInfo);
|
||||
new Notice('保存成功');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
new Notice(`保存失败:${error}`);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async clear() {
|
||||
this.settings.wxInfo = [];
|
||||
await this.plugin.saveSettings();
|
||||
this.wxInfo = '';
|
||||
this.displayWXInfo('')
|
||||
}
|
||||
|
||||
display() {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
this.wxInfo = this.parseWXInfo();
|
||||
|
||||
const helpEl = containerEl.createEl('div');
|
||||
helpEl.style.cssText = 'display: flex;flex-direction: row;align-items: center;';
|
||||
helpEl.createEl('h2', {text: '帮助文档'}).style.cssText = 'margin-right: 10px;';
|
||||
helpEl.createEl('a', {text: 'https://sunboshi.tech/doc', attr: {href: 'https://sunboshi.tech/doc'}});
|
||||
|
||||
containerEl.createEl('h2', {text: '插件设置'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('默认样式')
|
||||
.addDropdown(dropdown => {
|
||||
const styles = this.plugin.assetsManager.themes;
|
||||
for (let s of styles) {
|
||||
dropdown.addOption(s.className, s.name);
|
||||
}
|
||||
dropdown.setValue(this.settings.defaultStyle);
|
||||
dropdown.onChange(async (value) => {
|
||||
this.settings.defaultStyle = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('代码高亮')
|
||||
.addDropdown(dropdown => {
|
||||
const styles = this.plugin.assetsManager.highlights;
|
||||
for (let s of styles) {
|
||||
dropdown.addOption(s.name, s.name);
|
||||
}
|
||||
dropdown.setValue(this.settings.defaultHighlight);
|
||||
dropdown.onChange(async (value) => {
|
||||
this.settings.defaultHighlight = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('在工具栏展示样式选择')
|
||||
.setDesc('建议在移动端关闭,可以增大文章预览区域')
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(this.settings.showStyleUI);
|
||||
toggle.onChange(async (value) => {
|
||||
this.settings.showStyleUI = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('链接展示样式')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('inline', '内嵌');
|
||||
dropdown.addOption('footnote', '脚注');
|
||||
dropdown.setValue(this.settings.linkStyle);
|
||||
dropdown.onChange(async (value) => {
|
||||
this.settings.linkStyle = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('文件嵌入展示样式')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('quote', '引用');
|
||||
dropdown.addOption('content', '正文');
|
||||
dropdown.setValue(this.settings.embedStyle);
|
||||
dropdown.onChange(async (value) => {
|
||||
this.settings.embedStyle = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('数学公式语法')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('latex', 'latex');
|
||||
dropdown.addOption('asciimath', 'asciimath');
|
||||
dropdown.setValue(this.settings.math);
|
||||
dropdown.onChange(async (value) => {
|
||||
this.settings.math = value;
|
||||
cleanMathCache();
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('显示代码行号')
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(this.settings.lineNumber);
|
||||
toggle.onChange(async (value) => {
|
||||
this.settings.lineNumber = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('启用空行渲染')
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(this.settings.enableEmptyLine);
|
||||
toggle.onChange(async (value) => {
|
||||
this.settings.enableEmptyLine = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('渲染图片标题')
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(this.settings.useFigcaption);
|
||||
toggle.onChange(async (value) => {
|
||||
this.settings.useFigcaption = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excalidraw 渲染为 PNG 图片')
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(this.settings.excalidrawToPNG);
|
||||
toggle.onChange(async (value) => {
|
||||
this.settings.excalidrawToPNG = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('水印图片')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('请输入图片名称')
|
||||
.setValue(this.settings.watermark)
|
||||
.onChange(async (value) => {
|
||||
this.settings.watermark = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
.inputEl.setAttr('style', 'width: 320px;')
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('获取更多主题')
|
||||
.addButton(button => {
|
||||
button.setButtonText('下载');
|
||||
button.onClick(async () => {
|
||||
button.setButtonText('下载中...');
|
||||
await this.plugin.assetsManager.downloadThemes();
|
||||
button.setButtonText('下载完成');
|
||||
});
|
||||
})
|
||||
.addButton(button => {
|
||||
button.setIcon('folder-open');
|
||||
button.onClick(async () => {
|
||||
await this.plugin.assetsManager.openAssets();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('清空主题')
|
||||
.addButton(button => {
|
||||
button.setButtonText('清空');
|
||||
button.onClick(async () => {
|
||||
await this.plugin.assetsManager.removeThemes();
|
||||
this.settings.resetStyelAndHighlight();
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
new Setting(containerEl)
|
||||
.setName('全局CSS属性')
|
||||
.setDesc('只能填写CSS属性,不能写选择器')
|
||||
.addTextArea(text => {
|
||||
this.wxTextArea = text;
|
||||
text.setPlaceholder('请输入CSS属性,如:background: #fff;padding: 10px;')
|
||||
.setValue(this.settings.baseCSS)
|
||||
.onChange(async (value) => {
|
||||
this.settings.baseCSS = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
.inputEl.setAttr('style', 'width: 520px; height: 60px;');
|
||||
})
|
||||
const customCSSDoc = '使用指南:<a href="https://sunboshi.tech/customcss">https://sunboshi.tech/customcss</a>';
|
||||
new Setting(containerEl)
|
||||
.setName('自定义CSS笔记')
|
||||
.setDesc(sanitizeHTMLToDom(customCSSDoc))
|
||||
.addText(text => {
|
||||
text.setPlaceholder('请输入自定义CSS笔记标题')
|
||||
.setValue(this.settings.customCSSNote)
|
||||
.onChange(async (value) => {
|
||||
this.settings.customCSSNote = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.assetsManager.loadCustomCSS();
|
||||
})
|
||||
.inputEl.setAttr('style', 'width: 320px;')
|
||||
});
|
||||
|
||||
const expertDoc = '使用指南:<a href="https://sunboshi.tech/expert">https://sunboshi.tech/expert</a>';
|
||||
new Setting(containerEl)
|
||||
.setName('专家设置笔记')
|
||||
.setDesc(sanitizeHTMLToDom(expertDoc))
|
||||
.addText(text => {
|
||||
text.setPlaceholder('请输入专家设置笔记标题')
|
||||
.setValue(this.settings.expertSettingsNote)
|
||||
.onChange(async (value) => {
|
||||
this.settings.expertSettingsNote = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.assetsManager.loadExpertSettings();
|
||||
})
|
||||
.inputEl.setAttr('style', 'width: 320px;')
|
||||
});
|
||||
|
||||
let descHtml = '详情说明:<a href="https://sunboshi.tech/subscribe">https://sunboshi.tech/subscribe</a>';
|
||||
if (this.settings.isVip) {
|
||||
descHtml = '<span style="color:rgb(245, 70, 85);font-weight: bold;">👑永久会员</span><br/>' + descHtml;
|
||||
}
|
||||
else if (this.settings.expireat) {
|
||||
const timestr = this.settings.expireat.toLocaleString();
|
||||
descHtml = `有效期至:${timestr} <br/>${descHtml}`
|
||||
}
|
||||
new Setting(containerEl)
|
||||
.setName('注册码(AuthKey)')
|
||||
.setDesc(sanitizeHTMLToDom(descHtml))
|
||||
.addText(text => {
|
||||
text.setPlaceholder('请输入注册码')
|
||||
.setValue(this.settings.authKey)
|
||||
.onChange(async (value) => {
|
||||
this.settings.authKey = value.trim();
|
||||
this.settings.getExpiredDate();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
.inputEl.setAttr('style', 'width: 320px;')
|
||||
}).descEl.setAttr('style', '-webkit-user-select: text; user-select: text;')
|
||||
|
||||
|
||||
let isClear = this.settings.wxInfo.length > 0;
|
||||
let isRealClear = false;
|
||||
const buttonText = isClear ? '清空公众号信息' : '保存公众号信息';
|
||||
new Setting(containerEl)
|
||||
.setName('公众号信息')
|
||||
.addTextArea(text => {
|
||||
this.wxTextArea = text;
|
||||
text.setPlaceholder('请输入公众号信息\n格式:公众号名称|公众号AppID|公众号AppSecret\n多个公众号请换行输入\n输入完成后点击加密按钮')
|
||||
.setValue(this.wxInfo)
|
||||
.onChange(value => {
|
||||
this.wxInfo = value;
|
||||
})
|
||||
.inputEl.setAttr('style', 'width: 520px; height: 120px;');
|
||||
})
|
||||
|
||||
new Setting(containerEl).addButton(button => {
|
||||
button.setButtonText(buttonText);
|
||||
button.onClick(async () => {
|
||||
if (isClear) {
|
||||
isRealClear = true;
|
||||
isClear = false;
|
||||
button.setButtonText('确认清空?');
|
||||
}
|
||||
else if (isRealClear) {
|
||||
isRealClear = false;
|
||||
isClear = false;
|
||||
this.clear();
|
||||
button.setButtonText('保存公众号信息');
|
||||
}
|
||||
else {
|
||||
button.setButtonText('保存中...');
|
||||
if (await this.encrypt()) {
|
||||
isClear = true;
|
||||
isRealClear = false;
|
||||
button.setButtonText('清空公众号信息');
|
||||
}
|
||||
else {
|
||||
button.setButtonText('保存公众号信息');
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.addButton(button => {
|
||||
button.setButtonText('测试公众号');
|
||||
button.onClick(async () => {
|
||||
button.setButtonText('测试中...');
|
||||
await this.testWXInfo();
|
||||
button.setButtonText('测试公众号');
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
203
src/settings.ts
Normal file
203
src/settings.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* 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 { wxKeyInfo } from './weixin-api';
|
||||
|
||||
export class NMPSettings {
|
||||
defaultStyle: string;
|
||||
defaultHighlight: string;
|
||||
showStyleUI: boolean;
|
||||
linkStyle: string;
|
||||
embedStyle: string;
|
||||
lineNumber: boolean;
|
||||
authKey: string;
|
||||
useCustomCss: boolean;
|
||||
customCSSNote: string;
|
||||
expertSettingsNote: string;
|
||||
wxInfo: {name:string, appid:string, secret:string}[];
|
||||
math: string;
|
||||
expireat: Date | null = null;
|
||||
isVip: boolean = false;
|
||||
baseCSS: string;
|
||||
watermark: string;
|
||||
useFigcaption: boolean;
|
||||
excalidrawToPNG: boolean;
|
||||
isLoaded: boolean = false;
|
||||
enableEmptyLine: boolean = false;
|
||||
|
||||
private static instance: NMPSettings;
|
||||
|
||||
// 静态方法,用于获取实例
|
||||
public static getInstance(): NMPSettings {
|
||||
if (!NMPSettings.instance) {
|
||||
NMPSettings.instance = new NMPSettings();
|
||||
}
|
||||
return NMPSettings.instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
this.defaultStyle = 'obsidian-light';
|
||||
this.defaultHighlight = '默认';
|
||||
this.showStyleUI = true;
|
||||
this.linkStyle = 'inline';
|
||||
this.embedStyle = 'content';
|
||||
this.lineNumber = true;
|
||||
this.useCustomCss = false;
|
||||
this.authKey = '';
|
||||
this.wxInfo = [];
|
||||
this.math = 'latex';
|
||||
this.baseCSS = '';
|
||||
this.watermark = '';
|
||||
this.useFigcaption = false;
|
||||
this.customCSSNote = '';
|
||||
this.excalidrawToPNG = false;
|
||||
this.expertSettingsNote = '';
|
||||
this.enableEmptyLine = false;
|
||||
}
|
||||
|
||||
resetStyelAndHighlight() {
|
||||
this.defaultStyle = 'obsidian-light';
|
||||
this.defaultHighlight = '默认';
|
||||
}
|
||||
|
||||
public static loadSettings(data: any) {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
const {
|
||||
defaultStyle,
|
||||
linkStyle,
|
||||
embedStyle,
|
||||
showStyleUI,
|
||||
lineNumber,
|
||||
defaultHighlight,
|
||||
authKey,
|
||||
wxInfo,
|
||||
math,
|
||||
useCustomCss,
|
||||
baseCSS,
|
||||
watermark,
|
||||
useFigcaption,
|
||||
customCSSNote,
|
||||
excalidrawToPNG,
|
||||
expertSettingsNote,
|
||||
ignoreEmptyLine,
|
||||
} = data;
|
||||
|
||||
const settings = NMPSettings.getInstance();
|
||||
if (defaultStyle) {
|
||||
settings.defaultStyle = defaultStyle;
|
||||
}
|
||||
if (defaultHighlight) {
|
||||
settings.defaultHighlight = defaultHighlight;
|
||||
}
|
||||
if (showStyleUI !== undefined) {
|
||||
settings.showStyleUI = showStyleUI;
|
||||
}
|
||||
if (linkStyle) {
|
||||
settings.linkStyle = linkStyle;
|
||||
}
|
||||
if (embedStyle) {
|
||||
settings.embedStyle = embedStyle;
|
||||
}
|
||||
if (lineNumber !== undefined) {
|
||||
settings.lineNumber = lineNumber;
|
||||
}
|
||||
if (authKey) {
|
||||
settings.authKey = authKey;
|
||||
}
|
||||
if (wxInfo) {
|
||||
settings.wxInfo = wxInfo;
|
||||
}
|
||||
if (math) {
|
||||
settings.math = math;
|
||||
}
|
||||
if (useCustomCss !== undefined) {
|
||||
settings.useCustomCss = useCustomCss;
|
||||
}
|
||||
if (baseCSS) {
|
||||
settings.baseCSS = baseCSS;
|
||||
}
|
||||
if (watermark) {
|
||||
settings.watermark = watermark;
|
||||
}
|
||||
if (useFigcaption !== undefined) {
|
||||
settings.useFigcaption = useFigcaption;
|
||||
}
|
||||
if (customCSSNote) {
|
||||
settings.customCSSNote = customCSSNote;
|
||||
}
|
||||
if (excalidrawToPNG !== undefined) {
|
||||
settings.excalidrawToPNG = excalidrawToPNG;
|
||||
}
|
||||
if (expertSettingsNote) {
|
||||
settings.expertSettingsNote = expertSettingsNote;
|
||||
}
|
||||
if (ignoreEmptyLine !== undefined) {
|
||||
settings.enableEmptyLine = ignoreEmptyLine;
|
||||
}
|
||||
settings.getExpiredDate();
|
||||
settings.isLoaded = true;
|
||||
}
|
||||
|
||||
public static allSettings() {
|
||||
const settings = NMPSettings.getInstance();
|
||||
return {
|
||||
'defaultStyle': settings.defaultStyle,
|
||||
'defaultHighlight': settings.defaultHighlight,
|
||||
'showStyleUI': settings.showStyleUI,
|
||||
'linkStyle': settings.linkStyle,
|
||||
'embedStyle': settings.embedStyle,
|
||||
'lineNumber': settings.lineNumber,
|
||||
'authKey': settings.authKey,
|
||||
'wxInfo': settings.wxInfo,
|
||||
'math': settings.math,
|
||||
'useCustomCss': settings.useCustomCss,
|
||||
'baseCSS': settings.baseCSS,
|
||||
'watermark': settings.watermark,
|
||||
'useFigcaption': settings.useFigcaption,
|
||||
'customCSSNote': settings.customCSSNote,
|
||||
'excalidrawToPNG': settings.excalidrawToPNG,
|
||||
'expertSettingsNote': settings.expertSettingsNote,
|
||||
'ignoreEmptyLine': settings.enableEmptyLine,
|
||||
}
|
||||
}
|
||||
|
||||
getExpiredDate() {
|
||||
if (this.authKey.length == 0) return;
|
||||
wxKeyInfo(this.authKey).then((res) => {
|
||||
if (res.status == 200) {
|
||||
if (res.json.vip) {
|
||||
this.isVip = true;
|
||||
}
|
||||
this.expireat = new Date(res.json.expireat);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
isAuthKeyVaild() {
|
||||
if (this.authKey.length == 0) return false;
|
||||
if (this.isVip) return true;
|
||||
if (this.expireat == null) return false;
|
||||
return this.expireat > new Date();
|
||||
}
|
||||
}
|
||||
251
src/utils.ts
Normal file
251
src/utils.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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 { App, sanitizeHTMLToDom, requestUrl, Platform } from "obsidian";
|
||||
import * as postcss from "./postcss/postcss";
|
||||
|
||||
let PluginVersion = "0.0.0";
|
||||
let PlugPlatform = "obsidian";
|
||||
|
||||
export function setVersion(version: string) {
|
||||
PluginVersion = version;
|
||||
if (Platform.isWin) {
|
||||
PlugPlatform = "win";
|
||||
}
|
||||
else if (Platform.isMacOS) {
|
||||
PlugPlatform = "mac";
|
||||
}
|
||||
else if (Platform.isLinux) {
|
||||
PlugPlatform = "linux";
|
||||
}
|
||||
else if (Platform.isIosApp) {
|
||||
PlugPlatform = "ios";
|
||||
}
|
||||
else if (Platform.isAndroidApp) {
|
||||
PlugPlatform = "android";
|
||||
}
|
||||
}
|
||||
|
||||
function getStyleSheet() {
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var sheet = document.styleSheets[i];
|
||||
if (sheet.title == 'note-to-mp-style') {
|
||||
return sheet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyStyles(element: HTMLElement, styles: CSSStyleDeclaration, computedStyle: CSSStyleDeclaration) {
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
const propertyName = styles[i];
|
||||
let propertyValue = computedStyle.getPropertyValue(propertyName);
|
||||
if (propertyName == 'width' && styles.getPropertyValue(propertyName) == 'fit-content') {
|
||||
propertyValue = 'fit-content';
|
||||
}
|
||||
if (propertyName.indexOf('margin') >= 0 && styles.getPropertyValue(propertyName).indexOf('auto') >= 0) {
|
||||
propertyValue = styles.getPropertyValue(propertyName);
|
||||
}
|
||||
element.style.setProperty(propertyName, propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndApplyStyles(element: HTMLElement, sheet:CSSStyleSheet) {
|
||||
try {
|
||||
const computedStyle = getComputedStyle(element);
|
||||
for (let i = 0; i < sheet.cssRules.length; i++) {
|
||||
const rule = sheet.cssRules[i];
|
||||
if (rule instanceof CSSStyleRule && element.matches(rule.selectorText)) {
|
||||
applyStyles(element, rule.style, computedStyle);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Unable to access stylesheet: " + sheet.href, e);
|
||||
}
|
||||
}
|
||||
|
||||
function traverse(root: HTMLElement, sheet:CSSStyleSheet) {
|
||||
let element = root.firstElementChild;
|
||||
while (element) {
|
||||
if (element.tagName === 'svg') {
|
||||
// pass
|
||||
}
|
||||
else {
|
||||
traverse(element as HTMLElement, sheet);
|
||||
}
|
||||
element = element.nextElementSibling;
|
||||
}
|
||||
parseAndApplyStyles(root, sheet);
|
||||
}
|
||||
|
||||
export async function CSSProcess(content: HTMLElement) {
|
||||
// 获取样式表
|
||||
const style = getStyleSheet();
|
||||
if (style) {
|
||||
traverse(content, style);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCSS(css: string) {
|
||||
return postcss.parse(css);
|
||||
}
|
||||
|
||||
export function ruleToStyle(rule: postcss.Rule) {
|
||||
let style = '';
|
||||
rule.walkDecls(decl => {
|
||||
style += decl.prop + ':' + decl.value + ';';
|
||||
})
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
function processPseudoSelector(selector: string) {
|
||||
if (selector.includes('::before') || selector.includes('::after')) {
|
||||
selector = selector.replace(/::before/g, '').replace(/::after/g, '');
|
||||
}
|
||||
return selector;
|
||||
}
|
||||
|
||||
function getPseudoType(selector: string) {
|
||||
if (selector.includes('::before')) {
|
||||
return 'before';
|
||||
}
|
||||
else if (selector.includes('::after')) {
|
||||
return 'after';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function applyStyle(root: HTMLElement, cssRoot: postcss.Root) {
|
||||
if (root.tagName.toLowerCase() === 'a' && root.classList.contains('wx_topic_link')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cssText = root.style.cssText;
|
||||
cssRoot.walkRules(rule => {
|
||||
const selector = processPseudoSelector(rule.selector);
|
||||
try {
|
||||
if (root.matches(selector)) {
|
||||
let item = root;
|
||||
|
||||
const pseudoType = getPseudoType(rule.selector);
|
||||
if (pseudoType) {
|
||||
let content = '';
|
||||
rule.walkDecls('content', decl => {
|
||||
content = decl.value || '';
|
||||
})
|
||||
item = createSpan();
|
||||
item.textContent = content.replace(/(^")|("$)/g, '');
|
||||
|
||||
if (pseudoType === 'before') {
|
||||
root.prepend(item);
|
||||
}
|
||||
else if (pseudoType === 'after') {
|
||||
root.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
rule.walkDecls(decl => {
|
||||
// 如果已经设置了,则不覆盖
|
||||
const setted = cssText.includes(decl.prop);
|
||||
if (!setted || decl.important) {
|
||||
item.style.setProperty(decl.prop, decl.value);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (err.message && err.message.includes('is not a valid selector')) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (root.tagName === 'svg') {
|
||||
return;
|
||||
}
|
||||
|
||||
let element = root.firstElementChild;
|
||||
while (element) {
|
||||
applyStyle(element as HTMLElement, cssRoot);
|
||||
element = element.nextElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCSS(html: string, css: string) {
|
||||
const doc = sanitizeHTMLToDom(html);
|
||||
const root = doc.firstChild as HTMLElement;
|
||||
const cssRoot = postcss.parse(css);
|
||||
applyStyle(root, cssRoot);
|
||||
return root.outerHTML;
|
||||
}
|
||||
|
||||
export function uevent(name: string) {
|
||||
const url = `https://u.sunboshi.tech/event?name=${name}&platform=${PlugPlatform}&v=${PluginVersion}`;
|
||||
requestUrl(url).then().catch(error => {
|
||||
console.error("Failed to send event: " + url, error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个防抖函数
|
||||
* @param func 要执行的函数
|
||||
* @param wait 等待时间(毫秒)
|
||||
* @returns 防抖处理后的函数
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
return function(this: any, ...args: Parameters<T>) {
|
||||
const context = this;
|
||||
|
||||
const later = () => {
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
};
|
||||
|
||||
if (timeout !== null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
export function cleanUrl(href: string) {
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
export async function waitForLayoutReady(app: App): Promise<void> {
|
||||
if (app.workspace.layoutReady) {
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
app.workspace.onLayoutReady(() => resolve());
|
||||
});
|
||||
}
|
||||
58
src/wasm/wasm.ts
Normal file
58
src/wasm/wasm.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 AssetsManager from "../assets";
|
||||
require('./wasm_exec.js');
|
||||
|
||||
declare class Go {
|
||||
argv: string[];
|
||||
env: { [envKey: string]: string };
|
||||
exit: (code: number) => void;
|
||||
importObject: WebAssembly.Imports;
|
||||
exited: boolean;
|
||||
mem: DataView;
|
||||
run(instance: WebAssembly.Instance): Promise<void>;
|
||||
}
|
||||
|
||||
let WasmLoaded = false;
|
||||
|
||||
export function IsWasmReady() {
|
||||
return WasmLoaded;
|
||||
}
|
||||
|
||||
export async function LoadWasm() {
|
||||
if (WasmLoaded) {
|
||||
return;
|
||||
}
|
||||
const assets = AssetsManager.getInstance();
|
||||
const wasmContent = await assets.loadWasm();
|
||||
|
||||
if (!wasmContent) {
|
||||
console.error('WASM content not found');
|
||||
// throw new Error('WASM content not found');
|
||||
return;
|
||||
}
|
||||
const go = new Go();
|
||||
const ret = await WebAssembly.instantiate(wasmContent, go.importObject);
|
||||
go.run(ret.instance);
|
||||
WasmLoaded = true;
|
||||
}
|
||||
561
src/wasm/wasm_exec.js
Normal file
561
src/wasm/wasm_exec.js
Normal file
@@ -0,0 +1,561 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substring(0, nl));
|
||||
outputBuf = outputBuf.substring(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const setInt32 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
},
|
||||
gojs: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8),
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
228
src/weixin-api.ts
Normal file
228
src/weixin-api.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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 { requestUrl, RequestUrlParam, getBlobArrayBuffer } from "obsidian";
|
||||
|
||||
const PluginHost = 'https://obplugin.sunboshi.tech';
|
||||
|
||||
// 获取token
|
||||
export async function wxGetToken(authkey:string, appid:string, secret:string) {
|
||||
const url = PluginHost + '/v1/wx/token';
|
||||
const body = {
|
||||
authkey,
|
||||
appid,
|
||||
secret
|
||||
}
|
||||
const res = await requestUrl({
|
||||
url,
|
||||
method: 'POST',
|
||||
throw: false,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function wxEncrypt(authkey:string, wechat:any[]) {
|
||||
const url = PluginHost + '/v1/wx/encrypt';
|
||||
const body = JSON.stringify({
|
||||
authkey,
|
||||
wechat
|
||||
});
|
||||
const res = await requestUrl({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
throw: false,
|
||||
contentType: 'application/json',
|
||||
body: body
|
||||
});
|
||||
return res
|
||||
}
|
||||
|
||||
export async function wxKeyInfo(authkey:string) {
|
||||
const url = PluginHost + '/v1/wx/info/' + authkey;
|
||||
const res = await requestUrl({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
throw: false,
|
||||
contentType: 'application/json',
|
||||
});
|
||||
return res
|
||||
}
|
||||
|
||||
export async function wxWidget(authkey: string, params: string) {
|
||||
const host = 'https://obplugin.sunboshi.tech';
|
||||
const path = '/math/widget';
|
||||
const url = `${host}${path}`;
|
||||
try {
|
||||
const res = await requestUrl({
|
||||
url,
|
||||
throw: false,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
authkey
|
||||
},
|
||||
body: params
|
||||
})
|
||||
if (res.status === 200) {
|
||||
return res.json.content;
|
||||
}
|
||||
return res.json.msg;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
export async function wxUploadImage(data: Blob, filename: string, token: string, type?: string) {
|
||||
let url = '';
|
||||
if (type == null || type === '') {
|
||||
url = 'https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=' + token;
|
||||
} else {
|
||||
url = `https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=${type}`
|
||||
}
|
||||
|
||||
const N = 16 // The length of our random boundry string
|
||||
const randomBoundryString = "djmangoBoundry" + Array(N+1).join((Math.random().toString(36)+'00000000000000000').slice(2, 18)).slice(0, N)
|
||||
|
||||
// Construct the form data payload as a string
|
||||
const pre_string = `------${randomBoundryString}\r\nContent-Disposition: form-data; name="media"; filename="${filename}"\r\nContent-Type: "application/octet-stream"\r\n\r\n`;
|
||||
const post_string = `\r\n------${randomBoundryString}--`
|
||||
|
||||
// Convert the form data payload to a blob by concatenating the pre_string, the file data, and the post_string, and then return the blob as an array buffer
|
||||
const pre_string_encoded = new TextEncoder().encode(pre_string);
|
||||
// const data = file;
|
||||
const post_string_encoded = new TextEncoder().encode(post_string);
|
||||
const concatenated = await new Blob([pre_string_encoded, await getBlobArrayBuffer(data), post_string_encoded]).arrayBuffer()
|
||||
|
||||
// Now that we have the form data payload as an array buffer, we can pass it to requestURL
|
||||
// We also need to set the content type to multipart/form-data and pass in the boundry string
|
||||
const options: RequestUrlParam = {
|
||||
method: 'POST',
|
||||
url: url,
|
||||
contentType: `multipart/form-data; boundary=----${randomBoundryString}`,
|
||||
body: concatenated
|
||||
};
|
||||
|
||||
const res = await requestUrl(options);
|
||||
const resData = await res.json;
|
||||
return {
|
||||
url: resData.url || '',
|
||||
media_id: resData.media_id || '',
|
||||
errcode: resData.errcode || 0,
|
||||
errmsg: resData.errmsg || '',
|
||||
}
|
||||
}
|
||||
|
||||
// 新建草稿
|
||||
export interface DraftArticle {
|
||||
title: string;
|
||||
author?: string;
|
||||
digest?: string;
|
||||
cover?: string;
|
||||
content: string;
|
||||
content_source_url?: string;
|
||||
thumb_media_id: string;
|
||||
need_open_comment?: number;
|
||||
only_fans_can_comment?: number;
|
||||
pic_crop_235_1?: string;
|
||||
pic_crop_1_1?: string;
|
||||
appid?: string;
|
||||
theme?: string;
|
||||
highlight?: string;
|
||||
}
|
||||
|
||||
export async function wxAddDraft(token: string, data: DraftArticle) {
|
||||
const url = 'https://api.weixin.qq.com/cgi-bin/draft/add?access_token=' + token;
|
||||
const body = {articles:[{
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
digest: data.digest,
|
||||
thumb_media_id: data.thumb_media_id,
|
||||
... data.pic_crop_235_1 && {pic_crop_235_1: data.pic_crop_235_1},
|
||||
... data.pic_crop_1_1 && {pic_crop_1_1: data.pic_crop_1_1},
|
||||
... data.content_source_url && {content_source_url: data.content_source_url},
|
||||
... data.need_open_comment !== undefined && {need_open_comment: data.need_open_comment},
|
||||
... data.only_fans_can_comment !== undefined && {only_fans_can_comment: data.only_fans_can_comment},
|
||||
... data.author && {author: data.author},
|
||||
}]};
|
||||
|
||||
const res = await requestUrl({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
throw: false,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export interface DraftImageMediaId {
|
||||
image_media_id: string;
|
||||
}
|
||||
|
||||
export interface DraftImageInfo {
|
||||
image_list: DraftImageMediaId[];
|
||||
}
|
||||
|
||||
export interface DraftImages {
|
||||
article_type: string;
|
||||
title: string;
|
||||
content: string;
|
||||
need_open_commnet: number;
|
||||
only_fans_can_comment: number;
|
||||
image_info: DraftImageInfo;
|
||||
}
|
||||
|
||||
export async function wxAddDraftImages(token: string, data: DraftImages) {
|
||||
const url = 'https://api.weixin.qq.com/cgi-bin/draft/add?access_token=' + token;
|
||||
const body = {articles:[data]};
|
||||
|
||||
const res = await requestUrl({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
throw: false,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function wxBatchGetMaterial(token: string, type: string, offset: number = 0, count: number = 10) {
|
||||
const url = 'https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=' + token;
|
||||
const body = {
|
||||
type,
|
||||
offset,
|
||||
count
|
||||
};
|
||||
|
||||
const res = await requestUrl({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
throw: false,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return await res.json;
|
||||
}
|
||||
77
src/widgets-modal.ts
Normal file
77
src/widgets-modal.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 { App, Modal, MarkdownView } from "obsidian";
|
||||
import { uevent } from "./utils";
|
||||
|
||||
export class WidgetsModal extends Modal {
|
||||
listener: any = null;
|
||||
url: string = 'https://widgets.sunboshi.tech';
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
insertMarkdown(markdown: string) {
|
||||
const editor = this.app.workspace.getActiveViewOfType(MarkdownView)?.editor;
|
||||
if (!editor) return;
|
||||
editor.replaceSelection(markdown);
|
||||
editor.exec("goRight");
|
||||
uevent('insert-widgets');
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
let { contentEl, modalEl } = this;
|
||||
modalEl.style.width = '640px';
|
||||
modalEl.style.height = '500px';
|
||||
const iframe = contentEl.createEl('iframe', {
|
||||
attr: {
|
||||
src: this.url,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
allow: 'clipboard-read; clipboard-write',
|
||||
},
|
||||
});
|
||||
|
||||
iframe.style.border = 'none';
|
||||
|
||||
this.listener = this.handleMessage.bind(this);
|
||||
window.addEventListener('message', this.listener);
|
||||
uevent('open-widgets');
|
||||
}
|
||||
|
||||
handleMessage(event: MessageEvent) {
|
||||
if (event.origin === this.url) {
|
||||
const { type, data } = event.data;
|
||||
if (type === 'cmd') {
|
||||
this.insertMarkdown(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.listener) {
|
||||
window.removeEventListener('message', this.listener);
|
||||
}
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
162
styles.css
Normal file
162
styles.css
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* =========================================================== */
|
||||
/* UI 样式 */
|
||||
/* =========================================================== */
|
||||
.note-preview {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.render-div {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.preview-toolbar {
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
border-bottom: #e4e4e4 1px solid;
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.toolbar-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin: 10px 10px;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.upload-input {
|
||||
margin-left: 10px;
|
||||
visibility: hidden;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.style-label {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.style-select {
|
||||
margin-right: 10px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.msg-view {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--background-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.msg-title {
|
||||
margin-bottom: 20px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.note-mpcard-wrapper {
|
||||
margin: 20px 20px;
|
||||
background-color: rgb(250, 250, 250);
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.note-mpcard-content {
|
||||
display: flex;
|
||||
}
|
||||
.note-mpcard-headimg {
|
||||
border: none !important;
|
||||
border-radius: 27px !important;
|
||||
box-shadow: none !important;
|
||||
width: 54px !important;
|
||||
height: 54px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.note-mpcard-info {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.note-mpcard-nickname {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.note-mpcard-signature {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.note-mpcard-foot {
|
||||
margin-top: 20px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #ececec;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.loading-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px; /* 可调整大小 */
|
||||
height: 50px;
|
||||
border: 4px solid #fcd6ff; /* 底色,浅灰 */
|
||||
border-top: 4px solid #bb0cdf; /* 主色,蓝色顶部产生旋转感 */
|
||||
border-radius: 50%; /* 圆形 */
|
||||
animation: spin 1s linear infinite; /* 旋转动画 */
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
71
tools/download.mjs
Normal file
71
tools/download.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 https from 'node:https';
|
||||
import { exec } from 'node:child_process';
|
||||
|
||||
// 仓库信息
|
||||
const owner = 'sunbooshi';
|
||||
const repo = 'mweb-themes';
|
||||
const assetName = 'assets.zip'; // 要下载的文件名
|
||||
|
||||
// GitHub API 获取最新 Release 信息
|
||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
||||
|
||||
https.get(apiUrl, { headers: { 'User-Agent': 'Node.js' } }, (apiRes) => {
|
||||
let data = '';
|
||||
|
||||
// 接收 API 响应数据
|
||||
apiRes.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
apiRes.on('end', () => {
|
||||
try {
|
||||
const releaseInfo = JSON.parse(data);
|
||||
|
||||
// 查找 assets.zip 文件
|
||||
const asset = releaseInfo.assets.find((a) => a.name === assetName);
|
||||
|
||||
if (!asset) {
|
||||
console.error(`未找到 ${assetName} 文件`);
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadUrl = asset.browser_download_url;
|
||||
console.log(`找到 ${assetName},下载链接: ${downloadUrl}`);
|
||||
|
||||
// 使用系统 wget 命令下载
|
||||
exec(`wget "${downloadUrl}" -O "${assetName}"`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`下载失败: ${error}`);
|
||||
return;
|
||||
}
|
||||
console.log(`${assetName} 下载完成!`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('解析 API 响应失败:', err);
|
||||
}
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
console.error('请求 GitHub API 失败:', err);
|
||||
});
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2018",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"1.3.0": "1.4.5"
|
||||
}
|
||||
Reference in New Issue
Block a user