diff --git a/mengyanote-backend/Dockerfile b/mengyanote-backend/Dockerfile index dd422bc..5522d6b 100644 --- a/mengyanote-backend/Dockerfile +++ b/mengyanote-backend/Dockerfile @@ -15,9 +15,11 @@ COPY requirements.txt . # 安装 Python 依赖 RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple -# 复制应用代码 +# 复制应用代码(笔记目录在仓库中为 data/mengyanote,运行时挂载到 /app/mengyanote 与 main.py 一致) COPY main.py . -COPY mengyanote ./mengyanote +COPY data/mengyanote ./mengyanote +# 默认配置(config.json 含 admin_token;ignore.json)。部署可用卷覆盖整个目录 +COPY data/config ./data/config # 暴露端口 EXPOSE 8000 diff --git a/mengyanote-backend/data/config/config.json b/mengyanote-backend/data/config/config.json new file mode 100644 index 0000000..ed43571 --- /dev/null +++ b/mengyanote-backend/data/config/config.json @@ -0,0 +1,3 @@ +{ + "admin_token":"shumengya520" +} \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/ignore.json b/mengyanote-backend/data/config/ignore.json similarity index 82% rename from mengyanote-backend/data/mengyanote/ignore.json rename to mengyanote-backend/data/config/ignore.json index 52c1c90..7ba3254 100644 --- a/mengyanote-backend/data/mengyanote/ignore.json +++ b/mengyanote-backend/data/config/ignore.json @@ -1,17 +1,18 @@ -{ - "ignore": [ - ".obsidian", - ".trash", - "华为ICT", - "鉴政", - "临时解决方案", - "树萌芽的小本本", - "树萌芽制作的小东西", - "树萌芽の小想法", - "西南石油大学", - "临时", - "树萌芽の喃喃自语", - "树萌芽の小本本", - "树萌芽の收藏夹" - ] -} +{ + "ignore": [ + ".obsidian", + ".trash", + "临时", + "临时解决方案", + "华为ICT", + "树萌芽の喃喃自语", + "树萌芽の小想法", + "树萌芽の小本本", + "树萌芽の收藏夹", + "树萌芽制作的小东西", + "树萌芽的小本本", + "西南石油大学", + "鉴政", + "项目策划" + ] +} \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/app.json b/mengyanote-backend/data/mengyanote/.obsidian/app.json index c769a06..c9b14c3 100644 --- a/mengyanote-backend/data/mengyanote/.obsidian/app.json +++ b/mengyanote-backend/data/mengyanote/.obsidian/app.json @@ -4,5 +4,11 @@ "strictLineBreaks": false, "showLineNumber": false, "rightToLeft": false, - "spellcheck": false + "spellcheck": false, + "autoConvertHtml": true, + "autoPairBrackets": false, + "autoPairMarkdown": false, + "smartIndentList": false, + "useTab": true, + "tabSize": 4 } \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/appearance.json b/mengyanote-backend/data/mengyanote/.obsidian/appearance.json index 37e1a5a..468f509 100644 --- a/mengyanote-backend/data/mengyanote/.obsidian/appearance.json +++ b/mengyanote-backend/data/mengyanote/.obsidian/appearance.json @@ -1,10 +1,13 @@ { "cssTheme": "Phycat", "theme": "moonstone", - "monospaceFontFamily": "霞鹜文楷,Maple Mono NF CN", + "monospaceFontFamily": "Maple Mono CN,霞鹜文楷", "baseFontSize": 15, - "interfaceFontFamily": "Maple Mono NF CN", - "textFontFamily": "霞鹜文楷,Maple Mono NF CN", + "interfaceFontFamily": "霞鹜文楷", + "textFontFamily": "霞鹜文楷", "nativeMenus": false, - "accentColor": "#ffbb00" + "accentColor": "#ffbb00", + "enabledCssSnippets": [ + "md" + ] } \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/community-plugins.json b/mengyanote-backend/data/mengyanote/.obsidian/community-plugins.json index 7511a0a..07171c0 100644 --- a/mengyanote-backend/data/mengyanote/.obsidian/community-plugins.json +++ b/mengyanote-backend/data/mengyanote/.obsidian/community-plugins.json @@ -4,6 +4,6 @@ "remotely-save", "obsidian42-brat", "editing-toolbar", - "obsidian-icon-folder", - "tray" + "tray", + "obsidian-regex-replace" ] \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/editing-toolbar/data.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/editing-toolbar/data.json index e8526ab..2bb001b 100644 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/editing-toolbar/data.json +++ b/mengyanote-backend/data/mengyanote/.obsidian/plugins/editing-toolbar/data.json @@ -404,7 +404,7 @@ "cMenuNumRows": 12, "cMenuWidth": 610, "cMenuFontColor": "#c00000", - "cMenuBackgroundColor": "#d3f8b6", + "cMenuBackgroundColor": "#b1ffff", "autohide": false, "Iscentered": false, "custom_bg1": "#FFB78B8C", diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/data.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/data.json deleted file mode 100644 index 867ce2e..0000000 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/data.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "LatestVersion": "1.6.0", - "currentWallpaper": { - "path": "plugins/live-wallpaper/wallpapers/active/normal/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png", - "type": "image", - "zIndex": 5, - "opacity": 25, - "brightness": 130, - "blurRadius": 4, - "contrast": 105, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 0 - }, - "globalConfig": { - "config": { - "path": "plugins/live-wallpaper/wallpapers/active/normal/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png", - "type": "image", - "zIndex": 5, - "opacity": 25, - "brightness": 130, - "blurRadius": 4, - "contrast": 105, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 0 - }, - "enabled": true - }, - "Preview": false, - "WallpaperConfigs": [ - { - "path": "plugins/live-wallpaper/wallpapers/active/normal/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 0 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 1 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 2 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 3 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 4 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 5 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 6 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 7 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 8 - }, - { - "path": "", - "type": "image", - "zIndex": 5, - "opacity": 40, - "brightness": 100, - "blurRadius": 8, - "contrast": 100, - "playbackSpeed": 1, - "Reposition": false, - "Quality": false, - "useObjectFit": true, - "position": "Center", - "positionX": 50, - "positionY": 50, - "Scale": 1, - "Index": 9 - } - ], - "HistoryPaths": [ - { - "path": "plugins/live-wallpaper/wallpapers/history/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png", - "type": "image", - "fileName": "wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png" - } - ], - "mobileBackgroundWidth": "100vw", - "mobileBackgroundHeight": "100vh", - "AdnvOpend": false, - "modalStyle": { - "effect": "blur+dim", - "blurRadius": 10, - "dimOpacity": 0.7, - "dimColor": "black", - "disableModalBg": false - }, - "TextArenas": [ - { - "attribute": "" - } - ], - "Color": "#000000", - "INBUILD": false, - "SizeLimited": true, - "ScheduledOptions": { - "dayNightMode": false, - "weekly": false, - "autoSwitch": false, - "dayStartTime": "08:00", - "nightStartTime": "20:00", - "intervalCheckTime": "00:10", - "isCustomInterval": false - }, - "migrated": false -} \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/main.js b/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/main.js deleted file mode 100644 index 647279d..0000000 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/main.js +++ /dev/null @@ -1,2345 +0,0 @@ -"use strict"; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// src/main.ts -var main_exports = {}; -__export(main_exports, { - DEFAULT_SETTINGS: () => DEFAULT_SETTINGS, - default: () => LiveWallpaperPlugin2, - defaultWallpaper: () => defaultWallpaper -}); -module.exports = __toCommonJS(main_exports); - -// src/Styles/TextArenaStyles.ts -function LoadOrUnloadChanges(doc, TextArenas, load) { - const el = doc.body.classList.contains("theme-dark") || doc.body.classList.contains("theme-light") ? doc.body : doc.documentElement; - if (!el) return; - for (const { attribute } of TextArenas) { - try { - const attr = attribute?.trim(); - if (!attr) continue; - const isVar = attr.startsWith("--"); - if (isVar) { - if (load) { - el.style.setProperty(attr, "transparent", "important"); - } else { - el.style.removeProperty(attr); - } - continue; - } - if (load) { - el.style.setProperty(attr, "transparent", "important"); - } else { - el.style.removeProperty(attr); - if (!el.getAttribute("style")) { - el.removeAttribute("style"); - } - } - } catch (error) { - console.error("Error processing element:", { attribute }, error); - } - } -} -function ApplyChanges(doc, TextArenas, id) { - const { attribute } = TextArenas[id]; - const attr = attribute.trim(); - const isVar = attr.startsWith("--"); - let el = null; - if (isVar) { - el = doc.body.classList.contains("theme-dark") || doc.body.classList.contains("theme-light") ? doc.body : doc.documentElement; - } else { - el = doc.body; - } - if (!el) return; - el.style.setProperty(attr, "transparent", "important"); -} -function RemoveChanges(doc, TextArenas, id, oldAttribute) { - if (id < 0 || id >= TextArenas.length) return; - const attribute = (oldAttribute ?? TextArenas[id].attribute)?.trim(); - const el = doc.body.classList.contains("theme-dark") || doc.body.classList.contains("theme-light") ? doc.body : doc.documentElement; - if (!attribute || !el) return; - try { - el.style.removeProperty(attribute); - if (!el.getAttribute("style")) { - el.removeAttribute("style"); - } - } catch (error) { - console.error(`Error removing '${attribute}' at index ${id}:`, error); - } -} - -// src/Scheduler.ts -var Scheduler = class { - static ValidateText(text) { - const timePattern = /^(?:[01]?\d|2[0-3])(?::[0-5]\d){1,2}$/; - return timePattern.test(text); - } - static Check(options, exceptKey) { - const BOOLEAN_KEYS = [ - "dayNightMode", - "weekly", - "autoSwitch" - ]; - return BOOLEAN_KEYS.some((k) => { - if (k === exceptKey) return false; - const val = options[k]; - return val === true || val === "true"; - }); - } - static getIntervalInMs(options) { - const timeStr = options.intervalCheckTime ?? "00:10"; - const [hh, mm] = timeStr.split(":").map(Number); - return (hh * 60 + mm) * 60 * 1e3; - } -}; - -// src/WallpaperConfigUtils.ts -var WallpaperConfigUtils = class _WallpaperConfigUtils { - static async GetCurrentConfig(Plugin2) { - const Settings = Plugin2.settings; - const AnyScheduledWallpaperEnabled = Scheduler.Check(Settings.ScheduledOptions); - if (Settings.Preview) return Plugin2.settings.currentWallpaper; - else if (AnyScheduledWallpaperEnabled) { - const index = this.getWallpaperIndex(Plugin2); - if (index == void 0) { - Plugin2.settings.WallpaperConfigs = _WallpaperConfigUtils.NewConfig(Plugin2.settings.WallpaperConfigs); - Plugin2.saveSettings(); - return Plugin2.settings.WallpaperConfigs[Plugin2.settings.WallpaperConfigs.length - 1]; - } - if (Settings.globalConfig.enabled) { - return _WallpaperConfigUtils.applyGlobalConfig(Settings.WallpaperConfigs[index], Settings.globalConfig.config); - } - return Settings.WallpaperConfigs[index]; - } - if (Settings.globalConfig.enabled) return _WallpaperConfigUtils.applyGlobalConfig(Settings.WallpaperConfigs[0], Settings.globalConfig.config); - return Settings.WallpaperConfigs[0]; - } - static getWallpaperIndex(Plugin2) { - const now = /* @__PURE__ */ new Date(); - const options = Plugin2.settings.ScheduledOptions; - if (options.dayNightMode) { - const currentHour = now.getHours(); - const currentMinute = now.getMinutes(); - const currentTime = currentHour * 60 + currentMinute; - const [dayHour, dayMinute] = options.dayStartTime.split(":").map(Number); - const [nightHour, nightMinute] = options.nightStartTime.split(":").map(Number); - const dayTime = dayHour * 60 + dayMinute; - const nightTime = nightHour * 60 + nightMinute; - const isDay = dayTime < nightTime ? currentTime >= dayTime && currentTime < nightTime : currentTime >= dayTime || currentTime < nightTime; - return isDay ? 1 : 2; - } else if (options.weekly) { - let day = now.getDay(); - day = (day + 6) % 7 + 1; - return day + 2; - } else if (options.autoSwitch) { - const min = 10; - const max = Plugin2.settings.WallpaperConfigs.length - 1; - if (min > max) return void 0; - const range = max - min + 1; - const index = ((Plugin2.settings.currentWallpaper.Index + 1 - min) % range + range) % range + min; - return index; - } - return 1; - } - static getPaths(slotIndex, Configs) { - const configs = Configs; - if (slotIndex >= 1 && slotIndex <= 2) { - return configs.slice(1, 3).map((cfg) => cfg.path); - } else if (slotIndex <= 9) { - return configs.slice(3, 10).map((cfg) => cfg.path); - } else { - return configs.slice(10, Configs.length).map((cfg) => cfg.path); - } - } - static getPathAndType(slotIndex, Configs) { - const configs = Configs; - if (slotIndex >= 1 && slotIndex <= 2) { - return configs.slice(1, 3).map((cfg) => ({ path: cfg.path, type: cfg.type })); - } else if (slotIndex <= 9) { - return configs.slice(3, 10).map((cfg) => ({ path: cfg.path, type: cfg.type })); - } else { - return configs.slice(10, Configs.length).map((cfg) => ({ path: cfg.path, type: cfg.type })); - } - } - static applyGlobalConfig(config, globalConfig) { - globalConfig.path = config.path; - globalConfig.type = config.type; - globalConfig.Index = config.Index; - return globalConfig; - } - static computeActiveSubfolder(slotIndex) { - if (slotIndex >= 10) return "autoSwitch"; - else if (slotIndex > 2) return "weekly"; - else if (slotIndex != 0) return "daily"; - return "normal"; - } - static NewConfig(configs) { - return [ - ...configs, - { - ...defaultWallpaper, - Index: configs.length - } - ]; - } - static RemoveConfig(configs, config) { - return configs.filter((c) => c.Index !== config.Index).map((c, i) => ({ ...c, Index: i })); - } - static ClearConfigsFromIndex(configs, startIndex) { - return configs.filter((c) => c.Index < startIndex); - } -}; - -// src/Wallpaper/mediaUtils.ts -function UpdatePaths(plugin, Args) { - plugin.lastPath = Args.path; - plugin.lastType = Args.type; -} -async function GetConfig(plugin, skipConfigReload) { - try { - if (skipConfigReload) { - return plugin.settings.currentWallpaper; - } - return await WallpaperConfigUtils.GetCurrentConfig(plugin); - } catch (err) { - console.error("Error while accessing wallpaper config:", err); - return void 0; - } -} -function GetFileName(FilePath) { - return FilePath.substring(FilePath.lastIndexOf("/") + 1); -} -async function waitForMediaDimensions(element, timeout = 2e3) { - if (element instanceof HTMLImageElement) { - if (element.complete && element.naturalWidth > 0) return; - } else { - if (element.videoWidth > 0) return; - } - return Promise.race([ - new Promise((resolve) => { - if (element instanceof HTMLImageElement) { - element.addEventListener("load", () => resolve(), { once: true }); - element.addEventListener("error", () => resolve(), { once: true }); - } else { - element.addEventListener("loadedmetadata", () => resolve(), { once: true }); - element.addEventListener("error", () => resolve(), { once: true }); - } - }), - new Promise( - (resolve) => setTimeout(() => resolve(), timeout) - ) - ]); -} - -// src/Wallpaper/wallpaperDom.ts -function removeExistingWallpaperElements(doc) { - const existingContainer = doc.getElementById("live-wallpaper-container"); - const existingStyles = doc.getElementById("live-wallpaper-overrides"); - const existingTitlebarStyles = doc.getElementById("live-wallpaper-titlebar-styles"); - existingContainer?.remove(); - existingStyles?.remove(); - existingTitlebarStyles?.remove(); - doc.body.classList.remove("live-wallpaper-active"); -} -function createWallpaperContainer(doc, currentConfig, adnvOpened) { - const container = doc.createElement("div"); - container.id = "live-wallpaper-container"; - Object.assign(container.style, { - position: "fixed", - top: "0", - left: "0", - width: "100vw", - height: "100vh", - overflow: "hidden", - pointerEvents: "none", - filter: `blur(${currentConfig.blurRadius}px) brightness(${currentConfig.brightness}%) contrast(${currentConfig.contrast}%)` - }); - if (adnvOpened) { - Object.assign(container.style, { - opacity: `1`, - zIndex: `0` - }); - } else { - Object.assign(container.style, { - opacity: `${Math.min(currentConfig.opacity / 100, 0.8)}`, - zIndex: `${currentConfig.zIndex}` - }); - } - return container; -} -function ChangeWallpaperContainer(doc, size) { - const container = doc.getElementById("live-wallpaper-container"); - if (container == null) return; - const width = size.width || "100vw"; - const height = size.height || "100vh"; - Object.assign(container.style, { - width, - height - }); -} -function applyContainerEffects(container, currentConfig, adnvOpened) { - if (adnvOpened) { - Object.assign(container.style, { - opacity: `1`, - zIndex: `0` - }); - } else { - Object.assign(container.style, { - opacity: `${Math.min(currentConfig.opacity / 100, 0.8)}`, - zIndex: `${currentConfig.zIndex}` - }); - } - Object.assign(container.style, { - filter: `blur(${currentConfig.blurRadius}px) brightness(${currentConfig.brightness}%) contrast(${currentConfig.contrast}%)` - }); -} - -// src/Wallpaper/wallpaperMedia.ts -async function createMediaElement(doc, plugin) { - const { currentWallpaper } = plugin.settings; - const isVideo = currentWallpaper.type === "video"; - const media = isVideo ? doc.createElement("video") : doc.createElement("img"); - media.id = "live-wallpaper-media"; - if (media instanceof HTMLImageElement) { - media.loading = "lazy"; - } - const path = `${plugin.app.vault.configDir}/${currentWallpaper.path}`; - const exists = await plugin.app.vault.adapter.exists(path); - if (!exists) { - currentWallpaper.path = ""; - return null; - } - media.src = plugin.app.vault.adapter.getResourcePath(path); - applyMediaStyles(media, currentWallpaper); - if (isVideo) { - const video = media; - video.autoplay = true; - video.loop = true; - video.muted = true; - video.playbackRate = currentWallpaper.playbackSpeed; - } - return media; -} -function applyMediaStyles(media, config) { - media.removeAttribute("style"); - Object.assign(media.style, { - width: "100%", - height: "100%", - position: "absolute", - objectFit: config.useObjectFit ? "unset" : "cover", - ...config.Reposition && { - objectPosition: config.position - } - }); - if (config.Quality) { - Object.assign(media.style, { - imageRendering: "auto", - willChange: "transform", - overflowClipMargin: "unset", - overflow: "clip" - }); - } -} - -// src/Wallpaper/WallpaperApplier.ts -var import_obsidian = require("obsidian"); -var WallpaperApplier = class _WallpaperApplier { - static async applyWallpaper(plugin, skipConfigReload = false, doc) { - const config = await GetConfig(plugin, skipConfigReload); - if (!config) { - return false; - } - plugin.settings.currentWallpaper = config; - if (plugin.settings.ScheduledOptions.dayNightMode || plugin.settings.ScheduledOptions.autoSwitch) { - plugin.startDayNightWatcher(); - } else { - plugin.stopDayNightWatcher(); - } - if (!plugin.settings.currentWallpaper || !plugin.settings.currentWallpaper.path) { - new import_obsidian.Notice("No wallpaper path defined, skipping applyWallpaper."); - return false; - } - const newPath = plugin.settings.currentWallpaper.path; - const newType = plugin.settings.currentWallpaper.type; - const container = doc.getElementById("live-wallpaper-container"); - let media = doc.getElementById("live-wallpaper-media"); - if (container && media) { - applyContainerEffects(container, plugin.settings.currentWallpaper, plugin.settings.AdnvOpend); - if (media.tagName === "VIDEO") { - const video = media; - video.playbackRate = plugin.settings.currentWallpaper.playbackSpeed; - } - if (newPath !== plugin.lastPath || newType !== plugin.lastType) { - const newMedia = await createMediaElement(doc, plugin); - if (newMedia) { - _WallpaperApplier.applyNewMedia(newMedia, container); - media = newMedia; - } - } - if (plugin.settings.currentWallpaper.Reposition) { - await SettingsUtils.applyImagePosition( - media, - plugin.settings.currentWallpaper.positionX, - plugin.settings.currentWallpaper.positionY, - plugin.settings.currentWallpaper.Scale - ); - } - return true; - } - await plugin.CreateMedia(doc); - return true; - } - static async applyNewMedia(newMedia, container) { - await waitForMediaDimensions(newMedia); - newMedia.style.opacity = "0"; - newMedia.style.transition = "opacity 1s ease-in-out"; - container.appendChild(newMedia); - await new Promise( - (resolve) => requestAnimationFrame(() => resolve()) - ); - await new Promise((resolve) => setTimeout(resolve, 20)); - const medias = container.querySelectorAll( - '[id^="live-wallpaper-media"]' - ); - medias.forEach((el, i) => { - if (i < medias.length - 1) { - const htmlEl = el; - htmlEl.style.transition = "opacity 1s ease-in-out"; - htmlEl.style.opacity = "0"; - newMedia.style.opacity = "1"; - setTimeout(() => { - if (htmlEl.parentElement) { - htmlEl.remove(); - } - }, 3e3); - } - }); - } -}; - -// src/Settings/SettingsUtils.ts -var import_obsidian2 = require("obsidian"); -var SettingsUtils = class { - static AttributeValid(attribute) { - const attr = attribute.trim(); - if (attr === "") return false; - if (attr.startsWith("--")) return true; - return false; - } - static TargetValid(target) { - const trimmed = target.trim(); - if (trimmed === "") return false; - try { - document.createDocumentFragment().querySelector(trimmed); - return true; - } catch { - return false; - } - } - static async getWallpaperPath(plugin, Index) { - const settings = plugin.settings; - const baseDir = plugin.app.vault.configDir; - let path = ""; - const config = settings.WallpaperConfigs[Index]; - path = `${baseDir}/${config.path}`; - return path; - } - static async getPathExists(plugin, relativePath) { - if (!relativePath || relativePath.trim() === "") { - return false; - } - const fullPath = `${plugin.app.vault.configDir}/${relativePath}`; - return await this.wallpaperExists(plugin, fullPath); - } - static async wallpaperExists(plugin, path) { - return await plugin.app.vault.adapter.exists(path); - } - static async applyImagePosition(element, posXPercent, posYPercent, scaleFactor) { - if (element.parentElement === null) return; - const container = element.parentElement; - const containerWidth = container.clientWidth; - const containerHeight = container.clientHeight; - const isImage = element.tagName === "IMG"; - const naturalWidth = isImage ? element.naturalWidth : element.videoWidth; - const naturalHeight = isImage ? element.naturalHeight : element.videoHeight; - const minScale = Math.max( - containerWidth / naturalWidth, - containerHeight / naturalHeight - ); - const scale = Math.max(minScale, minScale * scaleFactor); - const scaledWidth = naturalWidth * scale; - const scaledHeight = naturalHeight * scale; - const maxOffsetX = (scaledWidth - containerWidth) / 2; - const maxOffsetY = (scaledHeight - containerHeight) / 2; - const offsetX = (posXPercent - 50) / 50 * maxOffsetX; - const offsetY = (posYPercent - 50) / 50 * maxOffsetY; - element.style.width = `${scaledWidth}px`; - element.style.height = `${scaledHeight}px`; - element.style.objectFit = "cover"; - element.style.position = "absolute"; - element.style.left = `${containerWidth / 2 - scaledWidth / 2 + offsetX}px`; - element.style.top = `${containerHeight / 2 - scaledHeight / 2 + offsetY}px`; - } - static enableReposition(plugin, doc) { - if (this.resizeHandler) return; - this.resizeHandler = () => { - const media = doc.getElementById("live-wallpaper-media"); - if (!media) return; - if (!plugin.settings.currentWallpaper.Reposition) { - applyMediaStyles(media, plugin.settings.currentWallpaper); - return; - } - this.applyImagePosition( - media, - plugin.settings.currentWallpaper.positionX ?? 50, - plugin.settings.currentWallpaper.positionY ?? 50, - plugin.settings.currentWallpaper.Scale ?? 1 - ); - }; - doc.win.addEventListener("resize", this.resizeHandler); - } - static disableReposition(window2) { - if (!this.resizeHandler) return; - window2.removeEventListener("resize", this.resizeHandler); - this.resizeHandler = null; - } - static SaveSettingsDebounced(plugin) { - return (0, import_obsidian2.debounce)(async () => { - await plugin.saveSettings(); - }, 300); - } - static ApplyWallpaperDebounced(plugin) { - return (0, import_obsidian2.debounce)(async (skipConfigReload = false) => { - for (const win of plugin.windows) { - await WallpaperApplier.applyWallpaper(plugin, skipConfigReload, win.document); - } - UpdatePaths(plugin, { path: plugin.settings.currentWallpaper.path, type: plugin.settings.currentWallpaper.type }); - }, 300); - } -}; -SettingsUtils.resizeHandler = null; - -// src/Styles/ModalStyles.ts -async function toggleModalStyles(doc, Plugin2) { - const styleId = "extrastyles-dynamic-css"; - let style = doc.getElementById(styleId); - if (Plugin2.settings.AdnvOpend) { - if (!style) { - style = doc.createElement("style"); - style.id = styleId; - doc.head.appendChild(style); - } - const { effect, blurRadius, dimOpacity, dimColor, disableModalBg } = Plugin2.settings.modalStyle; - let background = "transparent"; - let backdrop = "none"; - let extraCss = ""; - if (effect.includes("dim")) { - const color = dimColor === "white" ? "255, 255, 255" : "0, 0, 0"; - background = `rgba(${color}, ${dimOpacity})`; - } - if (effect.includes("blur")) { - backdrop = `blur(${blurRadius}px)`; - } - if (disableModalBg) { - extraCss += `.modal-bg { opacity: 0 !important; }`; - } - style.textContent = ` - .modal-container.mod-dim, - .modal-container { - background: ${background}; - backdrop-filter: ${backdrop}; - } - ${extraCss} - `; - } else { - style?.remove(); - } - const wallpaperExists = await SettingsUtils.getPathExists(Plugin2, Plugin2.settings.currentWallpaper.path); - if (!wallpaperExists) { - for (const win of Plugin2.windows) { - LoadOrUnloadChanges(win.document, Plugin2.settings.TextArenas, false); - } - return; - } else { - for (const win of Plugin2.windows) { - LoadOrUnloadChanges(win.document, Plugin2.settings.TextArenas, Plugin2.settings.AdnvOpend); - } - } -} -function RemoveModalStyles(doc) { - const styleId = "extrastyles-dynamic-css"; - const existingStyle = doc.getElementById(styleId); - existingStyle != null ? existingStyle.remove() : ""; -} - -// src/Styles/BackgroundColor.ts -async function applyBackgroundColor(doc, AdnvOpend, Color) { - const existingElement = doc.getElementById("live-wallpaper-container"); - if (existingElement) { - if (AdnvOpend && Color) { - existingElement.parentElement?.style.setProperty("background-color", Color, "important"); - } - return; - } - await new Promise((resolve) => { - const observer = new MutationObserver((mutations, obs) => { - const element = doc.getElementById("live-wallpaper-container"); - if (element) { - obs.disconnect(); - resolve(); - } - }); - observer.observe(doc.body, { - childList: true, - subtree: true - }); - }); - if (AdnvOpend && Color) { - const Main = doc.getElementById("live-wallpaper-container"); - Main?.parentElement?.style.setProperty("background-color", Color, "important"); - } -} -async function clearBackgroundColor(doc) { - const Main = doc.getElementById("live-wallpaper-container"); - Main?.parentElement?.style.removeProperty("background-color"); -} - -// src/FilePicker/fileUtils.ts -async function removeFileIfUnused(plugin, index, filetoRemoveName) { - const matches = WallpaperConfigUtils.getPaths(index, plugin.settings.WallpaperConfigs).filter( - (file) => file.split("/").pop() === filetoRemoveName.split("/").pop() - ); - if (matches.length !== 1) return; - await plugin.app.vault.adapter.remove(`.obsidian/${filetoRemoveName}`).catch(() => { - }); -} -async function removeUnusedFilesInFolder(plugin, folderPath, index, currentPath) { - const filesInFolder = await plugin.app.vault.adapter.list(folderPath); - const validFileNames = new Set( - WallpaperConfigUtils.getPathAndType(index, plugin.settings.WallpaperConfigs).map((cfg) => cfg.path?.split("/").pop()).filter((p) => !!p) - ); - const currentFileName = currentPath?.split("/").pop(); - for (const file of filesInFolder.files) { - const fileName = file.split("/").pop(); - if (!fileName) continue; - if (fileName === currentFileName) continue; - if (!validFileNames.has(fileName)) { - await plugin.app.vault.adapter.remove(file).catch(() => { - }); - } - } -} -async function removeAllExcept(plugin, dirPath, keepFilePath) { - const fullDirPath = `${plugin.app.vault.configDir}/${dirPath}`; - const files = await plugin.app.vault.adapter.list(fullDirPath).catch(() => null); - if (!files || !files.files) return; - for (const file of files.files) { - if (file !== `${plugin.app.vault.configDir}/${keepFilePath}`) { - await plugin.app.vault.adapter.remove(file).catch(() => { - }); - } - } -} - -// src/FilePicker/historyManager.ts -function prependHistory(history, entry) { - return [ - entry, - ...history.filter((e) => e.path !== entry.path) - ]; -} -async function trimHistory(plugin, max, folderPath) { - const filesInFolder = await plugin.app.vault.adapter.list(folderPath); - if (filesInFolder.files.length <= max) return; - const allowed = new Set( - plugin.settings.HistoryPaths.slice(0, max).map((e) => e.path.split("/").pop()).filter((p) => !!p) - ); - const toRemove = filesInFolder.files.filter((file) => { - const fileName = file.split("/").pop(); - return fileName && !allowed.has(fileName); - }); - plugin.settings.HistoryPaths = plugin.settings.HistoryPaths.slice(0, max); - for (const file of toRemove) { - await plugin.app.vault.adapter.remove(file).catch(() => { - }); - } -} -async function cleanInvalidWallpaperHistory(plugin) { - const validPaths = []; - for (const entry of plugin.settings.HistoryPaths) { - const fullPath = `${plugin.app.vault.configDir}/${entry.path}`; - const exists = await plugin.app.vault.adapter.exists(fullPath); - if (exists) { - validPaths.push(entry); - } - } - plugin.settings.HistoryPaths = validPaths; - await plugin.saveSettings(); -} - -// src/FilePicker/imageProcessing.ts -var import_obsidian3 = require("obsidian"); -async function resizeImageToBlob(file, options) { - const img = await createImageBitmap(file); - let MAX_WIDTH = options.maxWidth; - if (import_obsidian3.Platform.isMobile) { - const parsed = parseInt(options.mobileBackgroundWidth); - if (!isNaN(parsed)) { - MAX_WIDTH = parsed; - } - } - if (options.allowFullRes || img.width <= MAX_WIDTH) { - return new Blob([await file.arrayBuffer()], { type: file.type }); - } - const newWidth = MAX_WIDTH; - const newHeight = img.height / img.width * newWidth; - const canvas = new OffscreenCanvas(newWidth, newHeight); - const ctx = canvas.getContext("2d"); - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = "high"; - const bmp = await createImageBitmap(img, { - resizeWidth: newWidth, - resizeHeight: newHeight, - resizeQuality: "high" - }); - ctx.drawImage(bmp, 0, 0, newWidth, newHeight); - return canvas.convertToBlob({ quality: 0.8, type: "image/jpeg" }); -} - -// src/FilePicker/fileManager.ts -async function saveUnder(plugin, baseDir, subfolder, fileName, arrayBuffer) { - const dir = `${baseDir}/${subfolder}`; - await plugin.app.vault.adapter.mkdir(dir); - const fullPath = `${dir}/${fileName}`; - await plugin.app.vault.adapter.writeBinary(fullPath, arrayBuffer); - return `plugins/${plugin.manifest.id}/wallpapers/${subfolder}/${fileName}`; -} -async function getFileArrayBuffer(file, options) { - if (file.type.startsWith("image/")) { - const blob = await resizeImageToBlob(file, options); - return blob.arrayBuffer(); - } - return file.arrayBuffer(); -} - -// src/FilePicker/filePicker.ts -async function openFilePicker(plugin, slotIndex, isScheduledPicker = false, doc) { - const file = await pickSingleFile(doc); - if (!file) return; - try { - await plugin.applyWallpaperFile(file, slotIndex, doc, isScheduledPicker); - plugin.debouncedSave(); - } catch (error) { - alert("Could not save the file. Check disk permissions."); - console.error(error); - } -} -async function pickFolderFiles(doc, accept = ".jpg,.jpeg,.png,.gif,.mp4,.webm,.avif") { - return new Promise((resolve) => { - const input = doc.createElement("input"); - input.type = "file"; - input.accept = accept; - input.multiple = true; - input.webkitdirectory = true; - input.addEventListener("cancel", () => { - resolve(null); - }); - input.onchange = () => { - resolve(Array.from(input.files ?? [])); - }; - input.click(); - }); -} -function pickSingleFile(doc, accept = ".jpg,.jpeg,.png,.gif,.mp4,.webm,.avif") { - return new Promise((resolve) => { - const input = doc.createElement("input"); - input.type = "file"; - input.accept = accept; - input.multiple = false; - input.addEventListener("change", () => { - resolve(input.files?.[0] ?? null); - }); - input.addEventListener("cancel", () => { - resolve(null); - }); - input.click(); - }); -} -function validateWallpaperFile(file, SizeLimited) { - const allowedExtensions = ["jpg", "jpeg", "png", "gif", "mp4", "webm", "avif"]; - const extension = file.name.split(".").pop()?.toLowerCase(); - if (!extension || !allowedExtensions.includes(extension)) { - alert("Unsupported file type!"); - return false; - } - if (SizeLimited && file.size > 12 * 1024 * 1024) { - alert("File is too large (max 12MB)."); - return false; - } - return true; -} -function getWallpaperType(filename) { - const extension = filename.split(".").pop()?.toLowerCase(); - if (["mp4", "webm"].includes(extension || "")) return "video"; - if (extension === "gif") return "gif"; - return "image"; -} - -// src/main.ts -var import_obsidian8 = require("obsidian"); - -// src/Settings/SettingsManager.ts -var import_obsidian7 = require("obsidian"); - -// src/Settings/Settings.ts -var import_obsidian4 = require("obsidian"); -var positions = /* @__PURE__ */ new Map([ - [100, "Right"], - [0, "Left"], - [50, "Center"] -]); -var SettingsApp = class extends import_obsidian4.PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - display() { - const { containerEl } = this; - containerEl.empty(); - const setting = new import_obsidian4.Setting(containerEl).setName("Wallpaper source").setDesc("Select an image, GIF, or video file to use as your wallpaper"); - setting.addButton(async (btn) => { - const pathExists = await SettingsUtils.getPathExists( - this.plugin, - this.plugin.settings.currentWallpaper.path - ); - if (pathExists) { - btn.setIcon("circle-check").setTooltip("Wallpaper path exists"); - } else { - btn.setIcon("circle-x").setTooltip("Wallpaper path is missing"); - } - }); - setting.addButton( - (btn) => btn.setButtonText("History").setIcon("history").setClass("mod-cta").onClick(async () => { - containerEl.empty(); - await cleanInvalidWallpaperHistory(this.plugin); - this.plugin.settings.HistoryPaths.forEach((entry) => { - new import_obsidian4.Setting(containerEl).setName(entry.fileName).setDesc(entry.path).addButton((button) => { - button.setButtonText("Select").onClick(async () => { - this.plugin.settings.currentWallpaper = await WallpaperConfigUtils.GetCurrentConfig(this.plugin); - const Index = this.plugin.settings.currentWallpaper.Index; - const pluginId = this.plugin.manifest.id; - const baseDir = `${this.app.vault.configDir}/plugins/${pluginId}/wallpapers`; - const sourceFullPath = `${this.app.vault.configDir}/${entry.path}`; - const ActiveSubFolder = WallpaperConfigUtils.computeActiveSubfolder(Index); - const targetFullPath = `${baseDir}/active/${ActiveSubFolder}/${entry.fileName}`; - const exists = await this.app.vault.adapter.exists(targetFullPath); - if (!exists) { - try { - await this.app.vault.adapter.copy(sourceFullPath, targetFullPath); - } catch (e) { - console.error("Failed to copy wallpaper from history to active folder", e); - return; - } - } - const relativeTargetFullPath = targetFullPath.replace(`${this.app.vault.configDir}/`, ""); - const folder = relativeTargetFullPath.substring(0, relativeTargetFullPath.lastIndexOf("/")); - if (ActiveSubFolder === "normal") { - await removeAllExcept(this.plugin, folder, relativeTargetFullPath); - } else { - const Path = `.obsidian/plugins/${pluginId}/wallpapers/active/${ActiveSubFolder}`; - await removeUnusedFilesInFolder(this.plugin, Path, this.plugin.settings.currentWallpaper.Index, this.plugin.settings.currentWallpaper.path); - } - this.plugin.settings.currentWallpaper.path = `plugins/${pluginId}/wallpapers/active/${ActiveSubFolder}/${entry.fileName}`; - this.plugin.settings.currentWallpaper.type = entry.type; - this.plugin.settings.WallpaperConfigs[Index].path = `plugins/${pluginId}/wallpapers/active/${ActiveSubFolder}/${entry.fileName}`; - this.plugin.settings.WallpaperConfigs[Index].type = entry.type; - await Promise.all( - Array.from(this.plugin.windows).map(async (win) => { - await toggleModalStyles(win.document, this.plugin); - await WallpaperApplier.applyWallpaper(this.plugin, true, win.document); - }) - ); - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - await this.plugin.saveSettings(); - this.display(); - }); - }); - }); - }) - ); - setting.addButton((btn) => { - btn.setButtonText("Check wallpaper").setIcon("image-file").onClick(async () => { - const path = await SettingsUtils.getWallpaperPath(this.plugin, this.plugin.settings.currentWallpaper.Index); - if (!path) { - new import_obsidian4.Notice("No wallpaper path set."); - return; - } - if (await SettingsUtils.wallpaperExists(this.plugin, path)) { - new import_obsidian4.Notice("Wallpaper loaded successfully."); - } else { - new import_obsidian4.Notice("Wallpaper file not found. Resetting path."); - this.plugin.settings.currentWallpaper.path = ""; - await this.plugin.saveSettings(); - } - }); - }); - setting.addButton( - (btn) => btn.setButtonText("Browse").setIcon("folder-open").setClass("mod-cta").onClick(async (evt) => { - const doc = evt.currentTarget.ownerDocument; - await openFilePicker(this.plugin, this.plugin.settings.currentWallpaper.Index, false, doc); - for (const win of this.plugin.windows) { - await toggleModalStyles(win.document, this.plugin); - } - }) - ); - new import_obsidian4.Setting(containerEl).setName("Use global configuration").setDesc("When enabled, all wallpapers will use the global settings instead of individual configurations.").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.globalConfig.enabled).onChange(async (value) => { - this.plugin.settings.globalConfig.enabled = value; - this.plugin.settings.Preview = false; - this.plugin.settings.currentWallpaper = await WallpaperConfigUtils.GetCurrentConfig(this.plugin); - await Promise.all( - Array.from(this.plugin.windows).map(async (win) => { - const media = win.document.getElementById("live-wallpaper-media"); - await toggleModalStyles(win.document, this.plugin); - applyMediaStyles(media, this.plugin.settings.currentWallpaper); - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - }) - ); - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - await this.plugin.saveSettings(); - this.display(); - }); - }); - if (!this.plugin.settings.globalConfig.enabled) { - const Preview = new import_obsidian4.Setting(containerEl).setName("Wallpaper preview").setDesc("Preview and test specific wallpaper schedules below."); - Preview.addDropdown((dropdown) => { - const MODAL_EFFECTS = { - "0": "No Schedule", - "1": "Day", - "2": "Night", - "3": "Monday", - "4": "Tuesday", - "5": "Wednesday", - "6": "Thursday", - "7": "Friday", - "8": "Saturday", - "9": "Sunday" - }; - dropdown.addOptions(MODAL_EFFECTS).setValue(this.plugin.settings.Preview ? this.plugin.settings.currentWallpaper.Index.toString() : "X").onChange(async (value) => { - const index = parseInt(value, 10); - const targetConfig = this.plugin.settings.WallpaperConfigs[index]; - if (targetConfig) { - this.plugin.settings.currentWallpaper = targetConfig; - this.plugin.settings.Preview = true; - await Promise.all( - Array.from(this.plugin.windows).map( - async (win) => { - await toggleModalStyles(win.document, this.plugin); - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - ) - ); - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - new import_obsidian4.Notice(`Previewing wallpaper for ${MODAL_EFFECTS[value]}`); - await this.plugin.saveSettings(); - this.display(); - } - }); - }); - Preview.addButton((button) => { - button.setButtonText("Turn off preview").setIcon("eye-off").onClick(async () => { - const currentConfig = await WallpaperConfigUtils.GetCurrentConfig(this.plugin); - if (currentConfig) { - this.plugin.settings.currentWallpaper = { ...currentConfig }; - this.plugin.settings.Preview = false; - await Promise.all( - Array.from(this.plugin.windows).map(async (win) => { - await toggleModalStyles(win.document, this.plugin); - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - }) - ); - new import_obsidian4.Notice("Preview turned off restored scheduled wallpaper."); - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - await this.plugin.saveSettings(); - this.display(); - } - }); - }); - } - new import_obsidian4.Setting(containerEl).setName("Use full-resolution wallpapers").setDesc("Keeps the original image size. To apply, add the wallpaper again.").addToggle((Toggle) => { - Toggle.setValue(this.plugin.settings.currentWallpaper.Quality).onChange(async (value) => { - this.plugin.settings.currentWallpaper.Quality = value; - await this.plugin.saveSettings(); - }); - }); - new import_obsidian4.Setting(containerEl).setName("Limit wallpaper size").setDesc("Enable to restrict wallpapers to a maximum size (currently 12 MB). Disable for unlimited size.").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.SizeLimited).onChange(async (value) => { - this.plugin.settings.SizeLimited = value; - await this.plugin.saveSettings(); - }); - }); - new import_obsidian4.Setting(containerEl).setName("Enable reposition").setDesc("Toggle to adjust the wallpaper's position and scale.").addToggle((Toggle) => { - Toggle.setValue(this.plugin.settings.currentWallpaper.Reposition).onChange(async (value) => { - for (const win of this.plugin.windows) { - const media = win.document.getElementById("live-wallpaper-media"); - if (value) { - SettingsUtils.enableReposition(this.plugin, win.document); - SettingsUtils.applyImagePosition(media, this.plugin.settings.currentWallpaper.positionX, this.plugin.settings.currentWallpaper.positionY, this.plugin.settings.currentWallpaper.Scale); - } else { - SettingsUtils.disableReposition(win); - applyMediaStyles(media, this.plugin.settings.currentWallpaper); - } - } - this.plugin.settings.currentWallpaper.Reposition = value; - await this.plugin.saveSettings(); - this.display(); - }); - }); - if (this.plugin.settings.currentWallpaper.Reposition) { - new import_obsidian4.Setting(containerEl).setName("Horizontal position").setDesc("Adjust the horizontal position of the wallpaper.").addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.currentWallpaper.positionX = DEFAULT_SETTINGS.currentWallpaper.positionX; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - }); - }).addSlider((slider) => { - slider.setLimits(0, 100, 1).setValue(this.plugin.settings.currentWallpaper.positionX).setDynamicTooltip().setInstant(true).onChange(async (value) => { - this.plugin.settings.currentWallpaper.positionX = value; - this.plugin.debouncedSave(); - await Promise.all( - Array.from(this.plugin.windows).map(async (win) => { - const media = win.document.getElementById("live-wallpaper-media"); - if (media) { - SettingsUtils.applyImagePosition(media, this.plugin.settings.currentWallpaper.positionX, this.plugin.settings.currentWallpaper.positionY, this.plugin.settings.currentWallpaper.Scale); - } - }) - ); - }); - }); - new import_obsidian4.Setting(containerEl).setName("Vertical position").setDesc("Adjust the vertical position of the wallpaper.").addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.currentWallpaper.positionY = DEFAULT_SETTINGS.currentWallpaper.positionY; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - }); - }).addSlider((slider) => { - slider.setLimits(0, 100, 1).setValue(this.plugin.settings.currentWallpaper.positionY).setDynamicTooltip().setInstant(true).onChange(async (value) => { - this.plugin.settings.currentWallpaper.positionY = value; - this.plugin.debouncedSave(); - await Promise.all( - Array.from(this.plugin.windows).map(async (win) => { - const media = win.document.getElementById("live-wallpaper-media"); - if (media) { - SettingsUtils.applyImagePosition(media, this.plugin.settings.currentWallpaper.positionX, this.plugin.settings.currentWallpaper.positionY, this.plugin.settings.currentWallpaper.Scale); - } - }) - ); - }); - }); - new import_obsidian4.Setting(containerEl).setName("Image scale").setDesc("Adjust the size of the wallpaper.").addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.currentWallpaper.Scale = DEFAULT_SETTINGS.currentWallpaper.Scale; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - }); - }).addSlider((slider) => { - slider.setLimits(0.5, 2, 0.1).setValue(this.plugin.settings.currentWallpaper.Scale ?? 1).setDynamicTooltip().setInstant(true).onChange(async (value) => { - this.plugin.settings.currentWallpaper.Scale = value; - this.plugin.debouncedSave(); - await Promise.all( - Array.from(this.plugin.windows).map(async (win) => { - const media = win.document.getElementById("live-wallpaper-media"); - if (media) { - await SettingsUtils.applyImagePosition( - media, - this.plugin.settings.currentWallpaper.positionX, - this.plugin.settings.currentWallpaper.positionY, - this.plugin.settings.currentWallpaper.Scale - ); - } - }) - ); - }); - }); - new import_obsidian4.Setting(containerEl).setName("Image position").setDesc("Adjust the image alignment when the main focus is off-center.").addDropdown((dropdown) => { - positions.forEach((label, key) => { - dropdown.addOption(key.toString(), label); - }); - dropdown.setValue(this.plugin.settings.currentWallpaper.position).onChange(async (value) => { - this.plugin.settings.currentWallpaper.position = value; - this.plugin.debouncedSave(); - await Promise.all( - Array.from(this.plugin.windows).map(async (win) => { - const media = win.document.getElementById("live-wallpaper-media"); - if (media) { - this.plugin.settings.currentWallpaper.positionX = Number.parseInt(value); - SettingsUtils.applyImagePosition(media, this.plugin.settings.currentWallpaper.positionX, this.plugin.settings.currentWallpaper.positionY, this.plugin.settings.currentWallpaper.Scale); - } - }) - ); - this.display(); - }); - }); - } else { - new import_obsidian4.Setting(containerEl).setName("Disable image cover").setDesc("Toggle this option to turn off object-fit: cover for the image.").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.currentWallpaper.useObjectFit).onChange(async (value) => { - for (const win of this.plugin.windows) { - const media = win.document.getElementById("live-wallpaper-media"); - if (media) { - Object.assign(media.style, { - objectFit: this.plugin.settings.currentWallpaper.useObjectFit ? "unset" : "cover" - }); - } - } - this.plugin.settings.currentWallpaper.useObjectFit = value; - this.plugin.debouncedSave(); - }); - }); - } - new import_obsidian4.Setting(containerEl).setName("Wallpaper opacity").setDesc( - "Controls the transparency level of the wallpaper (0% = fully transparent, 100% = fully visible)" - ).addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - if (this.plugin.settings.AdnvOpend) return; - this.plugin.settings.currentWallpaper.opacity = DEFAULT_SETTINGS.currentWallpaper.opacity; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - }); - }).addSlider((slider) => { - const valueEl = containerEl.createEl("span", { - text: ` ${this.plugin.settings.currentWallpaper.opacity}%`, - cls: "setting-item-description" - }); - const initialValue = this.plugin.settings.AdnvOpend ? 100 : this.plugin.settings.currentWallpaper.opacity; - if (this.plugin.settings.AdnvOpend) { - slider.setDisabled(true); - valueEl.textContent = ` 100%`; - } - slider.setLimits(0, 80, 1).setValue(initialValue).setDisabled(this.plugin.settings.AdnvOpend).setDynamicTooltip().setInstant(true).onChange(async (v) => { - if (!this.plugin.settings.AdnvOpend) { - this.plugin.settings.currentWallpaper.opacity = v; - valueEl.textContent = ` ${v}%`; - this.plugin.debouncedApplyWallpaper(); - this.plugin.debouncedSave(); - } - }); - }); - new import_obsidian4.Setting(containerEl).setName("Blur radius").setDesc("Applies a blur effect to the wallpaper in pixels").addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.currentWallpaper.blurRadius = DEFAULT_SETTINGS.currentWallpaper.blurRadius; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - }); - }).addSlider((slider) => { - const valueEl = containerEl.createEl("span", { - text: ` ${this.plugin.settings.currentWallpaper.blurRadius}px`, - cls: "setting-item-description" - }); - slider.setInstant(true).setLimits(0, 20, 1).setValue(this.plugin.settings.currentWallpaper.blurRadius).onChange(async (v) => { - this.plugin.settings.currentWallpaper.blurRadius = v; - valueEl.textContent = ` ${v}px`; - this.plugin.debouncedApplyWallpaper(); - this.plugin.debouncedSave(); - }); - }); - new import_obsidian4.Setting(containerEl).setName("Brightness").setDesc("Adjusts the wallpaper brightness (100% = normal)").addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.currentWallpaper.brightness = DEFAULT_SETTINGS.currentWallpaper.brightness; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - }); - }).addSlider((slider) => { - const valueEl = containerEl.createEl("span", { - text: ` ${this.plugin.settings.currentWallpaper.brightness}%`, - cls: "setting-item-description" - }); - slider.setInstant(true).setLimits(20, 130, 1).setValue(this.plugin.settings.currentWallpaper.brightness).onChange(async (v) => { - this.plugin.settings.currentWallpaper.brightness = v; - valueEl.textContent = ` ${v}%`; - this.plugin.debouncedApplyWallpaper(); - this.plugin.debouncedSave(); - }); - }); - new import_obsidian4.Setting(containerEl).setName("Contrast").setDesc("Controls the wallpaper contrast intensity 100% represents the original image").addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.currentWallpaper.contrast = DEFAULT_SETTINGS.currentWallpaper.contrast; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - }); - }).addSlider((slider) => { - const valueEl = containerEl.createEl("span", { - text: ` ${this.plugin.settings.currentWallpaper.contrast}%`, - cls: "setting-item-description" - }); - slider.setInstant(true).setLimits(0, 200, 1).setValue(this.plugin.settings.currentWallpaper.contrast).onChange(async (v) => { - this.plugin.settings.currentWallpaper.contrast = v; - valueEl.textContent = ` ${v}%`; - this.plugin.debouncedApplyWallpaper(); - this.plugin.debouncedSave(); - }); - }); - new import_obsidian4.Setting(containerEl).setName("Layer position (z\u2011index)").setDesc( - "Determines the stacking order: higher values bring the wallpaper closer to the front" - ).addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - if (!this.plugin.settings.AdnvOpend) { - this.plugin.settings.currentWallpaper.zIndex = DEFAULT_SETTINGS.currentWallpaper.zIndex; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - } - }); - }).addSlider((slider) => { - const valueEl = containerEl.createEl("span", { - text: ` ${this.plugin.settings.currentWallpaper.zIndex}`, - cls: "setting-item-description" - }); - slider.setInstant(true).setLimits(-10, 100, 1).setValue(this.plugin.settings.currentWallpaper.zIndex).setDisabled(this.plugin.settings.AdnvOpend).onChange(async (v) => { - if (!this.plugin.settings.AdnvOpend) { - this.plugin.settings.currentWallpaper.zIndex = v; - valueEl.textContent = ` ${v}`; - this.plugin.debouncedApplyWallpaper(); - this.plugin.debouncedSave(); - } - }); - }); - new import_obsidian4.Setting(containerEl).setName("Change playback speed").setDesc( - "Adjust the playback speed for videos (0.25x \u2013 2x). This does not affect GIFs." - ).addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.currentWallpaper.playbackSpeed = DEFAULT_SETTINGS.currentWallpaper.playbackSpeed; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.display(); - }); - }).addSlider((slider) => { - const valueEl = containerEl.createSpan({ - text: `${this.plugin.settings.currentWallpaper.playbackSpeed.toFixed(2)}x`, - cls: "setting-item-description" - }); - slider.setInstant(true).setLimits(0.25, 2, 0.25).setValue(this.plugin.settings.currentWallpaper.playbackSpeed).onChange(async (val) => { - this.plugin.settings.currentWallpaper.playbackSpeed = val; - this.plugin.debouncedApplyWallpaper(); - this.plugin.debouncedSave(); - valueEl.setText(`${val.toFixed(2)}x`); - }); - }); - if (import_obsidian4.Platform.isMobileApp) { - const desc = document.createElement("div"); - desc.textContent = "On mobile devices, zooming can affect background size. You can manually set the height and width to maintain consistency."; - containerEl.appendChild(desc); - new import_obsidian4.Setting(containerEl).setName("Background width").setDesc( - "Set a custom width for the background on mobile (e.g., 100vw or 500px)." - ).addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.mobileBackgroundWidth = DEFAULT_SETTINGS.mobileBackgroundWidth; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - ChangeWallpaperContainer(win.document, { width: this.plugin.settings.mobileBackgroundWidth, height: this.plugin.settings.mobileBackgroundHeight }); - } - this.display(); - }); - }).addText( - (text) => text.setPlaceholder("e.g., 100vw").setValue(this.plugin.settings.mobileBackgroundWidth || "").onChange(async (value) => { - this.plugin.settings.mobileBackgroundWidth = value; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - ChangeWallpaperContainer(win.document, { width: this.plugin.settings.mobileBackgroundWidth, height: this.plugin.settings.mobileBackgroundHeight }); - } - }) - ); - new import_obsidian4.Setting(containerEl).setName("Background height").setDesc( - "Set a custom height for the background on mobile (e.g., 100vh or 800px)." - ).addExtraButton((button) => { - button.setIcon("rotate-ccw").setTooltip("Reset").onClick(async () => { - this.plugin.settings.mobileBackgroundHeight = DEFAULT_SETTINGS.mobileBackgroundHeight; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - ChangeWallpaperContainer(win.document, { width: this.plugin.settings.mobileBackgroundWidth, height: this.plugin.settings.mobileBackgroundHeight }); - } - this.display(); - }); - }).addText( - (text) => text.setPlaceholder("e.g., 100vh").setValue(this.plugin.settings.mobileBackgroundHeight || "").onChange(async (value) => { - this.plugin.settings.mobileBackgroundHeight = value; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - ChangeWallpaperContainer(win.document, { width: this.plugin.settings.mobileBackgroundWidth, height: this.plugin.settings.mobileBackgroundHeight }); - } - }) - ); - new import_obsidian4.Setting(containerEl).setName("Match screen size").setDesc( - "Automatically set the background size to match your device's screen dimensions." - ).addButton( - (button) => button.setButtonText("Resize to screen").onClick(async () => { - for (const win of this.plugin.windows) { - this.plugin.settings.mobileBackgroundHeight = win.innerHeight.toString() + "px"; - this.plugin.settings.mobileBackgroundWidth = win.innerWidth.toString() + "px"; - ChangeWallpaperContainer(win.document, { width: this.plugin.settings.mobileBackgroundWidth, height: this.plugin.settings.mobileBackgroundHeight }); - } - await this.plugin.saveSettings(); - this.display(); - }) - ); - } - } -}; - -// src/Settings/ScheduledWallpaperSettings.ts -var import_obsidian5 = require("obsidian"); -var ScheduledApp = class extends import_obsidian5.PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - display() { - const { containerEl } = this; - containerEl.empty(); - new import_obsidian5.Setting(containerEl).setName("Day and night mode").setDesc("Enable different wallpapers for day and night").addToggle( - (toggle) => toggle.setValue( - this.plugin.settings.ScheduledOptions.dayNightMode - ).onChange(async (value) => { - const otherEnabled = Scheduler.Check( - this.plugin.settings.ScheduledOptions, - "dayNightMode" - ); - if (value && otherEnabled) { - new import_obsidian5.Notice("Only one mode can be enabled at a time."); - toggle.setValue(false); - return; - } - this.plugin.settings.ScheduledOptions.dayNightMode = value; - await Promise.all( - Array.from(this.plugin.windows).map( - async (win) => { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - ) - ); - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - await this.plugin.saveSettings(); - this.display(); - }) - ); - if (this.plugin.settings.ScheduledOptions.dayNightMode) { - const paths = WallpaperConfigUtils.getPaths(this.plugin.settings.currentWallpaper.Index, this.plugin.settings.WallpaperConfigs); - if (!paths[0]) paths[0] = ""; - if (!paths[1]) paths[1] = ""; - new import_obsidian5.Setting(containerEl).setName("Day Wallpaper").setDesc("Wallpaper to use during the day").addButton((btn) => btn.setIcon("folder-open").setTooltip("Browse for file").onClick((evt) => { - const doc = evt.currentTarget.ownerDocument; - openFilePicker(this.plugin, 1, true, doc); - })); - new import_obsidian5.Setting(containerEl).setName("Night Wallpaper").setDesc("Wallpaper to use at night").addButton((btn) => btn.setIcon("folder-open").setTooltip("Browse for file").onClick((evt) => { - const doc = evt.currentTarget.ownerDocument; - openFilePicker(this.plugin, 2, true, doc); - })); - let dayTimeValue = this.plugin.settings.ScheduledOptions.dayStartTime; - let nightTimeValue = this.plugin.settings.ScheduledOptions.nightStartTime; - const Time = new import_obsidian5.Setting(containerEl).setName("Time").setDesc("Enter time in HH:MM format (e.g., 23:54)"); - Time.addText((area) => { - area.setPlaceholder("HH:MM").setValue(this.plugin.settings.ScheduledOptions.dayStartTime ?? "").onChange((value) => { - dayTimeValue = value; - }); - }); - Time.addText((area) => { - area.setPlaceholder("HH:MM").setValue(this.plugin.settings.ScheduledOptions.nightStartTime ?? "").onChange((value) => { - nightTimeValue = value; - }); - }); - new import_obsidian5.Setting(containerEl).addButton( - (btn) => btn.setButtonText("Apply now").setCta().onClick(async () => { - if (Scheduler.ValidateText(dayTimeValue) && Scheduler.ValidateText(nightTimeValue)) { - this.plugin.settings.ScheduledOptions.dayStartTime = dayTimeValue; - this.plugin.settings.ScheduledOptions.nightStartTime = nightTimeValue; - await this.plugin.saveSettings(); - new import_obsidian5.Notice("Wallpaper schedule has been set."); - for (const win of this.plugin.windows) { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - } else { - new import_obsidian5.Notice( - "One or both time values are invalid. Use HH:MM format." - ); - } - }) - ); - new import_obsidian5.Setting(containerEl).setName("Check day and night wallpapers").setDesc( - "Check whether the paths to the day and night wallpapers are set and whether the files exist." - ).addButton(async (btn) => { - btn.setButtonText("Check").onClick(async () => { - const dayPath = this.plugin.settings.WallpaperConfigs[1].path; - const nightPath = this.plugin.settings.WallpaperConfigs[2].path; - const dayExists = dayPath ? await SettingsUtils.getPathExists(this.plugin, dayPath) : false; - const nightExists = nightPath ? await SettingsUtils.getPathExists(this.plugin, nightPath) : false; - if (dayExists && nightExists) { - new import_obsidian5.Notice("Both wallpapers (day and night) are set and exist."); - } else if (!dayExists && !nightExists) { - new import_obsidian5.Notice("Both wallpapers are missing: day and night."); - } else if (!dayExists) { - new import_obsidian5.Notice("The day wallpaper is not set or does not exist."); - } else { - new import_obsidian5.Notice("The night wallpaper is not set or does not exist."); - } - }); - }); - } - new import_obsidian5.Setting(containerEl).setName("Weekly mode").setDesc("Enable different wallpapers for any day").addToggle( - (toggle) => toggle.setValue(this.plugin.settings.ScheduledOptions.weekly).onChange(async (value) => { - const otherEnabled = Scheduler.Check( - this.plugin.settings.ScheduledOptions, - "weekly" - ); - if (value && otherEnabled) { - new import_obsidian5.Notice("Only one mode can be enabled at a time."); - toggle.setValue(false); - return; - } - this.plugin.settings.ScheduledOptions.weekly = value; - await Promise.all( - Array.from(this.plugin.windows).map( - async (win) => { - await WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - ) - ); - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - await this.plugin.saveSettings(); - this.display(); - }) - ); - if (this.plugin.settings.ScheduledOptions.weekly) { - const paths = WallpaperConfigUtils.getPaths(this.plugin.settings.currentWallpaper.Index, this.plugin.settings.WallpaperConfigs); - let selectedDay = "Monday"; - const daysOfWeek = [ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday" - ]; - daysOfWeek.forEach((_, index) => { - if (!paths[index]) { - paths[index] = ""; - } - }); - new import_obsidian5.Setting(containerEl).setName("Day Wallpaper").setDesc("Wallpaper to use during the day").addButton( - (btn) => btn.setIcon("folder-open").setTooltip("Browse for file").onClick((evt) => { - const doc = evt.currentTarget.ownerDocument; - const index = daysOfWeek.indexOf(selectedDay); - if (index !== -1) { - openFilePicker(this.plugin, index + 3, true, doc); - } else { - console.warn("Invalid day selected"); - } - }) - ).addDropdown((dropdown) => { - daysOfWeek.forEach((day) => { - dropdown.addOption(day, day); - }); - dropdown.setValue(selectedDay); - dropdown.onChange((value) => { - selectedDay = value; - }); - }); - new import_obsidian5.Setting(containerEl).setName("Check weekly wallpapers").setDesc( - "Check if the paths for the weekly wallpapers are set and if the files exist." - ).addButton((btn) => { - btn.setButtonText("Check").onClick(async () => { - const missingDays = []; - for (let i = 0; i < paths.length; i++) { - const pathExists = await SettingsUtils.getPathExists(this.plugin, paths[i]); - if (!pathExists) { - missingDays.push(daysOfWeek[i]); - } - } - if (missingDays.length > 0) { - new import_obsidian5.Notice(`Missing wallpapers for: ${missingDays.join(", ")}`); - } else { - new import_obsidian5.Notice("All weekly wallpapers are loaded."); - } - }); - }); - } - new import_obsidian5.Setting(containerEl).setName("Auto-rotate wallpapers").setDesc("Change wallpapers automatically over time").addToggle( - (toggle) => toggle.setValue(this.plugin.settings.ScheduledOptions.autoSwitch).onChange(async (value) => { - const otherEnabled = Scheduler.Check( - this.plugin.settings.ScheduledOptions, - "autoSwitch" - ); - if (value && otherEnabled) { - new import_obsidian5.Notice("Only one mode can be enabled at a time."); - toggle.setValue(false); - return; - } - this.plugin.settings.ScheduledOptions.autoSwitch = value; - const results = await Promise.all( - Array.from(this.plugin.windows).map( - (win) => WallpaperApplier.applyWallpaper(this.plugin, false, win.document) - ) - ); - if (results.some((r) => !r)) { - await this.plugin.saveSettings(); - this.display(); - return; - } - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - await this.plugin.saveSettings(); - this.display(); - }) - ); - if (this.plugin.settings.ScheduledOptions.autoSwitch) { - this.plugin.settings.WallpaperConfigs.slice(10, this.plugin.settings.WallpaperConfigs.length).forEach((Config) => { - new import_obsidian5.Setting(containerEl).setName(`Wallpaper ${GetFileName(Config.path)}`).setDesc("Order in the automatic rotation").addButton( - (btn) => btn.setIcon("folder-open").setTooltip("Browse for file").onClick(async (evt) => { - const doc = evt.currentTarget.ownerDocument; - await openFilePicker(this.plugin, Config.Index, true, doc); - await Promise.all( - Array.from(this.plugin.windows).map( - async (win) => { - await toggleModalStyles(win.document, this.plugin); - } - ) - ); - this.display(); - }) - ).addExtraButton( - (btn) => btn.setIcon("x").setTooltip("Remove").onClick(async () => { - await removeFileIfUnused(this.plugin, Config.Index, this.plugin.settings.WallpaperConfigs[Config.Index].path); - this.plugin.settings.WallpaperConfigs = WallpaperConfigUtils.RemoveConfig(this.plugin.settings.WallpaperConfigs, Config); - await this.plugin.saveSettings(); - this.display(); - }) - ); - }); - new import_obsidian5.Setting(containerEl).addButton( - (btn) => btn.setButtonText("Add new element").setClass("text-arena-center-button").setTooltip("Add a new row to the table").onClick(async (evt) => { - const doc = evt.currentTarget.ownerDocument; - this.plugin.settings.WallpaperConfigs = WallpaperConfigUtils.NewConfig(this.plugin.settings.WallpaperConfigs); - await openFilePicker(this.plugin, this.plugin.settings.WallpaperConfigs.length - 1, true, doc); - await Promise.all( - Array.from(this.plugin.windows).map( - async (win) => { - await toggleModalStyles(win.document, this.plugin); - } - ) - ); - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - await this.plugin.saveSettings(); - this.display(); - }) - ); - new import_obsidian5.Setting(containerEl).setName("Wallpaper folder").setDesc("Select a folder and load all wallpapers").addButton( - (btn) => btn.setIcon("folder").setButtonText("Select folder").onClick(async (evt) => { - const doc = evt.currentTarget.ownerDocument; - await this.plugin.openFolderPicker(doc); - await Promise.all( - Array.from(this.plugin.windows).map( - async (win) => { - await toggleModalStyles(win.document, this.plugin); - } - ) - ); - UpdatePaths(this.plugin, { path: this.plugin.settings.currentWallpaper.path, type: this.plugin.settings.currentWallpaper.type }); - this.display(); - }) - ); - } - const WALLPAPER_INTERVALS = { - "00:01": "Every 1 minute", - "00:05": "Every 5 minutes", - "00:10": "Every 10 minutes", - "00:30": "Every 30 minutes", - "01:00": "Every 1 hour", - "custom": "Custom interval" - }; - const currentInterval = this.plugin.settings.ScheduledOptions.intervalCheckTime ?? "00:10"; - new import_obsidian5.Setting(containerEl).setName("Wallpaper change interval").setDesc("How often the wallpaper should be checked and changed").addDropdown((dropdown) => { - dropdown.addOptions(WALLPAPER_INTERVALS); - dropdown.setValue(this.plugin.settings.ScheduledOptions.isCustomInterval ? "custom" : currentInterval); - dropdown.onChange(async (value) => { - if (value !== "custom") { - this.plugin.settings.ScheduledOptions.intervalCheckTime = value; - this.plugin.settings.ScheduledOptions.isCustomInterval = false; - await this.plugin.saveSettings(); - this.plugin.startDayNightWatcher(); - this.display(); - } else { - this.plugin.settings.ScheduledOptions.intervalCheckTime = "00:42"; - this.plugin.settings.ScheduledOptions.isCustomInterval = true; - await this.plugin.saveSettings(); - this.display(); - } - }); - }); - if (this.plugin.settings.ScheduledOptions.isCustomInterval) { - let customValue = currentInterval; - new import_obsidian5.Setting(containerEl).setName("Custom interval").setDesc("Enter time in HH:MM format (e.g., 00:42)").addText((text) => { - text.setPlaceholder("HH:MM").setValue(currentInterval).onChange((value) => { - customValue = value; - }); - }); - new import_obsidian5.Setting(containerEl).addButton( - (btn) => btn.setButtonText("Apply custom interval").setCta().onClick(async () => { - if (!Scheduler.ValidateText(customValue)) { - new import_obsidian5.Notice("Invalid format. Use HH:MM."); - return; - } - this.plugin.settings.ScheduledOptions.intervalCheckTime = customValue; - await this.plugin.saveSettings(); - this.plugin.startDayNightWatcher(); - new import_obsidian5.Notice("Custom interval applied."); - }) - ); - } - } -}; - -// src/Settings/TransparencySettings.ts -var import_obsidian6 = require("obsidian"); -var TransparencySettingsTab = class extends import_obsidian6.PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - display() { - const { containerEl } = this; - containerEl.empty(); - const transparencySection = containerEl.createDiv(); - new import_obsidian6.Setting(transparencySection).setName("Transparency options").setHeading(); - new import_obsidian6.Setting(transparencySection).setName( - "Fine-tune transparency and visual effects to seamlessly integrate your wallpaper. These features allow for deeper customization but may require CSS knowledge." - ); - const toggleTransparencyButton = transparencySection.createEl("button", { - text: this.plugin.settings.AdnvOpend ? "Disable transparency settings" : "Enable transparency settings" - }); - const transparencyOptionsContainer = transparencySection.createDiv(); - transparencyOptionsContainer.style.display = this.plugin.settings.AdnvOpend ? "block" : "none"; - toggleTransparencyButton.onclick = async () => { - this.plugin.settings.AdnvOpend = !this.plugin.settings.AdnvOpend; - transparencyOptionsContainer.style.display = this.plugin.settings.AdnvOpend ? "block" : "none"; - toggleTransparencyButton.setText(this.plugin.settings.AdnvOpend ? "Hide transparency options" : "Show transparency options"); - for (const win of this.plugin.windows) { - await toggleModalStyles(win.document, this.plugin); - WallpaperApplier.applyWallpaper(this.plugin, false, win.document); - } - this.plugin.saveSettings(); - this.display(); - }; - const tableDescription = transparencyOptionsContainer.createEl("p", { - cls: "transparency-options-description" - }); - tableDescription.innerHTML = "Define UI elements and CSS attributes that should be made transparent. This allows the wallpaper to appear behind the interface, improving readability and aesthetics. Example attributes you can modify:
\u2022 attribute: --background-primary
\u2022 attribute: --background-secondary
\u2022 attribute: --background-secondary-alt
\u2022 attribute: --col-pr-background
\u2022 attribute: --col-bckg-mainpanels
\u2022 attribute: --col-txt-titlebars

You can inspect elements and variables using browser dev tools (CTRL + SHIFT + I) to discover more attributes to adjust."; - const tableContainer = transparencyOptionsContainer.createEl("div", { - cls: "text-arena-table-container" - }); - const table = tableContainer.createEl("table", { cls: "text-arena-table" }); - const thead = table.createEl("thead"); - const headerRow = thead.createEl("tr"); - headerRow.createEl("th", { text: "Attribute to modify" }); - this.plugin.settings.TextArenas.forEach((entry, index) => { - const row = table.createEl("tr"); - new import_obsidian6.Setting(row).addText((text) => { - text.setValue(entry.attribute).onChange(async (value) => { - if (!SettingsUtils.AttributeValid(value)) { - return; - } - for (const win of this.plugin.windows) { - RemoveChanges(win.document, this.plugin.settings.TextArenas, index); - } - this.plugin.settings.TextArenas[index].attribute = value; - for (const win of this.plugin.windows) { - await LoadOrUnloadChanges(win.document, this.plugin.settings.TextArenas, true); - ApplyChanges(win.document, this.plugin.settings.TextArenas, index); - } - await this.plugin.saveSettings(); - }); - }); - const actionCell = row.createEl("td"); - new import_obsidian6.Setting(actionCell).addExtraButton((btn) => { - btn.setIcon("cross").setTooltip("Remove this entry").onClick(() => { - for (const win of this.plugin.windows) { - RemoveChanges(win.document, this.plugin.settings.TextArenas, index); - } - this.plugin.settings.TextArenas.splice(index, 1); - for (const win of this.plugin.windows) { - LoadOrUnloadChanges(win.document, this.plugin.settings.TextArenas, true); - } - this.plugin.saveSettings(); - this.display(); - }); - }); - }); - new import_obsidian6.Setting(transparencyOptionsContainer).addButton( - (btn) => btn.setButtonText("Add new element").setClass("text-arena-center-button").setTooltip("Add a new row to the table").onClick(() => { - this.plugin.settings.TextArenas.push({ attribute: "" }); - this.display(); - }) - ); - let colorPickerRef = null; - new import_obsidian6.Setting(transparencyOptionsContainer).setName("Custom background color").setDesc("Set a custom color for the plugin's styling logic").addColorPicker((picker) => { - colorPickerRef = picker; - picker.setValue(this.plugin.settings.Color || "#000000").onChange(async (value) => { - this.plugin.settings.Color = value; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - applyBackgroundColor(win.document, this.plugin.settings.AdnvOpend, this.plugin.settings.Color); - } - }); - }).addExtraButton( - (btn) => btn.setIcon("reset").setTooltip("Reset to default").onClick(async () => { - this.plugin.settings.Color = ""; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - applyBackgroundColor(win.document, this.plugin.settings.AdnvOpend, this.plugin.settings.Color); - } - if (colorPickerRef) { - colorPickerRef.setValue("#000000"); - } - }) - ); - if (import_obsidian6.Platform.isDesktop) { - new import_obsidian6.Setting(transparencyOptionsContainer).setName("Modal background effect").setDesc("Choose how the modal background is styled when transparency options are enabled").addDropdown((dropdown) => { - const MODAL_EFFECTS = { - none: "No effect", - blur: "Apply blur effect", - dim: "Dim the background", - "blur+dim": "Apply both blur and dim effects" - }; - dropdown.addOptions(MODAL_EFFECTS).setValue(this.plugin.settings.modalStyle.effect).onChange(async (value) => { - this.plugin.settings.modalStyle.effect = value; - await this.plugin.saveSettings(); - for (const win of this.plugin.windows) { - await toggleModalStyles(win.document, this.plugin); - } - }); - }); - new import_obsidian6.Setting(transparencyOptionsContainer).setName("Modal blur radius").setDesc("Adjust the blur intensity applied to the modal background").addSlider((slider) => { - slider.setValue(this.plugin.settings.modalStyle.blurRadius).setLimits(0, 30, 1).setInstant(true).setDynamicTooltip().onChange(async (value) => { - this.plugin.settings.modalStyle.blurRadius = value; - for (const win of this.plugin.windows) { - await toggleModalStyles(win.document, this.plugin); - } - this.plugin.debouncedSave(); - }); - }); - new import_obsidian6.Setting(transparencyOptionsContainer).setName("Modal dim opacity").setDesc("Adjust the darkness level applied to the modal background").addSlider((slider) => { - slider.setValue(this.plugin.settings.modalStyle.dimOpacity * 100).setLimits(0, 100, 5).setInstant(true).setDynamicTooltip().onChange(async (value) => { - this.plugin.settings.modalStyle.dimOpacity = value / 100; - for (const win of this.plugin.windows) { - await toggleModalStyles(win.document, this.plugin); - } - this.plugin.debouncedSave(); - }); - }); - new import_obsidian6.Setting(transparencyOptionsContainer).setName("Modal dim color").setDesc("Choose whether the modal background dim is black or white").addDropdown((dropdown) => { - dropdown.addOption("black", "Black").addOption("white", "White").setValue(this.plugin.settings.modalStyle.dimColor).onChange(async (value) => { - this.plugin.settings.modalStyle.dimColor = value; - for (const win of this.plugin.windows) { - await toggleModalStyles(win.document, this.plugin); - } - this.plugin.debouncedSave(); - }); - }); - new import_obsidian6.Setting(transparencyOptionsContainer).setName("Disable modal background").setDesc("Turns off the default modal background dim").addToggle((toggle) => { - toggle.setValue(this.plugin.settings.modalStyle.disableModalBg).onChange(async (value) => { - this.plugin.settings.modalStyle.disableModalBg = value; - for (const win of this.plugin.windows) { - await toggleModalStyles(win.document, this.plugin); - } - this.plugin.debouncedSave(); - }); - }); - new import_obsidian6.Setting(transparencyOptionsContainer).setName("Reset modal settings").setDesc("Restore default blur and dim opacity for the modal background").addButton( - (btn) => btn.setIcon("reset").setTooltip("Reset modal styles to default").onClick(async () => { - const defaults = DEFAULT_SETTINGS; - this.plugin.settings.modalStyle = { ...defaults.modalStyle }; - for (const win of this.plugin.windows) { - await toggleModalStyles(win.document, this.plugin); - } - this.plugin.debouncedSave(); - this.display(); - }) - ); - } - } -}; - -// src/Settings/SettingsManager.ts -var LiveWallpaperSettingManager = class extends import_obsidian7.PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - this.regularTab = new SettingsApp(app, plugin); - this.scheduledTab = new ScheduledApp(app, plugin); - this.transparencyTab = new TransparencySettingsTab(app, plugin); - this.activeTab = "regular"; - } - display() { - const { containerEl } = this; - containerEl.empty(); - const navContainer = containerEl.createDiv({ - cls: "live-wallpaper-settings-nav" - }); - new import_obsidian7.Setting(navContainer).addButton((button) => { - button.setButtonText("General settings").setClass(this.activeTab === "regular" ? "mod-cta" : "mod-off").onClick(() => { - this.activeTab = "regular"; - this.display(); - }); - }).addButton((button) => { - button.setButtonText("Scheduled themes").setClass(this.activeTab === "dynamic" ? "mod-cta" : "mod-off").onClick(() => { - this.activeTab = "dynamic"; - this.display(); - }); - }).addButton((button) => { - button.setButtonText("Transparency settings").setClass(this.activeTab === "transparency" ? "mod-cta" : "mod-off").onClick(() => { - this.activeTab = "transparency"; - this.display(); - }); - }); - const contentContainer = containerEl.createDiv({ - cls: "live-wallpaper-settings-content" - }); - if (this.activeTab === "regular") { - this.regularTab.containerEl = contentContainer; - this.regularTab.display(); - } else if (this.activeTab === "transparency") { - this.transparencyTab.containerEl = contentContainer; - this.transparencyTab.display(); - } else { - this.scheduledTab.containerEl = contentContainer; - this.scheduledTab.display(); - } - } -}; - -// src/MigrationManager.ts -var Migrate = class { - static async migrateOldSettings(Plugin2) { - const settings = Plugin2.settings; - const scheduled = Plugin2.settings.scheduledWallpapers; - if (!scheduled || typeof scheduled !== "object") { - return; - } - if (Array.isArray(scheduled.wallpaperPaths)) { - const oldPaths = scheduled.wallpaperPaths; - scheduled.wallpaperDayPaths = [oldPaths[0] ?? "", oldPaths[1] ?? ""]; - scheduled.wallpaperWeekPaths = oldPaths.slice(2, 9); - delete scheduled.wallpaperPaths; - } - if (Array.isArray(scheduled.wallpaperTypes)) { - const oldTypes = scheduled.wallpaperTypes; - scheduled.wallpaperDayTypes = [ - this.isValidWallpaperType(oldTypes[0]) ? oldTypes[0] : "image", - this.isValidWallpaperType(oldTypes[1]) ? oldTypes[1] : "image" - ]; - scheduled.wallpaperWeekTypes = oldTypes.slice(2, 9).map((t) => t ?? "image"); - delete scheduled.wallpaperTypes; - } - const pluginDir = `${Plugin2.app.vault.configDir}/plugins/${Plugin2.manifest.id}`; - const oldDir = `${pluginDir}/wallpaper`; - const exists = await Plugin2.app.vault.adapter.exists(oldDir); - if (exists) { - const oldFolder = `${Plugin2.app.vault.configDir}/plugins/${Plugin2.manifest.id}/wallpaper`; - try { - await Plugin2.app.vault.adapter.rmdir(oldFolder, true); - } catch (err) { - console.error("Could not remove old wallpaper folder:", err); - } - } - if (!Array.isArray(settings.WallpaperConfigs)) { - settings.WallpaperConfigs = Array.from({ length: 10 }, (_, i) => ({ - ...defaultWallpaper, - Index: i - })); - } else if (settings.WallpaperConfigs.length < 10) { - for (let i = settings.WallpaperConfigs.length; i < 10; i++) { - settings.WallpaperConfigs.push({ ...defaultWallpaper, Index: i }); - } - } - if (settings.wallpaperPath) { - const path = settings.wallpaperPath; - const oldConfig = Plugin2.settings.WallpaperConfigs[0] ?? {}; - settings.WallpaperConfigs[0] = { - ...defaultWallpaper, - ...oldConfig, - path, - type: settings.wallpaperType ?? "image", - zIndex: settings.zIndex ?? defaultWallpaper.zIndex, - opacity: settings.opacity ?? defaultWallpaper.opacity, - playbackSpeed: settings.playbackSpeed ?? defaultWallpaper.playbackSpeed, - Quality: settings.Quality ?? defaultWallpaper.Quality, - blurRadius: settings.blurRadius ?? defaultWallpaper.blurRadius, - Reposition: settings.Reposition ?? defaultWallpaper.Reposition, - positionX: settings.PositionX ?? defaultWallpaper.positionX, - positionY: settings.PositionY ?? defaultWallpaper.positionY, - position: settings.Position ?? defaultWallpaper.position, - Scale: settings.Scale ?? defaultWallpaper.Scale, - useObjectFit: settings.useObjectFit ?? defaultWallpaper.useObjectFit, - Index: 0 - }; - Plugin2.settings.globalConfig.config = { - ...defaultWallpaper, - ...oldConfig, - path, - type: settings.wallpaperType ?? "image", - zIndex: settings.zIndex ?? defaultWallpaper.zIndex, - opacity: settings.opacity ?? defaultWallpaper.opacity, - playbackSpeed: settings.playbackSpeed ?? defaultWallpaper.playbackSpeed, - Quality: settings.Quality ?? defaultWallpaper.Quality, - blurRadius: settings.blurRadius ?? defaultWallpaper.blurRadius, - Reposition: settings.Reposition ?? defaultWallpaper.Reposition, - positionX: settings.PositionX ?? defaultWallpaper.positionX, - positionY: settings.PositionY ?? defaultWallpaper.positionY, - position: settings.Position ?? defaultWallpaper.position, - Scale: settings.Scale ?? defaultWallpaper.Scale, - useObjectFit: settings.useObjectFit ?? defaultWallpaper.useObjectFit, - Index: 0 - }; - } - if (scheduled) { - if (Array.isArray(scheduled.wallpaperDayPaths)) { - scheduled.wallpaperDayPaths.forEach((path, i) => { - const slotIndex = 1 + i; - if (slotIndex < settings.WallpaperConfigs.length) { - const oldConfig = settings.WallpaperConfigs[slotIndex] ?? {}; - settings.WallpaperConfigs[slotIndex] = { - ...defaultWallpaper, - ...oldConfig, - path, - type: scheduled.wallpaperDayTypes?.[i] ?? "image", - zIndex: settings.zIndex ?? defaultWallpaper.zIndex, - opacity: settings.opacity ?? defaultWallpaper.opacity, - playbackSpeed: settings.playbackSpeed ?? defaultWallpaper.playbackSpeed, - Quality: settings.Quality ?? defaultWallpaper.Quality, - blurRadius: settings.blurRadius ?? defaultWallpaper.blurRadius, - Reposition: settings.Reposition ?? defaultWallpaper.Reposition, - positionX: settings.PositionX ?? defaultWallpaper.positionX, - positionY: settings.PositionY ?? defaultWallpaper.positionY, - position: settings.Position ?? defaultWallpaper.position, - Scale: settings.Scale ?? defaultWallpaper.Scale, - useObjectFit: settings.useObjectFit ?? defaultWallpaper.useObjectFit, - Index: slotIndex - }; - } - }); - } - if (Array.isArray(scheduled.wallpaperWeekPaths)) { - scheduled.wallpaperWeekPaths.forEach((path, i) => { - const slotIndex = 3 + i; - if (slotIndex < settings.WallpaperConfigs.length) { - const oldConfig = settings.WallpaperConfigs[slotIndex] ?? {}; - settings.WallpaperConfigs[slotIndex] = { - ...defaultWallpaper, - ...oldConfig, - path, - type: scheduled.wallpaperWeekTypes?.[i] ?? "image", - zIndex: settings.zIndex ?? defaultWallpaper.zIndex, - opacity: settings.opacity ?? defaultWallpaper.opacity, - playbackSpeed: settings.playbackSpeed ?? defaultWallpaper.playbackSpeed, - Quality: settings.Quality ?? defaultWallpaper.Quality, - blurRadius: settings.blurRadius ?? defaultWallpaper.blurRadius, - Reposition: settings.Reposition ?? defaultWallpaper.Reposition, - positionX: settings.PositionX ?? defaultWallpaper.positionX, - positionY: settings.PositionY ?? defaultWallpaper.positionY, - position: settings.Position ?? defaultWallpaper.position, - Scale: settings.Scale ?? defaultWallpaper.Scale, - useObjectFit: settings.useObjectFit ?? defaultWallpaper.useObjectFit, - Index: slotIndex - }; - } - }); - } - } - delete settings.scheduledWallpapers; - delete settings.scheduled; - const obsoleteKeys = [ - "wallpaperPath", - "wallpaperType", - "playbackSpeed", - "Quality", - "Reposition", - "opacity", - "zIndex", - "blurRadius", - "brightness", - "PositionX", - "PositionY", - "Position", - "Scale", - "useObjectFit" - ]; - for (const key of obsoleteKeys) { - if (key in settings) { - delete settings[key]; - } - } - } - static isValidWallpaperType(t) { - return ["image", "video", "gif"].includes(t); - } -}; - -// src/main.ts -var defaultWallpaper = { - path: "", - type: "image", - zIndex: 5, - opacity: 40, - brightness: 100, - blurRadius: 8, - contrast: 100, - playbackSpeed: 1, - Reposition: false, - Quality: false, - useObjectFit: true, - position: "Center", - positionX: 50, - positionY: 50, - Scale: 1, - Index: 0 -}; -var DEFAULT_SETTINGS = { - LatestVersion: "1.6.0", - currentWallpaper: defaultWallpaper, - globalConfig: { - config: defaultWallpaper, - enabled: true - }, - Preview: false, - WallpaperConfigs: Array.from({ length: 10 }, (_, i) => ({ ...defaultWallpaper, Index: i })), - HistoryPaths: [], - mobileBackgroundWidth: "100vw", - mobileBackgroundHeight: "100vh", - AdnvOpend: false, - modalStyle: { - effect: "blur+dim", - blurRadius: 10, - dimOpacity: 0.7, - dimColor: "black", - disableModalBg: false - }, - TextArenas: [ - { attribute: "" } - ], - Color: "#000000", - INBUILD: false, - SizeLimited: true, - ScheduledOptions: { - dayNightMode: false, - weekly: false, - autoSwitch: false, - dayStartTime: "08:00", - nightStartTime: "20:00", - intervalCheckTime: "00:10", - isCustomInterval: false - }, - migrated: false -}; -var LiveWallpaperPlugin2 = class extends import_obsidian8.Plugin { - constructor() { - super(...arguments); - this.settings = DEFAULT_SETTINGS; - this.lastPath = null; - this.lastType = null; - this.windows = /* @__PURE__ */ new Set(); - this.resizeRegistered = false; - this.debouncedSave = SettingsUtils.SaveSettingsDebounced(this); - this.debouncedApplyWallpaper = SettingsUtils.ApplyWallpaperDebounced(this); - } - async onload() { - await this.loadSettings(); - await this.ensureWallpaperFolderExists(); - if (this.isVersionLess(this.settings.LatestVersion, "1.5.1")) { - await Migrate.migrateOldSettings(this); - this.settings.LatestVersion = "1.6.0"; - await this.saveSettings(); - } - const anyOptionEnabled = Scheduler.Check(this.settings.ScheduledOptions); - this.settings.currentWallpaper = await WallpaperConfigUtils.GetCurrentConfig(this); - this.addSettingTab(new LiveWallpaperSettingManager(this.app, this)); - this.windows.add(window); - this.app.workspace.iterateAllLeaves((leaf) => { - const view = leaf.view; - const container = view?.containerEl; - if (!container) return; - const doc = container.ownerDocument; - const win = doc.defaultView; - if (win) { - this.windows.add(win); - } - }); - this.registerEvent( - this.app.workspace.on("window-open", (win, winWindow) => { - this.windows.add(winWindow); - this.initWallpaperForWindow(winWindow.document); - }) - ); - this.registerEvent( - this.app.workspace.on("window-close", (win, winWindow) => { - this.windows.delete(winWindow); - }) - ); - this.registerEvent( - this.app.workspace.on("css-change", () => { - WallpaperApplier.applyWallpaper(this, anyOptionEnabled, window.document); - }) - ); - for (const win of this.windows) { - await this.initWallpaperForWindow(win.document); - } - const firstWin = this.windows.values().next().value; - if (Scheduler.Check(this.settings.ScheduledOptions) && firstWin === window) { - this.startDayNightWatcher(); - } - } - async initWallpaperForWindow(doc) { - if (!this.settings.currentWallpaper) { - this.settings.currentWallpaper = this.settings.WallpaperConfigs[0]; - } - ChangeWallpaperContainer(doc, { width: this.settings.mobileBackgroundWidth, height: this.settings.mobileBackgroundHeight }); - removeExistingWallpaperElements(doc); - toggleModalStyles(doc, this); - const newContainer = createWallpaperContainer(doc, this.settings.currentWallpaper, this.settings.AdnvOpend); - const appContainer = doc.querySelector(".app-container"); - if (appContainer) appContainer.insertAdjacentElement("beforebegin", newContainer); - else doc.body.appendChild(newContainer); - doc.body.classList.add("live-wallpaper-active"); - WallpaperApplier.applyWallpaper(this, false, doc); - UpdatePaths(this, { path: this.settings.currentWallpaper.path, type: this.settings.currentWallpaper.type }); - await applyBackgroundColor(doc, this.settings.AdnvOpend, this.settings.Color); - if (this.settings.currentWallpaper.Reposition) { - SettingsUtils.enableReposition(this, doc); - const media = doc.getElementById("live-wallpaper-media"); - if (media && media.parentElement) { - const reposition = () => { - SettingsUtils.applyImagePosition( - media, - this.settings.currentWallpaper.positionX, - this.settings.currentWallpaper.positionY, - this.settings.currentWallpaper.Scale - ); - }; - const imageLoadHandler = () => { - reposition(); - media.removeEventListener("load", imageLoadHandler); - }; - media.addEventListener("load", imageLoadHandler); - } - } - } - async unload() { - for (const win of this.windows) { - removeExistingWallpaperElements(win.document); - win.document.body.classList.remove("live-wallpaper-active"); - await clearBackgroundColor(win.document); - RemoveModalStyles(win.document); - LoadOrUnloadChanges(win.document, this.settings.TextArenas, false); - SettingsUtils.disableReposition(win); - } - this.windows.clear(); - this.stopDayNightWatcher(); - super.unload(); - } - async loadSettings() { - try { - const loaded = await this.loadData(); - this.settings = { ...DEFAULT_SETTINGS, ...loaded }; - } catch (e) { - console.error("Live Wallpaper Plugin \u2013 loadSettings error:", e); - this.settings = { ...DEFAULT_SETTINGS }; - } - } - async saveSettings() { - await this.saveData(this.settings); - } - isVersionLess(current, target) { - const c = current.split(".").map(Number); - const t = target.split(".").map(Number); - for (let i = 0; i < t.length; i++) { - if ((c[i] || 0) < t[i]) return true; - if ((c[i] || 0) > t[i]) return false; - } - return false; - } - async ensureWallpaperFolderExists() { - try { - const dir = this.manifest.dir; - if (!dir) throw new Error("manifest.dir is undefined"); - const wallpaperFolder = `${dir}/wallpaper`; - return await this.app.vault.adapter.exists(wallpaperFolder); - } catch (e) { - console.error("Failed to check wallpaper folder:", e); - return false; - } - } - async CreateMedia(doc) { - removeExistingWallpaperElements(doc); - const newContainer = createWallpaperContainer(doc, this.settings.currentWallpaper, this.settings.AdnvOpend); - const newMedia = await createMediaElement(doc, this); - if (newMedia) { - newMedia.id = "live-wallpaper-media"; - newContainer.appendChild(newMedia); - } - const appContainer = doc.querySelector(".app-container"); - if (appContainer) appContainer.insertAdjacentElement("beforebegin", newContainer); - else doc.body.appendChild(newContainer); - doc.body.classList.add("live-wallpaper-active"); - if (this.settings.currentWallpaper.Reposition) { - await waitForMediaDimensions(newMedia); - SettingsUtils.applyImagePosition( - newMedia, - this.settings.currentWallpaper.positionX, - this.settings.currentWallpaper.positionY, - this.settings.currentWallpaper.Scale - ); - } - } - async applyWallpaperFile(file, slotIndex, doc, isScheduledPicker = false) { - if (!validateWallpaperFile(file, this.settings.SizeLimited)) { - return; - } - const baseDir = `${this.app.vault.configDir}/plugins/${this.manifest.id}/wallpapers`; - const arrayBuffer = await getFileArrayBuffer(file, { - maxWidth: doc.win.innerWidth, - mobileBackgroundWidth: this.settings.mobileBackgroundWidth, - allowFullRes: this.settings.currentWallpaper.Quality - }); - const targetSubfolder = WallpaperConfigUtils.computeActiveSubfolder(slotIndex); - let fileName = file.name; - if (file.type.startsWith("image/") && this.settings.currentWallpaper.Quality) { - const dotIndex = fileName.lastIndexOf("."); - fileName = dotIndex !== -1 ? fileName.slice(0, dotIndex) + "_quality" + fileName.slice(dotIndex) : fileName + "_quality"; - } - const activeRelPath = await saveUnder( - this, - baseDir, - `active/${targetSubfolder}`, - fileName, - arrayBuffer - ); - const historyRelPath = await saveUnder( - this, - baseDir, - `history`, - fileName, - arrayBuffer - ); - this.settings.HistoryPaths = prependHistory(this.settings.HistoryPaths, { path: historyRelPath, type: getWallpaperType(fileName), fileName }); - await trimHistory(this, 5, `${baseDir}/history`); - if (this.settings.Preview && !isScheduledPicker) { - this.settings.currentWallpaper.path = activeRelPath; - this.settings.currentWallpaper.type = getWallpaperType(fileName); - } - if (this.settings.globalConfig.enabled) { - this.settings.globalConfig.config.path = activeRelPath; - this.settings.globalConfig.config.type = getWallpaperType(fileName); - } - this.settings.WallpaperConfigs[slotIndex].path = activeRelPath; - this.settings.WallpaperConfigs[slotIndex].type = getWallpaperType(fileName); - for (const win of this.windows) { - await WallpaperApplier.applyWallpaper(this, false, win.document); - } - if (slotIndex === 0) { - const folder = activeRelPath.substring(0, activeRelPath.lastIndexOf("/")); - await removeAllExcept(this, folder, activeRelPath); - } else { - const folder = `${baseDir}/active/${targetSubfolder}`; - await removeUnusedFilesInFolder(this, folder, slotIndex, activeRelPath); - } - UpdatePaths(this, { path: activeRelPath, type: getWallpaperType(fileName) }); - } - async openFolderPicker(doc) { - const files = await pickFolderFiles(doc); - if (files === null) return; - this.settings.WallpaperConfigs = WallpaperConfigUtils.ClearConfigsFromIndex(this.settings.WallpaperConfigs, 10); - const validFiles = files.filter( - (f) => validateWallpaperFile(f, this.settings.SizeLimited) - ); - if (validFiles.length === 0) return; - try { - const START_INDEX = 10; - for (let i = 0; i < validFiles.length; i++) { - const slotIndex = START_INDEX + i; - this.settings.WallpaperConfigs = WallpaperConfigUtils.NewConfig(this.settings.WallpaperConfigs); - await this.applyWallpaperFile(validFiles[i], slotIndex, doc, false); - } - for (const win of this.windows) { - await WallpaperApplier.applyWallpaper(this, false, win.document); - } - this.debouncedSave(); - } catch (error) { - alert("Could not import wallpaper folder."); - console.error(error); - } - } - startDayNightWatcher() { - this.stopDayNightWatcher(); - this._dayNightInterval = window.setInterval(async () => { - if (this.settings.Preview) return; - const index = WallpaperConfigUtils.getWallpaperIndex(this); - if (index !== void 0) { - if (this.settings.globalConfig.enabled) { - this.settings.currentWallpaper = WallpaperConfigUtils.applyGlobalConfig(this.settings.WallpaperConfigs[index], this.settings.globalConfig.config); - } else { - this.settings.currentWallpaper = this.settings.WallpaperConfigs[index]; - } - for (const win of this.windows) { - await WallpaperApplier.applyWallpaper(this, true, win.document); - } - UpdatePaths(this, { path: this.settings.currentWallpaper.path, type: this.settings.currentWallpaper.type }); - } - }, Scheduler.getIntervalInMs(this.settings.ScheduledOptions)); - } - stopDayNightWatcher() { - if (this._dayNightInterval) { - clearInterval(this._dayNightInterval); - this._dayNightInterval = -1; - } - } -}; - -/* nosourcemap */ \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/manifest.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/manifest.json deleted file mode 100644 index 90a1c75..0000000 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "live-wallpaper", - "name": "Live Background", - "version": "1.6.0", - "minAppVersion": "1.1.0", - "author": "Rememememe :3", - "authorUrl": "https://github.com/remememe", - "description": "Add dynamic live wallpapers (images/videos) to your workspace with customizable effects", - "isDesktopOnly": false -} \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/styles.css b/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/styles.css deleted file mode 100644 index d85779b..0000000 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/styles.css +++ /dev/null @@ -1,97 +0,0 @@ -.titlebar { - background-color: transparent !important; - z-index: 99999; -} -.windows .titlebar { - -webkit-app-region: drag; -} - -.titlebar-button-container { - z-index: 9999; - -webkit-app-region: no-drag; -} - -.titlebar-button { - color: var(--text-normal) !important; -} - -.titlebar-button:hover { - background-color: var(--background-modifier-hover) !important; -} - -.live-wallpaper-active .titlebar { - border-bottom: none !important; - -webkit-app-region: drag; -} - -#live-wallpaper-container { - transition: opacity 0.3s ease, filter 0.3s ease; - pointer-events: none; -} - -video#live-wallpaper-media { - transition: playback-rate 0.3s ease; -} -.text-arena-table { - border-collapse: collapse; - margin: 10px 0; -} - -.text-arena-table th, -.text-arena-table td { - border: 1px solid var(--background-modifier-border); - padding: 8px; - text-align: left; -} - -.text-arena-table th { - background-color: var(--background-primary-alt); - font-weight: bold; -} - -.text-arena-table tr:nth-child(even) { - background-color: var(--background-primary); -} - -.text-arena-table tr:hover { - background-color: var(--background-secondary); -} - -.text-arena-table .setting-item { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.text-arena-table .setting-item-control { - width: 100%; -} -.EditCodeBlock { - background-color: #1e1e1e; - color: #dcdcdc; - padding: 10px; - border-radius: 6px; - font-family: monospace; - overflow-x: auto; - white-space: pre; - resize: none; -} -body.is-mobile .workspace-drawer.mod-left, -body.is-mobile .workspace-drawer.mod-right, -body.is-mobile .vertical-tab-content-container, -body.is-mobile .modal-header { - backdrop-filter: blur(4px); - background: rgba(0, 0, 0, 0.7); -} -body.is-mobile .menu{ - background: rgba(0, 0, 0, 0.7); -} -.live-wallpaper-settings-nav { - display: flex; - overflow-x: auto; - white-space: nowrap; - gap: 0.5rem; - padding-bottom: 0.5rem; -} -.live-wallpaper-settings-nav .setting-button { - flex-shrink: 0; -} \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/wallpapers/active/normal/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png b/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/wallpapers/active/normal/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png deleted file mode 100644 index 8098b6c..0000000 Binary files a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/wallpapers/active/normal/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png and /dev/null differ diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/wallpapers/history/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png b/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/wallpapers/history/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png deleted file mode 100644 index 8098b6c..0000000 Binary files a/mengyanote-backend/data/mengyanote/.obsidian/plugins/live-wallpaper/wallpapers/history/wallpaper-3bf8a59e58bd7144744b0ad4e9d5bbc3.png and /dev/null differ diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/data.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/data.json deleted file mode 100644 index 741c155..0000000 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/data.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "settings": { - "migrated": 6, - "iconPacksPath": ".obsidian/icons", - "fontSize": 16, - "emojiStyle": "native", - "iconColor": null, - "recentlyUsedIcons": [ - "LiActivitySquare" - ], - "recentlyUsedIconsSize": 5, - "rules": [], - "extraMargin": { - "top": 0, - "right": 4, - "bottom": 0, - "left": 0 - }, - "iconInTabsEnabled": false, - "iconInTitleEnabled": false, - "iconInTitlePosition": "above", - "iconInFrontmatterEnabled": false, - "iconInFrontmatterFieldName": "icon", - "iconColorInFrontmatterFieldName": "iconColor", - "iconsBackgroundCheckEnabled": false, - "iconsInNotesEnabled": true, - "iconsInLinksEnabled": true, - "iconIdentifier": ":", - "lucideIconPackType": "native", - "debugMode": false, - "useInternalPlugins": false - } -} \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/main.js b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/main.js deleted file mode 100644 index 271b2ff..0000000 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/main.js +++ /dev/null @@ -1,7158 +0,0 @@ -/* -THIS IS A GENERATED/BUNDLED FILE BY ROLLUP -if you want to view the source visit the plugins github repository -*/ - -'use strict'; - -var obsidian = require('obsidian'); -var view = require('@codemirror/view'); -var state = require('@codemirror/state'); -var language = require('@codemirror/language'); - -/****************************************************************************** -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */ -/* global Reflect, Promise, SuppressedError, Symbol, Iterator */ - - -function __awaiter(thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -} - -typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { - var e = new Error(message); - return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; -}; - -const iconPacks$1 = { - faBrands: { - name: 'font-awesome-brands', - displayName: 'FontAwesome Brands', - path: 'fontawesome-free-6.5.1-web/svgs/brands/', - downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.1/fontawesome-free-6.5.1-web.zip', - }, - faRegular: { - name: 'font-awesome-regular', - displayName: 'FontAwesome Regular', - path: 'fontawesome-free-6.5.1-web/svgs/regular/', - downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.1/fontawesome-free-6.5.1-web.zip', - }, - faSolid: { - name: 'font-awesome-solid', - displayName: 'FontAwesome Solid', - path: 'fontawesome-free-6.5.1-web/svgs/solid/', - downloadLink: 'https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.1/fontawesome-free-6.5.1-web.zip', - }, - remixIcons: { - name: 'remix-icons', - displayName: 'Remix Icons', - path: '', - downloadLink: 'https://github.com/Remix-Design/RemixIcon/releases/download/v4.2.0/RemixIcon_Svg_v4.2.0.zip', - }, - iconBrew: { - name: 'icon-brew', - displayName: 'Icon Brew', - path: '', - downloadLink: 'https://github.com/FlorianWoelki/obsidian-iconize/raw/main/iconPacks/icon-brew.zip', - }, - /** @source https://simpleicons.org/ */ - simpleIcons: { - name: 'simple-icons', - displayName: 'Simple Icons', - path: 'simple-icons-11.10.0/icons/', - downloadLink: 'https://github.com/simple-icons/simple-icons/archive/refs/tags/11.10.0.zip', - }, - lucide: { - name: 'lucide-icons', - displayName: 'Lucide', - path: '', - downloadLink: 'https://github.com/lucide-icons/lucide/releases/download/0.363.0/lucide-icons-0.363.0.zip', - }, - tablerIcons: { - name: 'tabler-icons', - displayName: 'Tabler Icons', - path: 'svg', - downloadLink: 'https://github.com/tabler/tabler-icons/releases/download/v3.1.0/tabler-icons-3.1.0.zip', - }, - /** @source https://boxicons.com/ */ - boxicons: { - name: 'boxicons', - displayName: 'Boxicons', - path: 'svg', - downloadLink: 'https://github.com/FlorianWoelki/obsidian-iconize/raw/main/iconPacks/boxicons.zip', - }, - /** @source http://nagoshiashumari.github.io/Rpg-Awesome/ */ - rpgAwesome: { - name: 'rpg-awesome', - displayName: 'RPG Awesome', - path: '', - downloadLink: 'https://github.com/FlorianWoelki/obsidian-iconize/raw/main/iconPacks/rpg-awesome.zip', - }, - /** @source https://coolicons.cool/ */ - coolicons: { - name: 'coolicons', - displayName: 'Coolicons', - path: 'cooliocns SVG', - downloadLink: 'https://github.com/krystonschwarze/coolicons/releases/download/v4.1/coolicons.v4.1.zip', - }, - /** @source https://feathericons.com/ */ - feathericons: { - name: 'feather-icons', - displayName: 'Feather Icons', - path: 'feather-4.29.1/icons/', - downloadLink: 'https://github.com/feathericons/feather/archive/refs/tags/v4.29.1.zip', - }, - /** @source https://github.com/primer/octicons */ - octicons: { - name: 'octicons', - displayName: 'Octicons', - path: 'octicons-19.8.0/icons/', - downloadLink: 'https://github.com/primer/octicons/archive/refs/tags/v19.8.0.zip', - }, -}; -/** - * Returns a possible path to the icon pack. - * @param name String of the icon pack name. - * @returns String of the path to the icon pack or undefined if the icon pack does not - * exist. - */ -const getExtraPath = (iconPackName) => { - var _a; - const path = (_a = Object.values(iconPacks$1).find((iconPack) => iconPack.name === iconPackName)) === null || _a === void 0 ? void 0 : _a.path; - return (path === null || path === void 0 ? void 0 : path.length) === 0 ? undefined : path; -}; - -// This library file does not include any other dependency and is a standalone file that -// only include utility functions for manipulating or extracting svg information. -/** - * Extracts an SVG string from a given input string and returns a cleaned up and - * formatted SVG string. - * @param svgString SVG string to extract from. - * @returns Cleaned up and formatted SVG string. - */ -const extract = (svgString) => { - var _a, _b; - // Removes unnecessary spaces and newlines. - svgString = svgString.replace(/(\r\n|\n|\r)/gm, ''); - svgString = svgString.replace(/>\s+<'); - // Create a parser for better parsing of HTML. - const parser = new DOMParser(); - const svg = parser - .parseFromString(svgString, 'text/html') - .querySelector('svg'); - // Removes `width` and `height` from the `style` attribute. - if (svg.hasAttribute('style')) { - svg.style.width = ''; - svg.style.height = ''; - } - // Add `viewbox`, if it is not already a attribute. - if (svg.viewBox.baseVal.width === 0 && svg.viewBox.baseVal.height === 0) { - const width = (_a = svg.width.baseVal.value) !== null && _a !== void 0 ? _a : 16; - const height = (_b = svg.height.baseVal.value) !== null && _b !== void 0 ? _b : 16; - svg.viewBox.baseVal.width = width; - svg.viewBox.baseVal.height = height; - } - if (!svg.hasAttribute('fill')) { - svg.setAttribute('fill', 'currentColor'); - } - const possibleTitle = svg.querySelector('title'); - if (possibleTitle) { - possibleTitle.remove(); - } - svg.setAttribute('width', '16px'); - svg.setAttribute('height', '16px'); - return svg.outerHTML; -}; -/** - * Sets the font size of an SVG string by modifying its width and/or height attributes. - * The font size will be always set in pixels. - * @param svgString SVG string to modify. - * @param fontSize Font size in pixels to set. - * @returns Modified SVG string. - */ -const setFontSize = (svgString, fontSize) => { - const widthRe = new RegExp(/width="[\d.]+(px)?"/); - const heightRe = new RegExp(/height="[\d.]+(px)?"/); - if (svgString.match(widthRe)) { - svgString = svgString.replace(widthRe, `width="${fontSize}px"`); - } - if (svgString.match(heightRe)) { - svgString = svgString.replace(heightRe, `height="${fontSize}px"`); - } - return svgString; -}; -/** - * Replaces the fill or stroke color of an SVG string with a given color. - * @param svgString SVG string to modify. - * @param color Color to set. Defaults to 'currentColor'. - * @returns The modified SVG string. - */ -const colorize = (svgString, color) => { - if (!color) { - color = 'currentColor'; - } - const parser = new DOMParser(); - // Tries to parse the string into a HTML node. - const parsedNode = parser.parseFromString(svgString, 'text/html'); - const svg = parsedNode.querySelector('svg'); - if (svg) { - if (svg.hasAttribute('fill') && svg.getAttribute('fill') !== 'none') { - svg.setAttribute('fill', color); - } - else if (svg.hasAttribute('stroke') && - svg.getAttribute('stroke') !== 'none') { - svg.setAttribute('stroke', color); - } - return svg.outerHTML; - } - return svgString; -}; -var svg = { - extract, - colorize, - setFontSize, -}; - -var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; - -function commonjsRequire(path) { - throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.'); -} - -var jszip_min = {exports: {}}; - -/*! - -JSZip v3.10.1 - A JavaScript class for generating and reading zip files - - -(c) 2009-2016 Stuart Knightley -Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown. - -JSZip uses the library pako released under the MIT license : -https://github.com/nodeca/pako/blob/main/LICENSE -*/ - -var hasRequiredJszip_min; - -function requireJszip_min () { - if (hasRequiredJszip_min) return jszip_min.exports; - hasRequiredJszip_min = 1; - (function (module, exports) { - !function(e){module.exports=e();}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof commonjsRequire&&commonjsRequire;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h);}return o[r].exports}for(var l="function"==typeof commonjsRequire&&commonjsRequire,e=0;e>2,s=(3&t)<<4|r>>4,a=1>6:64,o=2>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l};},{"./support":30,"./utils":32}],2:[function(e,t,r){var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i;}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o;},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate");},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e;}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return -1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t.charCodeAt(a))];return -1^e}(0|t,e,e.length,0):0};},{"./utils":32}],5:[function(e,t,r){r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null;},{}],6:[function(e,t,r){var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n};},{lie:37}],7:[function(e,t,r){var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={};}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1);},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0);},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null;},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta});};},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})};},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){function A(e,t){var r,n="";for(r=0;r>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[];}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}));},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}});}else this.accumulate=!0;},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null;},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i;},{"../utils":32}],19:[function(e,t,r){var n=e("./Uint8ArrayReader");function i(e){n.call(this,e);}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i;},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){var n=e("./DataReader");function i(e){n.call(this,e);}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i;},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){var n=e("./ArrayReader");function i(e){n.call(this,e);}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i;},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)};},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b";},{}],24:[function(e,t,r){var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e;}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta});},t.exports=s;},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0);}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e);},t.exports=s;},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0);}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length;}i.prototype.processChunk.call(this,e);},t.exports=s;},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat();},function(e){t.error(e);});}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null;},s.prototype.resume=function(){return !!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0));},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return !1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t);}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s;},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null;}n.prototype={push:function(e){this.emit("data",e);},end:function(){if(this.isFinished)return !1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0;}catch(e){this.emit("error",e);}return !0},error:function(e){return !this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[];},emit:function(e,t){if(this._listeners[e])for(var r=0;r "+e:e}},t.exports=n;},{}],29:[function(e,t,r){var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter");}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t);}).on("error",function(e){n=[],r(e);}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t>10&1023,a[r++]=56320|1023&n);}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length);}else t=this.leftOver.concat(t);this.leftOver=null;}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta});},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null);},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta});},s.Utf8EncodeWorker=l;},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0);},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4));}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r>10&1023,o[n++]=56320|1023&i);}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t};},{"./common":41}],43:[function(e,t,r){t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:e>>>1;t[r]=e;}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return -1^e};},{}],46:[function(e,t,r){var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return (e<<1)-(4e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0));}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm);}function U(e,t){e.pending_buf[e.pending++]=t;}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t;}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f;}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else {var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1;}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73);}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91);}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103);}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0e.lookahead&&(e.match_length=e.lookahead);}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else {if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<>>=y,p-=y),p<15&&(d+=z[n++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<>>=y,p-=y,(y=s-a)>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0;}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0;}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30;}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3;}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=_,l-=_,r.lens[r.have++]=b;else {if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2;}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3;}else {for(z=_+7;l>>=_)),u>>>=7,l-=7;}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k;}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v;}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra;}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v;}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra;}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window;}else m=i,p=a-r.offset,d=r.length;for(hd?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]];}if(k>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255;}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o));}}function W(e){var t;for(t=0;t>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u>=7;n>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8);}(e);};},{"../utils/common":41}],53:[function(e,t,r){t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0;};},{}],54:[function(e,t,r){(function(e){!function(r,n){if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e);});}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1;},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*");}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data);},function(e){t.port2.postMessage(e);}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null;},s.appendChild(t);}):function(e){setTimeout(c,0,e);},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r __awaiter(void 0, void 0, void 0, function* () { - const fetched = yield obsidian.requestUrl({ url }); - const bytes = fetched.arrayBuffer; - return bytes; -}); -/** - * Transforms a JSZip file into a File object. - * @param file JSZip file to transform. - * @returns File object of the JSZip file. - */ -const getFileFromJSZipFile = (file) => __awaiter(void 0, void 0, void 0, function* () { - const fileData = yield file.async('blob'); - const filename = file.name.split('/').pop(); - return new File([fileData], filename); -}); -/** - * Read a zip file and return the files inside it. - * @param bytes ArrayBuffer of the zip file. - * @param extraPath String path to filter the files inside the zip file. This can be used - * to set an extra path (like a directory inside the zip file) to filter the files. - * @returns Array of loaded files inside the zip file. - */ -const readZipFile = (bytes_1, ...args_1) => __awaiter(void 0, [bytes_1, ...args_1], void 0, function* (bytes, extraPath = '') { - const unzippedFiles = yield jszip_minExports.loadAsync(bytes); - return Promise.resolve(unzippedFiles).then((unzipped) => { - if (!Object.keys(unzipped.files).length) { - return Promise.reject('No file was found'); - } - const files = []; - // Regex for retrieving the files inside the zip file or inside the directory of a - // zip file. - const regex = new RegExp(extraPath + '(.+)\\.svg', 'g'); - Object.entries(unzippedFiles.files).forEach(([_, v]) => { - const matched = v.name.match(regex); - if (!v.dir && matched && matched.length > 0) { - files.push(v); - } - }); - return files; - }); -}); - -const PLUGIN_NAME = 'iconize'; -const TITLE_ICON_CLASS = 'iconize-title-icon'; -const INLINE_TITLE_WRAPPER_CLASS = 'iconize-inline-title-wrapper'; -/** - * The name of the attribute that is used to store the icon name in the node. - * The value of this attributes contains the prefix and the name of the icon. - */ -const ICON_ATTRIBUTE_NAME = 'data-icon'; -var config = { - PLUGIN_NAME, - TITLE_ICON_CLASS, - INLINE_TITLE_WRAPPER_CLASS, - ICON_ATTRIBUTE_NAME, -}; - -var LoggerPrefix; -(function (LoggerPrefix) { - LoggerPrefix["Outline"] = "Outline"; -})(LoggerPrefix || (LoggerPrefix = {})); -class ConsoleLogger { - constructor(projectPrefix, enabled = false) { - this.logLevels = { - log: { label: 'LOG:' }, - info: { label: 'INFO:' }, - warn: { label: 'WARN:' }, - error: { label: 'ERROR:' }, - }; - this.projectPrefix = projectPrefix; - this.enabled = enabled; - } - formatMessage(level, message, prefix, optionalParams) { - const timestamp = new Date().toISOString(); - const { label } = this.logLevels[level]; - const prefixAsStr = !prefix ? '' : `/${prefix}`; - return [ - `${this.projectPrefix}${prefixAsStr}: [${timestamp}] ${label} ${message}`, - ...optionalParams, - ]; - } - log(message, prefix, ...optionalParams) { - if (this.enabled) { - console.log(...this.formatMessage('log', message, prefix, optionalParams)); - } - } - info(message, prefix, ...optionalParams) { - if (this.enabled) { - console.info(...this.formatMessage('info', message, prefix, optionalParams)); - } - } - warn(message, prefix, ...optionalParams) { - if (this.enabled) { - console.warn(...this.formatMessage('warn', message, prefix, optionalParams)); - } - } - error(message, prefix, ...optionalParams) { - if (this.enabled) { - console.error(...this.formatMessage('error', message, prefix, optionalParams)); - } - } - toggleLogging(enabled) { - this.enabled = enabled; - } -} -const logger = new ConsoleLogger(config.PLUGIN_NAME); - -const LUCIDE_ICON_PACK_NAME = 'lucide-icons'; -let path; -const getPath = () => { - return path; -}; -const setPath = (newPath) => { - if (newPath === 'plugins/obsidian-icon-folder/icons') { - newPath = '.obsidian/plugins/obsidian-icon-folder/icons'; - new obsidian.Notice(`[${config.PLUGIN_NAME}] Due to a change in version v1.2.2, the icon pack folder changed. Please change it in the settings to not be directly in /plugins.`, 8000); - } - path = newPath; -}; -let preloadedIcons = []; -const getPreloadedIcons = () => { - return preloadedIcons; -}; -const resetPreloadedIcons = () => { - preloadedIcons = []; -}; -let iconPacks = []; -const addLucideIconsPack = (plugin) => { - iconPacks.push({ - name: LUCIDE_ICON_PACK_NAME, - prefix: 'Li', - custom: false, - icons: plugin.doesUseNativeLucideIconPack() - ? obsidian.getIconIds() - .map((iconId) => iconId.replace(/^lucide-/, '')) - .map((iconId) => { - const iconEl = obsidian.getIcon(iconId); - iconEl.removeClass('svg-icon'); // Removes native `svg-icon` class. - return { - name: getNormalizedName(iconId), - filename: iconId, - prefix: 'Li', - svgElement: iconEl === null || iconEl === void 0 ? void 0 : iconEl.outerHTML, - svgContent: iconEl === null || iconEl === void 0 ? void 0 : iconEl.innerHTML, - svgViewbox: '', - iconPackName: LUCIDE_ICON_PACK_NAME, - }; - }) - : [], - }); -}; -const addCustomLucideIconPack = (plugin) => __awaiter(void 0, void 0, void 0, function* () { - const iconPackIndex = iconPacks.findIndex((iconPack) => iconPack.name === LUCIDE_ICON_PACK_NAME); - if (iconPackIndex > -1) { - iconPacks.splice(iconPackIndex); - } - const iconPack = iconPacks$1['lucide']; - const arrayBuffer = yield downloadZipFile(iconPack.downloadLink); - yield createZipFile(plugin, `${iconPack.name}.zip`, arrayBuffer); - yield registerIconPack(iconPack.name, arrayBuffer); -}); -const removeCustomLucideIconPack = (plugin) => __awaiter(void 0, void 0, void 0, function* () { - const iconPackIndex = iconPacks.findIndex((iconPack) => iconPack.name === LUCIDE_ICON_PACK_NAME); - if (iconPackIndex > -1) { - iconPacks.splice(iconPackIndex); - } - yield deleteIconPack(plugin, LUCIDE_ICON_PACK_NAME); -}); -const moveIconPackDirectories = (plugin, from, to) => __awaiter(void 0, void 0, void 0, function* () { - // Tries to move all icon packs to the new folder. - for (let i = 0; i < iconPacks.length; i++) { - const iconPack = iconPacks[i]; - if (yield plugin.app.vault.adapter.exists(`${from}/${iconPack.name}`)) { - // Tries to create a new directory in the new path. - const doesDirExist = yield createDirectory(plugin, iconPack.name); - if (doesDirExist) { - new obsidian.Notice(`Directory with name ${iconPack.name} already exists.`); - continue; - } - } - new obsidian.Notice(`Moving ${iconPack.name}...`); - // Move the zip file. - if (yield plugin.app.vault.adapter.exists(`${from}/${iconPack.name}.zip`)) { - yield plugin.app.vault.adapter.copy(`${from}/${iconPack.name}.zip`, `${to}/${iconPack.name}.zip`); - } - // Move all other files inside of the iconpack directory. - const filesInDirectory = yield getFilesInDirectory(plugin, `${from}/${iconPack.name}`); - for (const file of filesInDirectory) { - const fileName = file.split('/').pop(); - yield plugin.app.vault.adapter.copy(`${from}/${iconPack.name}/${fileName}`, `${to}/${iconPack.name}/${fileName}`); - } - new obsidian.Notice(`...moved ${iconPack.name}`); - } - // Removes all the existing icon packs in the `from` directory. - for (let i = 0; i < iconPacks.length; i++) { - const iconPack = iconPacks[i]; - if (yield plugin.app.vault.adapter.exists(`${from}/${iconPack.name}`)) { - yield plugin.app.vault.adapter.rmdir(`${from}/${iconPack.name}`, true); - } - } - // Remove root directory that contains all the icon packs. - if (!to.startsWith(from)) { - yield plugin.app.vault.adapter.rmdir(`${from}`, true); - } -}); -const createCustomIconPackDirectory = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () { - yield createDirectory(plugin, dir); - const prefix = createIconPackPrefix(dir); - iconPacks.push({ name: dir, icons: [], prefix, custom: true }); -}); -const deleteIconPack = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () { - iconPacks = iconPacks.filter((iconPack) => iconPack.name !== dir); - // Check for the icon pack directory and delete it. - if (yield plugin.app.vault.adapter.exists(`${path}/${dir}`)) { - yield plugin.app.vault.adapter.rmdir(`${path}/${dir}`, true); - } - // Check for the icon pack zip file and delete it. - if (yield plugin.app.vault.adapter.exists(`${path}/${dir}.zip`)) { - yield plugin.app.vault.adapter.remove(`${path}/${dir}.zip`); - } -}); -const doesIconPackExist = (plugin, iconPackName) => { - return plugin.app.vault.adapter.exists(`${path}/${iconPackName}`); -}; -const createDirectory = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () { - const doesDirExist = yield plugin.app.vault.adapter.exists(`${path}/${dir}`); - if (!doesDirExist) { - yield plugin.app.vault.adapter.mkdir(`${path}/${dir}`); - } - return doesDirExist; -}); -const getNormalizedName = (s) => { - return s - .split(/[ -]|[ _]/g) - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(''); -}; -// export const normalizeFileName = async (plugin: Plugin, oldPath: string) => { -// const fileName = oldPath.split('/').pop(); -// const newPath = oldPath.substring(0, oldPath.indexOf(fileName)) + getNormalizedName(fileName); -// await plugin.app.vault.adapter.rename(oldPath, newPath); -// }; -const createZipFile = (plugin, filename, buffer) => __awaiter(void 0, void 0, void 0, function* () { - yield plugin.app.vault.adapter.writeBinary(`${path}/${filename}`, buffer); -}); -const createFile = (plugin, iconPackName, filename, content, absoluteFilename) => __awaiter(void 0, void 0, void 0, function* () { - const normalizedFilename = getNormalizedName(filename); - const exists = yield plugin.app.vault.adapter.exists(`${path}/${iconPackName}/${normalizedFilename}`); - if (exists) { - const folderSplit = absoluteFilename.split('/'); - if (folderSplit.length >= 2) { - const folderName = folderSplit[folderSplit.length - 2]; - const newFilename = folderName + normalizedFilename; - yield plugin.app.vault.adapter.write(`${path}/${iconPackName}/${newFilename}`, content); - logger.info(`Renamed old file ${normalizedFilename} to ${newFilename} due to duplication`); - new obsidian.Notice(`[${config.PLUGIN_NAME}] Renamed ${normalizedFilename} to ${newFilename} to avoid duplication.`, 8000); - } - else { - logger.warn(`Could not create icons with duplicated file names (file name: ${normalizedFilename})`); - new obsidian.Notice(`[${config.PLUGIN_NAME}] Could not create duplicated icon name (${normalizedFilename})`, 8000); - } - } - else { - yield plugin.app.vault.adapter.write(`${path}/${iconPackName}/${normalizedFilename}`, content); - } -}); -const createDefaultDirectory = (plugin) => __awaiter(void 0, void 0, void 0, function* () { - yield createDirectory(plugin, ''); -}); -const getAllIconPacks = () => { - return iconPacks; -}; -const getFilesInDirectory = (plugin, dir) => __awaiter(void 0, void 0, void 0, function* () { - if (!(yield plugin.app.vault.adapter.exists(dir))) { - return []; - } - return (yield plugin.app.vault.adapter.list(dir)).files; -}); -const validIconName = /^[(A-Z)|(0-9)]/; -const svgViewboxRegex = /viewBox="([^"]*)"/g; -const svgContentRegex = /(.*?)<\/svg>/g; -const generateIcon = (iconPackName, iconName, content) => { - if (content.length === 0) { - return; - } - content = content.replace(/(\r\n|\n|\r)/gm, ''); - content = content.replace(/>\s+<'); - const normalizedName = iconName.charAt(0).toUpperCase() + iconName.substring(1); - if (!validIconName.exec(normalizedName)) { - logger.info(`Skipping icon with invalid name: ${iconName}`); - return null; - } - const svgViewboxMatch = content.match(svgViewboxRegex); - let svgViewbox = ''; - if (svgViewboxMatch && svgViewboxMatch.length !== 0) { - svgViewbox = svgViewboxMatch[0]; - } - const svgContentMatch = content.match(svgContentRegex); - if (!svgContentMatch) { - logger.info(`Skipping icon with invalid svg content: ${iconName}`); - return null; - } - const svgContent = svgContentMatch.map((val) => val.replace(/<\/?svg>/g, '').replace(//g, ''))[0]; - const iconPackPrefix = createIconPackPrefix(iconPackName); - const icon = { - name: normalizedName.split('.svg')[0], - prefix: iconPackPrefix, - iconPackName, - filename: iconName, - svgContent, - svgViewbox, - svgElement: svg.extract(content), - }; - return icon; -}; -const createIconPackPrefix = (iconPackName) => { - if (iconPackName.includes('-')) { - const splitted = iconPackName.split('-'); - let result = splitted[0].charAt(0).toUpperCase(); - for (let i = 1; i < splitted.length; i++) { - result += splitted[i].charAt(0).toLowerCase(); - } - return result; - } - return (iconPackName.charAt(0).toUpperCase() + iconPackName.charAt(1).toLowerCase()); -}; -const loadUsedIcons = (plugin, icons) => __awaiter(void 0, void 0, void 0, function* () { - const iconPacks = (yield listPath(plugin)).folders.map((iconPack) => iconPack.split('/').pop()); - if (plugin.doesUseNativeLucideIconPack()) { - iconPacks.push(LUCIDE_ICON_PACK_NAME); - } - for (let i = 0; i < icons.length; i++) { - const entry = icons[i]; - if (!entry) { - continue; - } - yield loadIcon(plugin, iconPacks, entry); - } -}); -const listPath = (plugin, listPath) => { - return plugin.app.vault.adapter.list(path); -}; -const getIconPackNameByPrefix = (prefix) => { - var _a; - return (_a = iconPacks.find((iconPack) => iconPack.prefix === prefix)) === null || _a === void 0 ? void 0 : _a.name; -}; -const nextIdentifier = (iconName) => { - return iconName.substring(1).search(/[(A-Z)|(0-9)]/) + 1; -}; -const loadIcon = (plugin, iconPackNames, iconName) => __awaiter(void 0, void 0, void 0, function* () { - const nextLetter = nextIdentifier(iconName); - const prefix = iconName.substring(0, nextLetter); - const name = iconName.substring(nextLetter); - const iconPack = iconPackNames.find((folder) => { - const folderPrefix = createIconPackPrefix(folder); - return prefix === folderPrefix; - }); - if (!iconPack) { - // Ignore because background check automatically adds the icons and icon pack - // directories. - if (!plugin.getSettings().iconsBackgroundCheckEnabled) { - new obsidian.Notice(`Seems like you do not have an icon pack installed. (${iconName})`, 5000); - } - return; - } - if (iconPack === LUCIDE_ICON_PACK_NAME && - plugin.doesUseNativeLucideIconPack()) { - // Native lucide icons already exist for Obsidian. - const lucideIcons = iconPacks.find((iconPack) => iconPack.name === LUCIDE_ICON_PACK_NAME); - const icon = lucideIcons.icons.find((icon) => icon.name === name); - if (!icon) { - logger.warn(`Icon ${icon} does not exist in the native Lucide icon pack.`); - return; - } - preloadedIcons.push(icon); - return; - } - const fullPath = path + '/' + iconPack + '/' + name + '.svg'; - if (!(yield plugin.app.vault.adapter.exists(fullPath))) { - logger.info(`Icon with name '${name}' was not found (full path: ${fullPath})`); - return; - } - const content = yield plugin.app.vault.adapter.read(fullPath); - const icon = generateIcon(iconPack, name, content); - preloadedIcons.push(icon); -}); -const initIconPacks = (plugin) => __awaiter(void 0, void 0, void 0, function* () { - // Remove the beginning slash because paths which start with `/` are the same as without - // a slash. - if (path.startsWith('/')) { - path = path.slice(1); - } - const loadedIconPacks = yield plugin.app.vault.adapter.list(path); - // Extract all zip files which will be downloaded icon packs. - const zipFiles = {}; - for (let i = 0; i < loadedIconPacks.files.length; i++) { - const fileName = loadedIconPacks.files[i]; - if (fileName.endsWith('.zip')) { - const arrayBuffer = yield plugin.app.vault.adapter.readBinary(fileName); - const files = yield readZipFile(arrayBuffer); - const iconPackName = fileName.split('/').pop().split('.zip')[0]; - zipFiles[iconPackName] = files; - } - } - // Check for custom-made icon packs. - for (let i = 0; i < loadedIconPacks.folders.length; i++) { - const folderName = loadedIconPacks.folders[i].split('/').pop(); - // Continue if the icon pack does have a zip file. - if (zipFiles[folderName]) { - continue; - } - const files = yield getFilesInDirectory(plugin, `${path}/${folderName}`); - const loadedIcons = []; - // Convert files into loaded svgs. - for (let j = 0; j < files.length; j++) { - const iconNameRegex = files[j].match(new RegExp(path + '/' + folderName + '/(.*)')); - const iconName = getNormalizedName(iconNameRegex[1]); - const iconContent = yield plugin.app.vault.adapter.read(files[j]); - const icon = generateIcon(folderName, iconName, iconContent); - if (icon) { - loadedIcons.push(icon); - } - } - const prefix = createIconPackPrefix(folderName); - if (!iconPacks.some((iconPack) => iconPack.name === folderName)) { - iconPacks.push({ - name: folderName, - icons: loadedIcons, - prefix, - custom: true, - }); - logger.info(`Loaded icon pack '${folderName}' (amount of icons: ${loadedIcons.length})`); - } - } - // Extract all files from the zip files. - for (const zipFile in zipFiles) { - const files = zipFiles[zipFile]; - const loadedIcons = yield getLoadedIconsFromZipFile(zipFile, files); - const prefix = createIconPackPrefix(zipFile); - if (zipFile === LUCIDE_ICON_PACK_NAME && - !plugin.doesUseCustomLucideIconPack()) { - continue; - } - if (!iconPacks.some((iconPack) => iconPack.name === zipFile)) { - iconPacks.push({ - name: zipFile, - icons: loadedIcons, - prefix, - custom: false, - }); - logger.info(`Loaded icon pack '${zipFile}' (amount of icons: ${loadedIcons.length})`); - } - } -}); -const getLoadedIconsFromZipFile = (iconPackName, files) => __awaiter(void 0, void 0, void 0, function* () { - const loadedIcons = []; - const extraPath = getExtraPath(iconPackName); - for (let j = 0; j < files.length; j++) { - // Checks if the icon pack has an extra path. Also ignores files which do not start - // with the extra path. - if (extraPath && !files[j].name.startsWith(extraPath)) { - continue; - } - const file = yield getFileFromJSZipFile(files[j]); - const iconContent = yield file.text(); - const iconName = getNormalizedName(file.name); - const icon = generateIcon(iconPackName, iconName, iconContent); - if (icon) { - loadedIcons.push(icon); - } - } - return loadedIcons; -}); -const addIconToIconPack = (iconPackName, iconName, iconContent) => { - // Normalize the icon name to remove `-` or `_` in the name. - iconName = getNormalizedName(iconName); - const icon = generateIcon(iconPackName, iconName, iconContent); - if (!icon) { - logger.warn(`Icon could not be generated (icon: ${iconName}, content: ${iconContent})`); - return undefined; - } - const iconPack = iconPacks.find((iconPack) => iconPack.name === iconPackName); - if (!iconPack) { - logger.warn(`Iconpack with name '${iconPackName}' was not found`); - return undefined; - } - iconPack.icons.push(icon); - return icon; -}; -const removeIconFromIconPackDirectory = (plugin, iconPackName, iconName) => { - const iconPack = iconPacks.find((iconPack) => iconPack.name === iconPackName); - // Checks if icon pack is custom-made. - if (!iconPack.custom) { - return plugin.app.vault.adapter.rmdir(`${path}/${iconPackName}/${iconName}.svg`, true); - } -}; -const extractIconToIconPack = (plugin, icon, iconContent) => __awaiter(void 0, void 0, void 0, function* () { - const doesIconPackDirExist = yield plugin.app.vault.adapter.exists(`${path}/${icon.iconPackName}`); - if (!doesIconPackDirExist) { - yield plugin.app.vault.adapter.mkdir(`${path}/${icon.iconPackName}`); - } - const doesIconFileExists = yield plugin.app.vault.adapter.exists(`${path}/${icon.iconPackName}/${icon.name}.svg`); - if (!doesIconFileExists) { - yield createFile(plugin, icon.iconPackName, `${icon.name}.svg`, iconContent); - } -}); -const getAllLoadedIconNames = () => { - return iconPacks.reduce((total, iconPack) => { - total.push(...iconPack.icons); - return total; - }, []); -}; -const registerIconPack = (name, arrayBuffer) => __awaiter(void 0, void 0, void 0, function* () { - const files = yield readZipFile(arrayBuffer); - const loadedIcons = yield getLoadedIconsFromZipFile(name, files); - const prefix = createIconPackPrefix(name); - iconPacks.push({ name, icons: loadedIcons, prefix, custom: false }); - logger.info(`Loaded icon pack ${name} (amount of icons: ${loadedIcons.length})`); -}); -const doesIconExists = (iconName) => { - const icons = getAllLoadedIconNames(); - return (icons.find((icon) => icon.name === iconName || icon.prefix + icon.name === iconName) !== undefined); -}; -const getIconsFromIconPack = (iconPackName) => { - return iconPacks.find((iconPack) => iconPack.name === iconPackName); -}; -const getIconFromIconPack = (iconPackName, iconPrefix, iconName) => { - const foundIcon = preloadedIcons.find((icon) => icon.prefix.toLowerCase() === iconPrefix.toLowerCase() && - icon.name.toLowerCase() === iconName.toLowerCase()); - if (foundIcon) { - return foundIcon; - } - const iconPack = iconPacks.find((iconPack) => iconPack.name === iconPackName); - if (!iconPack) { - return undefined; - } - return iconPack.icons.find((icon) => getNormalizedName(icon.name) === iconName); -}; -const getSvgFromLoadedIcon = (iconPrefix, iconName) => { - let icon = ''; - let foundIcon = preloadedIcons.find((icon) => icon.prefix.toLowerCase() === iconPrefix.toLowerCase() && - icon.name.toLowerCase() === iconName.toLowerCase()); - if (!foundIcon) { - iconPacks.forEach((iconPack) => { - const icon = iconPack.icons.find((icon) => { - return (icon.prefix.toLowerCase() === iconPrefix.toLowerCase() && - getNormalizedName(icon.name).toLowerCase() === iconName.toLowerCase()); - }); - if (icon) { - foundIcon = icon; - } - }); - } - if (foundIcon) { - icon = foundIcon.svgElement; - } - return icon; -}; - -/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */ -var twemoji=function(){var twemoji={base:"https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/",ext:".png",size:"72x72",className:"emoji",convert:{fromCodePoint:fromCodePoint,toCodePoint:toCodePoint},onerror:function onerror(){if(this.parentNode){this.parentNode.replaceChild(createText(this.alt,false),this);}},parse:parse,replace:replace,test:test},escaper={"&":"&","<":"<",">":">","'":"'",'"':"""},re=/(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])(?:\u200d\u27a1\ufe0f)?|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f(?:\u200d\u27a1\ufe0f)?)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f(?:\u200d\u27a1\ufe0f)?|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83e\uddd1\u200d\ud83e\uddd1\u200d\ud83e\uddd2\u200d\ud83e\uddd2|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83e\uddd1\u200d\ud83e\uddd1\u200d\ud83e\uddd2|\ud83e\uddd1\u200d\ud83e\uddd2\u200d\ud83e\uddd2|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u26d3\ufe0f\u200d\ud83d\udca5|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udf44\u200d\ud83d\udfeb|\ud83c\udf4b\u200d\ud83d\udfe9|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc26\u200d\ud83d\udd25|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83d\ude42\u200d\u2194\ufe0f|\ud83d\ude42\u200d\u2195\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddd1\u200d\ud83e\uddd2|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b|\ud83d\udc26\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|\ud83e\udef0|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c\udfc3|\ud83d\udeb6|\ud83e\uddce)(?:\ud83c[\udffb-\udfff])?(?:\u200d\u27a1\ufe0f)?|(?:\ud83c[\udf85\udfc2\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4\udeb5\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd\uddcf\uddd1-\udddd\udec3-\udec5\udef1-\udef8]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedc-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude7c\ude80-\ude88\ude90-\udebd\udebf-\udec2\udece-\udedb\udee0-\udee8]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g,UFE0Fg=/\uFE0F/g,U200D=String.fromCharCode(8205),rescaper=/[&<>'"]/g,shouldntBeParsed=/^(?:iframe|noframes|noscript|script|select|style|textarea)$/,fromCharCode=String.fromCharCode;return twemoji;function createText(text,clean){return document.createTextNode(clean?text.replace(UFE0Fg,""):text)}function escapeHTML(s){return s.replace(rescaper,replacer)}function defaultImageSrcGenerator(icon,options){return "".concat(options.base,options.size,"/",icon,options.ext)}function grabAllTextNodes(node,allText){var childNodes=node.childNodes,length=childNodes.length,subnode,nodeType;while(length--){subnode=childNodes[length];nodeType=subnode.nodeType;if(nodeType===3){allText.push(subnode);}else if(nodeType===1&&!("ownerSVGElement"in subnode)&&!shouldntBeParsed.test(subnode.nodeName.toLowerCase())){grabAllTextNodes(subnode,allText);}}return allText}function grabTheRightIcon(rawText){return toCodePoint(rawText.indexOf(U200D)<0?rawText.replace(UFE0Fg,""):rawText)}function parseNode(node,options){var allText=grabAllTextNodes(node,[]),length=allText.length,attrib,attrname,modified,fragment,subnode,text,match,i,index,img,rawText,iconId,src;while(length--){modified=false;fragment=document.createDocumentFragment();subnode=allText[length];text=subnode.nodeValue;i=0;while(match=re.exec(text)){index=match.index;if(index!==i){fragment.appendChild(createText(text.slice(i,index),true));}rawText=match[0];iconId=grabTheRightIcon(rawText);i=index+rawText.length;src=options.callback(iconId,options);if(iconId&&src){img=new Image;img.onerror=options.onerror;img.setAttribute("draggable","false");attrib=options.attributes(rawText,iconId);for(attrname in attrib){if(attrib.hasOwnProperty(attrname)&&attrname.indexOf("on")!==0&&!img.hasAttribute(attrname)){img.setAttribute(attrname,attrib[attrname]);}}img.className=options.className;img.alt=rawText;img.src=src;modified=true;fragment.appendChild(img);}if(!img)fragment.appendChild(createText(rawText,false));img=null;}if(modified){if(i");}return ret})}function replacer(m){return escaper[m]}function returnNull(){return null}function toSizeSquaredAsset(value){return typeof value==="number"?value+"x"+value:value}function fromCodePoint(codepoint){var code=typeof codepoint==="string"?parseInt(codepoint,16):codepoint;if(code<65536){return fromCharCode(code)}code-=65536;return fromCharCode(55296+(code>>10),56320+(code&1023))}function parse(what,how){if(!how||typeof how==="function"){how={callback:how};}return (typeof what==="string"?parseString:parseNode)(what,{callback:how.callback||defaultImageSrcGenerator,attributes:typeof how.attributes==="function"?how.attributes:returnNull,base:typeof how.base==="string"?how.base:twemoji.base,ext:how.ext||twemoji.ext,size:how.folder||toSizeSquaredAsset(how.size||twemoji.size),className:how.className||twemoji.className,onerror:how.onerror||twemoji.onerror})}function replace(text,callback){return String(text).replace(re,callback)}function test(text){re.lastIndex=0;var result=re.test(text);re.lastIndex=0;return result}function toCodePoint(unicodeSurrogates,sep){var r=[],c=0,p=0,i=0;while(i { - return new RegExp(/[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC2\uDECE-\uDEDB\uDEE0-\uDEE8]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)/, 'g'); -}; -const shortNames = { - '😀': 'grinning face', - '😃': 'grinning face with big eyes', - '😄': 'grinning face with smiling eyes', - '😁': 'beaming face with smiling eyes', - '😆': 'grinning squinting face', - '😅': 'grinning face with sweat', - '🤣': 'rolling on the floor laughing', - '😂': 'face with tears of joy', - '🙂': 'slightly smiling face', - '🙃': 'upside-down face', - '🫠': '⊛ melting face', - '😉': 'winking face', - '😊': 'smiling face with smiling eyes', - '😇': 'smiling face with halo', - '🥰': 'smiling face with hearts', - '😍': 'smiling face with heart-eyes', - '🤩': 'star-struck', - '😘': 'face blowing a kiss', - '😗': 'kissing face', - '☺': 'smiling face', - '😚': 'kissing face with closed eyes', - '😙': 'kissing face with smiling eyes', - '🥲': 'smiling face with tear', - '😋': 'face savoring food', - '😛': 'face with tongue', - '😜': 'winking face with tongue', - '🤪': 'zany face', - '😝': 'squinting face with tongue', - '🤑': 'money-mouth face', - '🤗': 'smiling face with open hands', - '🤭': 'face with hand over mouth', - '🫢': '⊛ face with open eyes and hand over mouth', - '🫣': '⊛ face with peeking eye', - '🤫': 'shushing face', - '🤔': 'thinking face', - '🫡': '⊛ saluting face', - '🤐': 'zipper-mouth face', - '🤨': 'face with raised eyebrow', - '😐': 'neutral face', - '😑': 'expressionless face', - '😶': 'face without mouth', - '🫥': '⊛ dotted line face', - '😶‍🌫️': 'face in clouds', - '😏': 'smirking face', - '😒': 'unamused face', - '🙄': 'face with rolling eyes', - '😬': 'grimacing face', - '😮‍💨': 'face exhaling', - '🤥': 'lying face', - '😌': 'relieved face', - '😔': 'pensive face', - '😪': 'sleepy face', - '🤤': 'drooling face', - '😴': 'sleeping face', - '😷': 'face with medical mask', - '🤒': 'face with thermometer', - '🤕': 'face with head-bandage', - '🤢': 'nauseated face', - '🤮': 'face vomiting', - '🤧': 'sneezing face', - '🥵': 'hot face', - '🥶': 'cold face', - '🥴': 'woozy face', - '😵': 'face with crossed-out eyes', - '😵‍💫': 'face with spiral eyes', - '🤯': 'exploding head', - '🤠': 'cowboy hat face', - '🥳': 'partying face', - '🥸': 'disguised face', - '😎': 'smiling face with sunglasses', - '🤓': 'nerd face', - '🧐': 'face with monocle', - '😕': 'confused face', - '🫤': '⊛ face with diagonal mouth', - '😟': 'worried face', - '🙁': 'slightly frowning face', - '☹': 'frowning face', - '😮': 'face with open mouth', - '😯': 'hushed face', - '😲': 'astonished face', - '😳': 'flushed face', - '🥺': 'pleading face', - '🥹': '⊛ face holding back tears', - '😦': 'frowning face with open mouth', - '😧': 'anguished face', - '😨': 'fearful face', - '😰': 'anxious face with sweat', - '😥': 'sad but relieved face', - '😢': 'crying face', - '😭': 'loudly crying face', - '😱': 'face screaming in fear', - '😖': 'confounded face', - '😣': 'persevering face', - '😞': 'disappointed face', - '😓': 'downcast face with sweat', - '😩': 'weary face', - '😫': 'tired face', - '🥱': 'yawning face', - '😤': 'face with steam from nose', - '😡': 'pouting face', - '😠': 'angry face', - '🤬': 'face with symbols on mouth', - '😈': 'smiling face with horns', - '👿': 'angry face with horns', - '💀': 'skull', - '☠': 'skull and crossbones', - '💩': 'pile of poo', - '🤡': 'clown face', - '👹': 'ogre', - '👺': 'goblin', - '👻': 'ghost', - '👽': 'alien', - '👾': 'alien monster', - '🤖': 'robot', - '😺': 'grinning cat', - '😸': 'grinning cat with smiling eyes', - '😹': 'cat with tears of joy', - '😻': 'smiling cat with heart-eyes', - '😼': 'cat with wry smile', - '😽': 'kissing cat', - '🙀': 'weary cat', - '😿': 'crying cat', - '😾': 'pouting cat', - '🙈': 'see-no-evil monkey', - '🙉': 'hear-no-evil monkey', - '🙊': 'speak-no-evil monkey', - '💋': 'kiss mark', - '💌': 'love letter', - '💘': 'heart with arrow', - '💝': 'heart with ribbon', - '💖': 'sparkling heart', - '💗': 'growing heart', - '💓': 'beating heart', - '💞': 'revolving hearts', - '💕': 'two hearts', - '💟': 'heart decoration', - '❣': 'heart exclamation', - '💔': 'broken heart', - '❤️‍🔥': 'heart on fire', - '❤️‍🩹': 'mending heart', - '❤': 'red heart', - '🧡': 'orange heart', - '💛': 'yellow heart', - '💚': 'green heart', - '💙': 'blue heart', - '💜': 'purple heart', - '🤎': 'brown heart', - '🖤': 'black heart', - '🤍': 'white heart', - '💯': 'hundred points', - '💢': 'anger symbol', - '💥': 'collision', - '💫': 'dizzy', - '💦': 'sweat droplets', - '💨': 'dashing away', - '🕳': 'hole', - '💣': 'bomb', - '💬': 'speech balloon', - '👁️‍🗨️': 'eye in speech bubble', - '🗨': 'left speech bubble', - '🗯': 'right anger bubble', - '💭': 'thought balloon', - '💤': 'zzz', - '👋': 'waving hand', - '🤚': 'raised back of hand', - '🖐': 'hand with fingers splayed', - '✋': 'raised hand', - '🖖': 'vulcan salute', - '🫱': '⊛ rightwards hand', - '🫲': '⊛ leftwards hand', - '🫳': '⊛ palm down hand', - '🫴': '⊛ palm up hand', - '👌': 'OK hand', - '🤌': 'pinched fingers', - '🤏': 'pinching hand', - '✌': 'victory hand', - '🤞': 'crossed fingers', - '🫰': '⊛ hand with index finger and thumb crossed', - '🤟': 'love-you gesture', - '🤘': 'sign of the horns', - '🤙': 'call me hand', - '👈': 'backhand index pointing left', - '👉': 'backhand index pointing right', - '👆': 'backhand index pointing up', - '🖕': 'middle finger', - '👇': 'backhand index pointing down', - '☝': 'index pointing up', - '🫵': '⊛ index pointing at the viewer', - '👍': 'thumbs up', - '👎': 'thumbs down', - '✊': 'raised fist', - '👊': 'oncoming fist', - '🤛': 'left-facing fist', - '🤜': 'right-facing fist', - '👏': 'clapping hands', - '🙌': 'raising hands', - '🫶': '⊛ heart hands', - '👐': 'open hands', - '🤲': 'palms up together', - '🤝': 'handshake', - '🙏': 'folded hands', - '✍': 'writing hand', - '💅': 'nail polish', - '🤳': 'selfie', - '💪': 'flexed biceps', - '🦾': 'mechanical arm', - '🦿': 'mechanical leg', - '🦵': 'leg', - '🦶': 'foot', - '👂': 'ear', - '🦻': 'ear with hearing aid', - '👃': 'nose', - '🧠': 'brain', - '🫀': 'anatomical heart', - '🫁': 'lungs', - '🦷': 'tooth', - '🦴': 'bone', - '👀': 'eyes', - '👁': 'eye', - '👅': 'tongue', - '👄': 'mouth', - '🫦': '⊛ biting lip', - '👶': 'baby', - '🧒': 'child', - '👦': 'boy', - '👧': 'girl', - '🧑': 'person', - '👱': 'person: blond hair', - '👨': 'man', - '🧔': 'person: beard', - '🧔‍♂️': 'man: beard', - '🧔‍♀️': 'woman: beard', - '👨‍🦰': 'man: red hair', - '👨‍🦱': 'man: curly hair', - '👨‍🦳': 'man: white hair', - '👨‍🦲': 'man: bald', - '👩': 'woman', - '👩‍🦰': 'woman: red hair', - '🧑‍🦰': 'person: red hair', - '👩‍🦱': 'woman: curly hair', - '🧑‍🦱': 'person: curly hair', - '👩‍🦳': 'woman: white hair', - '🧑‍🦳': 'person: white hair', - '👩‍🦲': 'woman: bald', - '🧑‍🦲': 'person: bald', - '👱‍♀️': 'woman: blond hair', - '👱‍♂️': 'man: blond hair', - '🧓': 'older person', - '👴': 'old man', - '👵': 'old woman', - '🙍': 'person frowning', - '🙍‍♂️': 'man frowning', - '🙍‍♀️': 'woman frowning', - '🙎': 'person pouting', - '🙎‍♂️': 'man pouting', - '🙎‍♀️': 'woman pouting', - '🙅': 'person gesturing NO', - '🙅‍♂️': 'man gesturing NO', - '🙅‍♀️': 'woman gesturing NO', - '🙆': 'person gesturing OK', - '🙆‍♂️': 'man gesturing OK', - '🙆‍♀️': 'woman gesturing OK', - '💁': 'person tipping hand', - '💁‍♂️': 'man tipping hand', - '💁‍♀️': 'woman tipping hand', - '🙋': 'person raising hand', - '🙋‍♂️': 'man raising hand', - '🙋‍♀️': 'woman raising hand', - '🧏': 'deaf person', - '🧏‍♂️': 'deaf man', - '🧏‍♀️': 'deaf woman', - '🙇': 'person bowing', - '🙇‍♂️': 'man bowing', - '🙇‍♀️': 'woman bowing', - '🤦': 'person facepalming', - '🤦‍♂️': 'man facepalming', - '🤦‍♀️': 'woman facepalming', - '🤷': 'person shrugging', - '🤷‍♂️': 'man shrugging', - '🤷‍♀️': 'woman shrugging', - '🧑‍⚕️': 'health worker', - '👨‍⚕️': 'man health worker', - '👩‍⚕️': 'woman health worker', - '🧑‍🎓': 'student', - '👨‍🎓': 'man student', - '👩‍🎓': 'woman student', - '🧑‍🏫': 'teacher', - '👨‍🏫': 'man teacher', - '👩‍🏫': 'woman teacher', - '🧑‍⚖️': 'judge', - '👨‍⚖️': 'man judge', - '👩‍⚖️': 'woman judge', - '🧑‍🌾': 'farmer', - '👨‍🌾': 'man farmer', - '👩‍🌾': 'woman farmer', - '🧑‍🍳': 'cook', - '👨‍🍳': 'man cook', - '👩‍🍳': 'woman cook', - '🧑‍🔧': 'mechanic', - '👨‍🔧': 'man mechanic', - '👩‍🔧': 'woman mechanic', - '🧑‍🏭': 'factory worker', - '👨‍🏭': 'man factory worker', - '👩‍🏭': 'woman factory worker', - '🧑‍💼': 'office worker', - '👨‍💼': 'man office worker', - '👩‍💼': 'woman office worker', - '🧑‍🔬': 'scientist', - '👨‍🔬': 'man scientist', - '👩‍🔬': 'woman scientist', - '🧑‍💻': 'technologist', - '👨‍💻': 'man technologist', - '👩‍💻': 'woman technologist', - '🧑‍🎤': 'singer', - '👨‍🎤': 'man singer', - '👩‍🎤': 'woman singer', - '🧑‍🎨': 'artist', - '👨‍🎨': 'man artist', - '👩‍🎨': 'woman artist', - '🧑‍✈️': 'pilot', - '👨‍✈️': 'man pilot', - '👩‍✈️': 'woman pilot', - '🧑‍🚀': 'astronaut', - '👨‍🚀': 'man astronaut', - '👩‍🚀': 'woman astronaut', - '🧑‍🚒': 'firefighter', - '👨‍🚒': 'man firefighter', - '👩‍🚒': 'woman firefighter', - '👮': 'police officer', - '👮‍♂️': 'man police officer', - '👮‍♀️': 'woman police officer', - '🕵': 'detective', - '🕵️‍♂️': 'man detective', - '🕵️‍♀️': 'woman detective', - '💂': 'guard', - '💂‍♂️': 'man guard', - '💂‍♀️': 'woman guard', - '🥷': 'ninja', - '👷': 'construction worker', - '👷‍♂️': 'man construction worker', - '👷‍♀️': 'woman construction worker', - '🫅': '⊛ person with crown', - '🤴': 'prince', - '👸': 'princess', - '👳': 'person wearing turban', - '👳‍♂️': 'man wearing turban', - '👳‍♀️': 'woman wearing turban', - '👲': 'person with skullcap', - '🧕': 'woman with headscarf', - '🤵': 'person in tuxedo', - '🤵‍♂️': 'man in tuxedo', - '🤵‍♀️': 'woman in tuxedo', - '👰': 'person with veil', - '👰‍♂️': 'man with veil', - '👰‍♀️': 'woman with veil', - '🤰': 'pregnant woman', - '🫃': '⊛ pregnant man', - '🫄': '⊛ pregnant person', - '🤱': 'breast-feeding', - '👩‍🍼': 'woman feeding baby', - '👨‍🍼': 'man feeding baby', - '🧑‍🍼': 'person feeding baby', - '👼': 'baby angel', - '🎅': 'Santa Claus', - '🤶': 'Mrs. Claus', - '🧑‍🎄': 'mx claus', - '🦸': 'superhero', - '🦸‍♂️': 'man superhero', - '🦸‍♀️': 'woman superhero', - '🦹': 'supervillain', - '🦹‍♂️': 'man supervillain', - '🦹‍♀️': 'woman supervillain', - '🧙': 'mage', - '🧙‍♂️': 'man mage', - '🧙‍♀️': 'woman mage', - '🧚': 'fairy', - '🧚‍♂️': 'man fairy', - '🧚‍♀️': 'woman fairy', - '🧛': 'vampire', - '🧛‍♂️': 'man vampire', - '🧛‍♀️': 'woman vampire', - '🧜': 'merperson', - '🧜‍♂️': 'merman', - '🧜‍♀️': 'mermaid', - '🧝': 'elf', - '🧝‍♂️': 'man elf', - '🧝‍♀️': 'woman elf', - '🧞': 'genie', - '🧞‍♂️': 'man genie', - '🧞‍♀️': 'woman genie', - '🧟': 'zombie', - '🧟‍♂️': 'man zombie', - '🧟‍♀️': 'woman zombie', - '🧌': '⊛ troll', - '💆': 'person getting massage', - '💆‍♂️': 'man getting massage', - '💆‍♀️': 'woman getting massage', - '💇': 'person getting haircut', - '💇‍♂️': 'man getting haircut', - '💇‍♀️': 'woman getting haircut', - '🚶': 'person walking', - '🚶‍♂️': 'man walking', - '🚶‍♀️': 'woman walking', - '🧍': 'person standing', - '🧍‍♂️': 'man standing', - '🧍‍♀️': 'woman standing', - '🧎': 'person kneeling', - '🧎‍♂️': 'man kneeling', - '🧎‍♀️': 'woman kneeling', - '🧑‍🦯': 'person with white cane', - '👨‍🦯': 'man with white cane', - '👩‍🦯': 'woman with white cane', - '🧑‍🦼': 'person in motorized wheelchair', - '👨‍🦼': 'man in motorized wheelchair', - '👩‍🦼': 'woman in motorized wheelchair', - '🧑‍🦽': 'person in manual wheelchair', - '👨‍🦽': 'man in manual wheelchair', - '👩‍🦽': 'woman in manual wheelchair', - '🏃': 'person running', - '🏃‍♂️': 'man running', - '🏃‍♀️': 'woman running', - '💃': 'woman dancing', - '🕺': 'man dancing', - '🕴': 'person in suit levitating', - '👯': 'people with bunny ears', - '👯‍♂️': 'men with bunny ears', - '👯‍♀️': 'women with bunny ears', - '🧖': 'person in steamy room', - '🧖‍♂️': 'man in steamy room', - '🧖‍♀️': 'woman in steamy room', - '🧗': 'person climbing', - '🧗‍♂️': 'man climbing', - '🧗‍♀️': 'woman climbing', - '🤺': 'person fencing', - '🏇': 'horse racing', - '⛷': 'skier', - '🏂': 'snowboarder', - '🏌': 'person golfing', - '🏌️‍♂️': 'man golfing', - '🏌️‍♀️': 'woman golfing', - '🏄': 'person surfing', - '🏄‍♂️': 'man surfing', - '🏄‍♀️': 'woman surfing', - '🚣': 'person rowing boat', - '🚣‍♂️': 'man rowing boat', - '🚣‍♀️': 'woman rowing boat', - '🏊': 'person swimming', - '🏊‍♂️': 'man swimming', - '🏊‍♀️': 'woman swimming', - '⛹': 'person bouncing ball', - '⛹️‍♂️': 'man bouncing ball', - '⛹️‍♀️': 'woman bouncing ball', - '🏋': 'person lifting weights', - '🏋️‍♂️': 'man lifting weights', - '🏋️‍♀️': 'woman lifting weights', - '🚴': 'person biking', - '🚴‍♂️': 'man biking', - '🚴‍♀️': 'woman biking', - '🚵': 'person mountain biking', - '🚵‍♂️': 'man mountain biking', - '🚵‍♀️': 'woman mountain biking', - '🤸': 'person cartwheeling', - '🤸‍♂️': 'man cartwheeling', - '🤸‍♀️': 'woman cartwheeling', - '🤼': 'people wrestling', - '🤼‍♂️': 'men wrestling', - '🤼‍♀️': 'women wrestling', - '🤽': 'person playing water polo', - '🤽‍♂️': 'man playing water polo', - '🤽‍♀️': 'woman playing water polo', - '🤾': 'person playing handball', - '🤾‍♂️': 'man playing handball', - '🤾‍♀️': 'woman playing handball', - '🤹': 'person juggling', - '🤹‍♂️': 'man juggling', - '🤹‍♀️': 'woman juggling', - '🧘': 'person in lotus position', - '🧘‍♂️': 'man in lotus position', - '🧘‍♀️': 'woman in lotus position', - '🛀': 'person taking bath', - '🛌': 'person in bed', - '🧑‍🤝‍🧑': 'people holding hands', - '👭': 'women holding hands', - '👫': 'woman and man holding hands', - '👬': 'men holding hands', - '💏': 'kiss', - '👩‍❤️‍💋‍👨': 'kiss: woman, man', - '👨‍❤️‍💋‍👨': 'kiss: man, man', - '👩‍❤️‍💋‍👩': 'kiss: woman, woman', - '💑': 'couple with heart', - '👩‍❤️‍👨': 'couple with heart: woman, man', - '👨‍❤️‍👨': 'couple with heart: man, man', - '👩‍❤️‍👩': 'couple with heart: woman, woman', - '👪': 'family', - '👨‍👩‍👦': 'family: man, woman, boy', - '👨‍👩‍👧': 'family: man, woman, girl', - '👨‍👩‍👧‍👦': 'family: man, woman, girl, boy', - '👨‍👩‍👦‍👦': 'family: man, woman, boy, boy', - '👨‍👩‍👧‍👧': 'family: man, woman, girl, girl', - '👨‍👨‍👦': 'family: man, man, boy', - '👨‍👨‍👧': 'family: man, man, girl', - '👨‍👨‍👧‍👦': 'family: man, man, girl, boy', - '👨‍👨‍👦‍👦': 'family: man, man, boy, boy', - '👨‍👨‍👧‍👧': 'family: man, man, girl, girl', - '👩‍👩‍👦': 'family: woman, woman, boy', - '👩‍👩‍👧': 'family: woman, woman, girl', - '👩‍👩‍👧‍👦': 'family: woman, woman, girl, boy', - '👩‍👩‍👦‍👦': 'family: woman, woman, boy, boy', - '👩‍👩‍👧‍👧': 'family: woman, woman, girl, girl', - '👨‍👦': 'family: man, boy', - '👨‍👦‍👦': 'family: man, boy, boy', - '👨‍👧': 'family: man, girl', - '👨‍👧‍👦': 'family: man, girl, boy', - '👨‍👧‍👧': 'family: man, girl, girl', - '👩‍👦': 'family: woman, boy', - '👩‍👦‍👦': 'family: woman, boy, boy', - '👩‍👧': 'family: woman, girl', - '👩‍👧‍👦': 'family: woman, girl, boy', - '👩‍👧‍👧': 'family: woman, girl, girl', - '🗣': 'speaking head', - '👤': 'bust in silhouette', - '👥': 'busts in silhouette', - '🫂': 'people hugging', - '👣': 'footprints', - '🦰': 'red hair', - '🦱': 'curly hair', - '🦳': 'white hair', - '🦲': 'bald', - '🐵': 'monkey face', - '🐒': 'monkey', - '🦍': 'gorilla', - '🦧': 'orangutan', - '🐶': 'dog face', - '🐕': 'dog', - '🦮': 'guide dog', - '🐕‍🦺': 'service dog', - '🐩': 'poodle', - '🐺': 'wolf', - '🦊': 'fox', - '🦝': 'raccoon', - '🐱': 'cat face', - '🐈': 'cat', - '🐈‍⬛': 'black cat', - '🦁': 'lion', - '🐯': 'tiger face', - '🐅': 'tiger', - '🐆': 'leopard', - '🐴': 'horse face', - '🐎': 'horse', - '🦄': 'unicorn', - '🦓': 'zebra', - '🦌': 'deer', - '🦬': 'bison', - '🐮': 'cow face', - '🐂': 'ox', - '🐃': 'water buffalo', - '🐄': 'cow', - '🐷': 'pig face', - '🐖': 'pig', - '🐗': 'boar', - '🐽': 'pig nose', - '🐏': 'ram', - '🐑': 'ewe', - '🐐': 'goat', - '🐪': 'camel', - '🐫': 'two-hump camel', - '🦙': 'llama', - '🦒': 'giraffe', - '🐘': 'elephant', - '🦣': 'mammoth', - '🦏': 'rhinoceros', - '🦛': 'hippopotamus', - '🐭': 'mouse face', - '🐁': 'mouse', - '🐀': 'rat', - '🐹': 'hamster', - '🐰': 'rabbit face', - '🐇': 'rabbit', - '🐿': 'chipmunk', - '🦫': 'beaver', - '🦔': 'hedgehog', - '🦇': 'bat', - '🐻': 'bear', - '🐻‍❄️': 'polar bear', - '🐨': 'koala', - '🐼': 'panda', - '🦥': 'sloth', - '🦦': 'otter', - '🦨': 'skunk', - '🦘': 'kangaroo', - '🦡': 'badger', - '🐾': 'paw prints', - '🦃': 'turkey', - '🐔': 'chicken', - '🐓': 'rooster', - '🐣': 'hatching chick', - '🐤': 'baby chick', - '🐥': 'front-facing baby chick', - '🐦': 'bird', - '🐧': 'penguin', - '🕊': 'dove', - '🦅': 'eagle', - '🦆': 'duck', - '🦢': 'swan', - '🦉': 'owl', - '🦤': 'dodo', - '🪶': 'feather', - '🦩': 'flamingo', - '🦚': 'peacock', - '🦜': 'parrot', - '🐸': 'frog', - '🐊': 'crocodile', - '🐢': 'turtle', - '🦎': 'lizard', - '🐍': 'snake', - '🐲': 'dragon face', - '🐉': 'dragon', - '🦕': 'sauropod', - '🦖': 'T-Rex', - '🐳': 'spouting whale', - '🐋': 'whale', - '🐬': 'dolphin', - '🦭': 'seal', - '🐟': 'fish', - '🐠': 'tropical fish', - '🐡': 'blowfish', - '🦈': 'shark', - '🐙': 'octopus', - '🐚': 'spiral shell', - '🪸': '⊛ coral', - '🐌': 'snail', - '🦋': 'butterfly', - '🐛': 'bug', - '🐜': 'ant', - '🐝': 'honeybee', - '🪲': 'beetle', - '🐞': 'lady beetle', - '🦗': 'cricket', - '🪳': 'cockroach', - '🕷': 'spider', - '🕸': 'spider web', - '🦂': 'scorpion', - '🦟': 'mosquito', - '🪰': 'fly', - '🪱': 'worm', - '🦠': 'microbe', - '💐': 'bouquet', - '🌸': 'cherry blossom', - '💮': 'white flower', - '🪷': '⊛ lotus', - '🏵': 'rosette', - '🌹': 'rose', - '🥀': 'wilted flower', - '🌺': 'hibiscus', - '🌻': 'sunflower', - '🌼': 'blossom', - '🌷': 'tulip', - '🌱': 'seedling', - '🪴': 'potted plant', - '🌲': 'evergreen tree', - '🌳': 'deciduous tree', - '🌴': 'palm tree', - '🌵': 'cactus', - '🌾': 'sheaf of rice', - '🌿': 'herb', - '☘': 'shamrock', - '🍀': 'four leaf clover', - '🍁': 'maple leaf', - '🍂': 'fallen leaf', - '🍃': 'leaf fluttering in wind', - '🪹': '⊛ empty nest', - '🪺': '⊛ nest with eggs', - '🍇': 'grapes', - '🍈': 'melon', - '🍉': 'watermelon', - '🍊': 'tangerine', - '🍋': 'lemon', - '🍌': 'banana', - '🍍': 'pineapple', - '🥭': 'mango', - '🍎': 'red apple', - '🍏': 'green apple', - '🍐': 'pear', - '🍑': 'peach', - '🍒': 'cherries', - '🍓': 'strawberry', - '🫐': 'blueberries', - '🥝': 'kiwi fruit', - '🍅': 'tomato', - '🫒': 'olive', - '🥥': 'coconut', - '🥑': 'avocado', - '🍆': 'eggplant', - '🥔': 'potato', - '🥕': 'carrot', - '🌽': 'ear of corn', - '🌶': 'hot pepper', - '🫑': 'bell pepper', - '🥒': 'cucumber', - '🥬': 'leafy green', - '🥦': 'broccoli', - '🧄': 'garlic', - '🧅': 'onion', - '🍄': 'mushroom', - '🥜': 'peanuts', - '🫘': '⊛ beans', - '🌰': 'chestnut', - '🍞': 'bread', - '🥐': 'croissant', - '🥖': 'baguette bread', - '🫓': 'flatbread', - '🥨': 'pretzel', - '🥯': 'bagel', - '🥞': 'pancakes', - '🧇': 'waffle', - '🧀': 'cheese wedge', - '🍖': 'meat on bone', - '🍗': 'poultry leg', - '🥩': 'cut of meat', - '🥓': 'bacon', - '🍔': 'hamburger', - '🍟': 'french fries', - '🍕': 'pizza', - '🌭': 'hot dog', - '🥪': 'sandwich', - '🌮': 'taco', - '🌯': 'burrito', - '🫔': 'tamale', - '🥙': 'stuffed flatbread', - '🧆': 'falafel', - '🥚': 'egg', - '🍳': 'cooking', - '🥘': 'shallow pan of food', - '🍲': 'pot of food', - '🫕': 'fondue', - '🥣': 'bowl with spoon', - '🥗': 'green salad', - '🍿': 'popcorn', - '🧈': 'butter', - '🧂': 'salt', - '🥫': 'canned food', - '🍱': 'bento box', - '🍘': 'rice cracker', - '🍙': 'rice ball', - '🍚': 'cooked rice', - '🍛': 'curry rice', - '🍜': 'steaming bowl', - '🍝': 'spaghetti', - '🍠': 'roasted sweet potato', - '🍢': 'oden', - '🍣': 'sushi', - '🍤': 'fried shrimp', - '🍥': 'fish cake with swirl', - '🥮': 'moon cake', - '🍡': 'dango', - '🥟': 'dumpling', - '🥠': 'fortune cookie', - '🥡': 'takeout box', - '🦀': 'crab', - '🦞': 'lobster', - '🦐': 'shrimp', - '🦑': 'squid', - '🦪': 'oyster', - '🍦': 'soft ice cream', - '🍧': 'shaved ice', - '🍨': 'ice cream', - '🍩': 'doughnut', - '🍪': 'cookie', - '🎂': 'birthday cake', - '🍰': 'shortcake', - '🧁': 'cupcake', - '🥧': 'pie', - '🍫': 'chocolate bar', - '🍬': 'candy', - '🍭': 'lollipop', - '🍮': 'custard', - '🍯': 'honey pot', - '🍼': 'baby bottle', - '🥛': 'glass of milk', - '☕': 'hot beverage', - '🫖': 'teapot', - '🍵': 'teacup without handle', - '🍶': 'sake', - '🍾': 'bottle with popping cork', - '🍷': 'wine glass', - '🍸': 'cocktail glass', - '🍹': 'tropical drink', - '🍺': 'beer mug', - '🍻': 'clinking beer mugs', - '🥂': 'clinking glasses', - '🥃': 'tumbler glass', - '🫗': '⊛ pouring liquid', - '🥤': 'cup with straw', - '🧋': 'bubble tea', - '🧃': 'beverage box', - '🧉': 'mate', - '🧊': 'ice', - '🥢': 'chopsticks', - '🍽': 'fork and knife with plate', - '🍴': 'fork and knife', - '🥄': 'spoon', - '🔪': 'kitchen knife', - '🫙': '⊛ jar', - '🏺': 'amphora', - '🌍': 'globe showing Europe-Africa', - '🌎': 'globe showing Americas', - '🌏': 'globe showing Asia-Australia', - '🌐': 'globe with meridians', - '🗺': 'world map', - '🗾': 'map of Japan', - '🧭': 'compass', - '🏔': 'snow-capped mountain', - '⛰': 'mountain', - '🌋': 'volcano', - '🗻': 'mount fuji', - '🏕': 'camping', - '🏖': 'beach with umbrella', - '🏜': 'desert', - '🏝': 'desert island', - '🏞': 'national park', - '🏟': 'stadium', - '🏛': 'classical building', - '🏗': 'building construction', - '🧱': 'brick', - '🪨': 'rock', - '🪵': 'wood', - '🛖': 'hut', - '🏘': 'houses', - '🏚': 'derelict house', - '🏠': 'house', - '🏡': 'house with garden', - '🏢': 'office building', - '🏣': 'Japanese post office', - '🏤': 'post office', - '🏥': 'hospital', - '🏦': 'bank', - '🏨': 'hotel', - '🏩': 'love hotel', - '🏪': 'convenience store', - '🏫': 'school', - '🏬': 'department store', - '🏭': 'factory', - '🏯': 'Japanese castle', - '🏰': 'castle', - '💒': 'wedding', - '🗼': 'Tokyo tower', - '🗽': 'Statue of Liberty', - '⛪': 'church', - '🕌': 'mosque', - '🛕': 'hindu temple', - '🕍': 'synagogue', - '⛩': 'shinto shrine', - '🕋': 'kaaba', - '⛲': 'fountain', - '⛺': 'tent', - '🌁': 'foggy', - '🌃': 'night with stars', - '🏙': 'cityscape', - '🌄': 'sunrise over mountains', - '🌅': 'sunrise', - '🌆': 'cityscape at dusk', - '🌇': 'sunset', - '🌉': 'bridge at night', - '♨': 'hot springs', - '🎠': 'carousel horse', - '🛝': '⊛ playground slide', - '🎡': 'ferris wheel', - '🎢': 'roller coaster', - '💈': 'barber pole', - '🎪': 'circus tent', - '🚂': 'locomotive', - '🚃': 'railway car', - '🚄': 'high-speed train', - '🚅': 'bullet train', - '🚆': 'train', - '🚇': 'metro', - '🚈': 'light rail', - '🚉': 'station', - '🚊': 'tram', - '🚝': 'monorail', - '🚞': 'mountain railway', - '🚋': 'tram car', - '🚌': 'bus', - '🚍': 'oncoming bus', - '🚎': 'trolleybus', - '🚐': 'minibus', - '🚑': 'ambulance', - '🚒': 'fire engine', - '🚓': 'police car', - '🚔': 'oncoming police car', - '🚕': 'taxi', - '🚖': 'oncoming taxi', - '🚗': 'automobile', - '🚘': 'oncoming automobile', - '🚙': 'sport utility vehicle', - '🛻': 'pickup truck', - '🚚': 'delivery truck', - '🚛': 'articulated lorry', - '🚜': 'tractor', - '🏎': 'racing car', - '🏍': 'motorcycle', - '🛵': 'motor scooter', - '🦽': 'manual wheelchair', - '🦼': 'motorized wheelchair', - '🛺': 'auto rickshaw', - '🚲': 'bicycle', - '🛴': 'kick scooter', - '🛹': 'skateboard', - '🛼': 'roller skate', - '🚏': 'bus stop', - '🛣': 'motorway', - '🛤': 'railway track', - '🛢': 'oil drum', - '⛽': 'fuel pump', - '🛞': '⊛ wheel', - '🚨': 'police car light', - '🚥': 'horizontal traffic light', - '🚦': 'vertical traffic light', - '🛑': 'stop sign', - '🚧': 'construction', - '⚓': 'anchor', - '🛟': '⊛ ring buoy', - '⛵': 'sailboat', - '🛶': 'canoe', - '🚤': 'speedboat', - '🛳': 'passenger ship', - '⛴': 'ferry', - '🛥': 'motor boat', - '🚢': 'ship', - '✈': 'airplane', - '🛩': 'small airplane', - '🛫': 'airplane departure', - '🛬': 'airplane arrival', - '🪂': 'parachute', - '💺': 'seat', - '🚁': 'helicopter', - '🚟': 'suspension railway', - '🚠': 'mountain cableway', - '🚡': 'aerial tramway', - '🛰': 'satellite', - '🚀': 'rocket', - '🛸': 'flying saucer', - '🛎': 'bellhop bell', - '🧳': 'luggage', - '⌛': 'hourglass done', - '⏳': 'hourglass not done', - '⌚': 'watch', - '⏰': 'alarm clock', - '⏱': 'stopwatch', - '⏲': 'timer clock', - '🕰': 'mantelpiece clock', - '🕛': 'twelve o’clock', - '🕧': 'twelve-thirty', - '🕐': 'one o’clock', - '🕜': 'one-thirty', - '🕑': 'two o’clock', - '🕝': 'two-thirty', - '🕒': 'three o’clock', - '🕞': 'three-thirty', - '🕓': 'four o’clock', - '🕟': 'four-thirty', - '🕔': 'five o’clock', - '🕠': 'five-thirty', - '🕕': 'six o’clock', - '🕡': 'six-thirty', - '🕖': 'seven o’clock', - '🕢': 'seven-thirty', - '🕗': 'eight o’clock', - '🕣': 'eight-thirty', - '🕘': 'nine o’clock', - '🕤': 'nine-thirty', - '🕙': 'ten o’clock', - '🕥': 'ten-thirty', - '🕚': 'eleven o’clock', - '🕦': 'eleven-thirty', - '🌑': 'new moon', - '🌒': 'waxing crescent moon', - '🌓': 'first quarter moon', - '🌔': 'waxing gibbous moon', - '🌕': 'full moon', - '🌖': 'waning gibbous moon', - '🌗': 'last quarter moon', - '🌘': 'waning crescent moon', - '🌙': 'crescent moon', - '🌚': 'new moon face', - '🌛': 'first quarter moon face', - '🌜': 'last quarter moon face', - '🌡': 'thermometer', - '☀': 'sun', - '🌝': 'full moon face', - '🌞': 'sun with face', - '🪐': 'ringed planet', - '⭐': 'star', - '🌟': 'glowing star', - '🌠': 'shooting star', - '🌌': 'milky way', - '☁': 'cloud', - '⛅': 'sun behind cloud', - '⛈': 'cloud with lightning and rain', - '🌤': 'sun behind small cloud', - '🌥': 'sun behind large cloud', - '🌦': 'sun behind rain cloud', - '🌧': 'cloud with rain', - '🌨': 'cloud with snow', - '🌩': 'cloud with lightning', - '🌪': 'tornado', - '🌫': 'fog', - '🌬': 'wind face', - '🌀': 'cyclone', - '🌈': 'rainbow', - '🌂': 'closed umbrella', - '☂': 'umbrella', - '☔': 'umbrella with rain drops', - '⛱': 'umbrella on ground', - '⚡': 'high voltage', - '❄': 'snowflake', - '☃': 'snowman', - '⛄': 'snowman without snow', - '☄': 'comet', - '🔥': 'fire', - '💧': 'droplet', - '🌊': 'water wave', - '🎃': 'jack-o-lantern', - '🎄': 'Christmas tree', - '🎆': 'fireworks', - '🎇': 'sparkler', - '🧨': 'firecracker', - '✨': 'sparkles', - '🎈': 'balloon', - '🎉': 'party popper', - '🎊': 'confetti ball', - '🎋': 'tanabata tree', - '🎍': 'pine decoration', - '🎎': 'Japanese dolls', - '🎏': 'carp streamer', - '🎐': 'wind chime', - '🎑': 'moon viewing ceremony', - '🧧': 'red envelope', - '🎀': 'ribbon', - '🎁': 'wrapped gift', - '🎗': 'reminder ribbon', - '🎟': 'admission tickets', - '🎫': 'ticket', - '🎖': 'military medal', - '🏆': 'trophy', - '🏅': 'sports medal', - '🥇': '1st place medal', - '🥈': '2nd place medal', - '🥉': '3rd place medal', - '⚽': 'soccer ball', - '⚾': 'baseball', - '🥎': 'softball', - '🏀': 'basketball', - '🏐': 'volleyball', - '🏈': 'american football', - '🏉': 'rugby football', - '🎾': 'tennis', - '🥏': 'flying disc', - '🎳': 'bowling', - '🏏': 'cricket game', - '🏑': 'field hockey', - '🏒': 'ice hockey', - '🥍': 'lacrosse', - '🏓': 'ping pong', - '🏸': 'badminton', - '🥊': 'boxing glove', - '🥋': 'martial arts uniform', - '🥅': 'goal net', - '⛳': 'flag in hole', - '⛸': 'ice skate', - '🎣': 'fishing pole', - '🤿': 'diving mask', - '🎽': 'running shirt', - '🎿': 'skis', - '🛷': 'sled', - '🥌': 'curling stone', - '🎯': 'bullseye', - '🪀': 'yo-yo', - '🪁': 'kite', - '🎱': 'pool 8 ball', - '🔮': 'crystal ball', - '🪄': 'magic wand', - '🧿': 'nazar amulet', - '🪬': '⊛ hamsa', - '🎮': 'video game', - '🕹': 'joystick', - '🎰': 'slot machine', - '🎲': 'game die', - '🧩': 'puzzle piece', - '🧸': 'teddy bear', - '🪅': 'piñata', - '🪩': '⊛ mirror ball', - '🪆': 'nesting dolls', - '♠': 'spade suit', - '♥': 'heart suit', - '♦': 'diamond suit', - '♣': 'club suit', - '♟': 'chess pawn', - '🃏': 'joker', - '🀄': 'mahjong red dragon', - '🎴': 'flower playing cards', - '🎭': 'performing arts', - '🖼': 'framed picture', - '🎨': 'artist palette', - '🧵': 'thread', - '🪡': 'sewing needle', - '🧶': 'yarn', - '🪢': 'knot', - '👓': 'glasses', - '🕶': 'sunglasses', - '🥽': 'goggles', - '🥼': 'lab coat', - '🦺': 'safety vest', - '👔': 'necktie', - '👕': 't-shirt', - '👖': 'jeans', - '🧣': 'scarf', - '🧤': 'gloves', - '🧥': 'coat', - '🧦': 'socks', - '👗': 'dress', - '👘': 'kimono', - '🥻': 'sari', - '🩱': 'one-piece swimsuit', - '🩲': 'briefs', - '🩳': 'shorts', - '👙': 'bikini', - '👚': 'woman’s clothes', - '👛': 'purse', - '👜': 'handbag', - '👝': 'clutch bag', - '🛍': 'shopping bags', - '🎒': 'backpack', - '🩴': 'thong sandal', - '👞': 'man’s shoe', - '👟': 'running shoe', - '🥾': 'hiking boot', - '🥿': 'flat shoe', - '👠': 'high-heeled shoe', - '👡': 'woman’s sandal', - '🩰': 'ballet shoes', - '👢': 'woman’s boot', - '👑': 'crown', - '👒': 'woman’s hat', - '🎩': 'top hat', - '🎓': 'graduation cap', - '🧢': 'billed cap', - '🪖': 'military helmet', - '⛑': 'rescue worker’s helmet', - '📿': 'prayer beads', - '💄': 'lipstick', - '💍': 'ring', - '💎': 'gem stone', - '🔇': 'muted speaker', - '🔈': 'speaker low volume', - '🔉': 'speaker medium volume', - '🔊': 'speaker high volume', - '📢': 'loudspeaker', - '📣': 'megaphone', - '📯': 'postal horn', - '🔔': 'bell', - '🔕': 'bell with slash', - '🎼': 'musical score', - '🎵': 'musical note', - '🎶': 'musical notes', - '🎙': 'studio microphone', - '🎚': 'level slider', - '🎛': 'control knobs', - '🎤': 'microphone', - '🎧': 'headphone', - '📻': 'radio', - '🎷': 'saxophone', - '🪗': 'accordion', - '🎸': 'guitar', - '🎹': 'musical keyboard', - '🎺': 'trumpet', - '🎻': 'violin', - '🪕': 'banjo', - '🥁': 'drum', - '🪘': 'long drum', - '📱': 'mobile phone', - '📲': 'mobile phone with arrow', - '☎': 'telephone', - '📞': 'telephone receiver', - '📟': 'pager', - '📠': 'fax machine', - '🔋': 'battery', - '🪫': '⊛ low battery', - '🔌': 'electric plug', - '💻': 'laptop', - '🖥': 'desktop computer', - '🖨': 'printer', - '⌨': 'keyboard', - '🖱': 'computer mouse', - '🖲': 'trackball', - '💽': 'computer disk', - '💾': 'floppy disk', - '💿': 'optical disk', - '📀': 'dvd', - '🧮': 'abacus', - '🎥': 'movie camera', - '🎞': 'film frames', - '📽': 'film projector', - '🎬': 'clapper board', - '📺': 'television', - '📷': 'camera', - '📸': 'camera with flash', - '📹': 'video camera', - '📼': 'videocassette', - '🔍': 'magnifying glass tilted left', - '🔎': 'magnifying glass tilted right', - '🕯': 'candle', - '💡': 'light bulb', - '🔦': 'flashlight', - '🏮': 'red paper lantern', - '🪔': 'diya lamp', - '📔': 'notebook with decorative cover', - '📕': 'closed book', - '📖': 'open book', - '📗': 'green book', - '📘': 'blue book', - '📙': 'orange book', - '📚': 'books', - '📓': 'notebook', - '📒': 'ledger', - '📃': 'page with curl', - '📜': 'scroll', - '📄': 'page facing up', - '📰': 'newspaper', - '🗞': 'rolled-up newspaper', - '📑': 'bookmark tabs', - '🔖': 'bookmark', - '🏷': 'label', - '💰': 'money bag', - '🪙': 'coin', - '💴': 'yen banknote', - '💵': 'dollar banknote', - '💶': 'euro banknote', - '💷': 'pound banknote', - '💸': 'money with wings', - '💳': 'credit card', - '🧾': 'receipt', - '💹': 'chart increasing with yen', - '✉': 'envelope', - '📧': 'e-mail', - '📨': 'incoming envelope', - '📩': 'envelope with arrow', - '📤': 'outbox tray', - '📥': 'inbox tray', - '📦': 'package', - '📫': 'closed mailbox with raised flag', - '📪': 'closed mailbox with lowered flag', - '📬': 'open mailbox with raised flag', - '📭': 'open mailbox with lowered flag', - '📮': 'postbox', - '🗳': 'ballot box with ballot', - '✏': 'pencil', - '✒': 'black nib', - '🖋': 'fountain pen', - '🖊': 'pen', - '🖌': 'paintbrush', - '🖍': 'crayon', - '📝': 'memo', - '💼': 'briefcase', - '📁': 'file folder', - '📂': 'open file folder', - '🗂': 'card index dividers', - '📅': 'calendar', - '📆': 'tear-off calendar', - '🗒': 'spiral notepad', - '🗓': 'spiral calendar', - '📇': 'card index', - '📈': 'chart increasing', - '📉': 'chart decreasing', - '📊': 'bar chart', - '📋': 'clipboard', - '📌': 'pushpin', - '📍': 'round pushpin', - '📎': 'paperclip', - '🖇': 'linked paperclips', - '📏': 'straight ruler', - '📐': 'triangular ruler', - '✂': 'scissors', - '🗃': 'card file box', - '🗄': 'file cabinet', - '🗑': 'wastebasket', - '🔒': 'locked', - '🔓': 'unlocked', - '🔏': 'locked with pen', - '🔐': 'locked with key', - '🔑': 'key', - '🗝': 'old key', - '🔨': 'hammer', - '🪓': 'axe', - '⛏': 'pick', - '⚒': 'hammer and pick', - '🛠': 'hammer and wrench', - '🗡': 'dagger', - '⚔': 'crossed swords', - '🔫': 'water pistol', - '🪃': 'boomerang', - '🏹': 'bow and arrow', - '🛡': 'shield', - '🪚': 'carpentry saw', - '🔧': 'wrench', - '🪛': 'screwdriver', - '🔩': 'nut and bolt', - '⚙': 'gear', - '🗜': 'clamp', - '⚖': 'balance scale', - '🦯': 'white cane', - '🔗': 'link', - '⛓': 'chains', - '🪝': 'hook', - '🧰': 'toolbox', - '🧲': 'magnet', - '🪜': 'ladder', - '⚗': 'alembic', - '🧪': 'test tube', - '🧫': 'petri dish', - '🧬': 'dna', - '🔬': 'microscope', - '🔭': 'telescope', - '📡': 'satellite antenna', - '💉': 'syringe', - '🩸': 'drop of blood', - '💊': 'pill', - '🩹': 'adhesive bandage', - '🩼': '⊛ crutch', - '🩺': 'stethoscope', - '🩻': '⊛ x-ray', - '🚪': 'door', - '🛗': 'elevator', - '🪞': 'mirror', - '🪟': 'window', - '🛏': 'bed', - '🛋': 'couch and lamp', - '🪑': 'chair', - '🚽': 'toilet', - '🪠': 'plunger', - '🚿': 'shower', - '🛁': 'bathtub', - '🪤': 'mouse trap', - '🪒': 'razor', - '🧴': 'lotion bottle', - '🧷': 'safety pin', - '🧹': 'broom', - '🧺': 'basket', - '🧻': 'roll of paper', - '🪣': 'bucket', - '🧼': 'soap', - '🫧': '⊛ bubbles', - '🪥': 'toothbrush', - '🧽': 'sponge', - '🧯': 'fire extinguisher', - '🛒': 'shopping cart', - '🚬': 'cigarette', - '⚰': 'coffin', - '🪦': 'headstone', - '⚱': 'funeral urn', - '🗿': 'moai', - '🪧': 'placard', - '🪪': '⊛ identification card', - '🏧': 'ATM sign', - '🚮': 'litter in bin sign', - '🚰': 'potable water', - '♿': 'wheelchair symbol', - '🚹': 'men’s room', - '🚺': 'women’s room', - '🚻': 'restroom', - '🚼': 'baby symbol', - '🚾': 'water closet', - '🛂': 'passport control', - '🛃': 'customs', - '🛄': 'baggage claim', - '🛅': 'left luggage', - '⚠': 'warning', - '🚸': 'children crossing', - '⛔': 'no entry', - '🚫': 'prohibited', - '🚳': 'no bicycles', - '🚭': 'no smoking', - '🚯': 'no littering', - '🚱': 'non-potable water', - '🚷': 'no pedestrians', - '📵': 'no mobile phones', - '🔞': 'no one under eighteen', - '☢': 'radioactive', - '☣': 'biohazard', - '⬆': 'up arrow', - '↗': 'up-right arrow', - '➡': 'right arrow', - '↘': 'down-right arrow', - '⬇': 'down arrow', - '↙': 'down-left arrow', - '⬅': 'left arrow', - '↖': 'up-left arrow', - '↕': 'up-down arrow', - '↔': 'left-right arrow', - '↩': 'right arrow curving left', - '↪': 'left arrow curving right', - '⤴': 'right arrow curving up', - '⤵': 'right arrow curving down', - '🔃': 'clockwise vertical arrows', - '🔄': 'counterclockwise arrows button', - '🔙': 'BACK arrow', - '🔚': 'END arrow', - '🔛': 'ON! arrow', - '🔜': 'SOON arrow', - '🔝': 'TOP arrow', - '🛐': 'place of worship', - '⚛': 'atom symbol', - '🕉': 'om', - '✡': 'star of David', - '☸': 'wheel of dharma', - '☯': 'yin yang', - '✝': 'latin cross', - '☦': 'orthodox cross', - '☪': 'star and crescent', - '☮': 'peace symbol', - '🕎': 'menorah', - '🔯': 'dotted six-pointed star', - '♈': 'Aries', - '♉': 'Taurus', - '♊': 'Gemini', - '♋': 'Cancer', - '♌': 'Leo', - '♍': 'Virgo', - '♎': 'Libra', - '♏': 'Scorpio', - '♐': 'Sagittarius', - '♑': 'Capricorn', - '♒': 'Aquarius', - '♓': 'Pisces', - '⛎': 'Ophiuchus', - '🔀': 'shuffle tracks button', - '🔁': 'repeat button', - '🔂': 'repeat single button', - '▶': 'play button', - '⏩': 'fast-forward button', - '⏭': 'next track button', - '⏯': 'play or pause button', - '◀': 'reverse button', - '⏪': 'fast reverse button', - '⏮': 'last track button', - '🔼': 'upwards button', - '⏫': 'fast up button', - '🔽': 'downwards button', - '⏬': 'fast down button', - '⏸': 'pause button', - '⏹': 'stop button', - '⏺': 'record button', - '⏏': 'eject button', - '🎦': 'cinema', - '🔅': 'dim button', - '🔆': 'bright button', - '📶': 'antenna bars', - '📳': 'vibration mode', - '📴': 'mobile phone off', - '♀': 'female sign', - '♂': 'male sign', - '⚧': 'transgender symbol', - '✖': 'multiply', - '➕': 'plus', - '➖': 'minus', - '➗': 'divide', - '🟰': '⊛ heavy equals sign', - '♾': 'infinity', - '‼': 'double exclamation mark', - '⁉': 'exclamation question mark', - '❓': 'red question mark', - '❔': 'white question mark', - '❕': 'white exclamation mark', - '❗': 'red exclamation mark', - '〰': 'wavy dash', - '💱': 'currency exchange', - '💲': 'heavy dollar sign', - '⚕': 'medical symbol', - '♻': 'recycling symbol', - '⚜': 'fleur-de-lis', - '🔱': 'trident emblem', - '📛': 'name badge', - '🔰': 'Japanese symbol for beginner', - '⭕': 'hollow red circle', - '✅': 'check mark button', - '☑': 'check box with check', - '✔': 'check mark', - '❌': 'cross mark', - '❎': 'cross mark button', - '➰': 'curly loop', - '➿': 'double curly loop', - '〽': 'part alternation mark', - '✳': 'eight-spoked asterisk', - '✴': 'eight-pointed star', - '❇': 'sparkle', - '©': 'copyright', - '®': 'registered', - '™': 'trade mark', - '#️⃣': 'keycap: #', - '*️⃣': 'keycap: *', - '0️⃣': 'keycap: 0', - '1️⃣': 'keycap: 1', - '2️⃣': 'keycap: 2', - '3️⃣': 'keycap: 3', - '4️⃣': 'keycap: 4', - '5️⃣': 'keycap: 5', - '6️⃣': 'keycap: 6', - '7️⃣': 'keycap: 7', - '8️⃣': 'keycap: 8', - '9️⃣': 'keycap: 9', - '🔟': 'keycap: 10', - '🔠': 'input latin uppercase', - '🔡': 'input latin lowercase', - '🔢': 'input numbers', - '🔣': 'input symbols', - '🔤': 'input latin letters', - '🅰': 'A button (blood type)', - '🆎': 'AB button (blood type)', - '🅱': 'B button (blood type)', - '🆑': 'CL button', - '🆒': 'COOL button', - '🆓': 'FREE button', - ℹ: 'information', - '🆔': 'ID button', - 'Ⓜ': 'circled M', - '🆕': 'NEW button', - '🆖': 'NG button', - '🅾': 'O button (blood type)', - '🆗': 'OK button', - '🅿': 'P button', - '🆘': 'SOS button', - '🆙': 'UP! button', - '🆚': 'VS button', - '🈁': 'Japanese “here” button', - '🈂': 'Japanese “service charge” button', - '🈷': 'Japanese “monthly amount” button', - '🈶': 'Japanese “not free of charge” button', - '🈯': 'Japanese “reserved” button', - '🉐': 'Japanese “bargain” button', - '🈹': 'Japanese “discount” button', - '🈚': 'Japanese “free of charge” button', - '🈲': 'Japanese “prohibited” button', - '🉑': 'Japanese “acceptable” button', - '🈸': 'Japanese “application” button', - '🈴': 'Japanese “passing grade” button', - '🈳': 'Japanese “vacancy” button', - '㊗': 'Japanese “congratulations” button', - '㊙': 'Japanese “secret” button', - '🈺': 'Japanese “open for business” button', - '🈵': 'Japanese “no vacancy” button', - '🔴': 'red circle', - '🟠': 'orange circle', - '🟡': 'yellow circle', - '🟢': 'green circle', - '🔵': 'blue circle', - '🟣': 'purple circle', - '🟤': 'brown circle', - '⚫': 'black circle', - '⚪': 'white circle', - '🟥': 'red square', - '🟧': 'orange square', - '🟨': 'yellow square', - '🟩': 'green square', - '🟦': 'blue square', - '🟪': 'purple square', - '🟫': 'brown square', - '⬛': 'black large square', - '⬜': 'white large square', - '◼': 'black medium square', - '◻': 'white medium square', - '◾': 'black medium-small square', - '◽': 'white medium-small square', - '▪': 'black small square', - '▫': 'white small square', - '🔶': 'large orange diamond', - '🔷': 'large blue diamond', - '🔸': 'small orange diamond', - '🔹': 'small blue diamond', - '🔺': 'red triangle pointed up', - '🔻': 'red triangle pointed down', - '💠': 'diamond with a dot', - '🔘': 'radio button', - '🔳': 'white square button', - '🔲': 'black square button', - '🏁': 'chequered flag', - '🚩': 'triangular flag', - '🎌': 'crossed flags', - '🏴': 'black flag', - '🏳': 'white flag', - '🏳️‍🌈': 'rainbow flag', - '🏳️‍⚧️': 'transgender flag', - '🏴‍☠️': 'pirate flag', - '🇦🇨': 'flag: Ascension Island', - '🇦🇩': 'flag: Andorra', - '🇦🇪': 'flag: United Arab Emirates', - '🇦🇫': 'flag: Afghanistan', - '🇦🇬': 'flag: Antigua & Barbuda', - '🇦🇮': 'flag: Anguilla', - '🇦🇱': 'flag: Albania', - '🇦🇲': 'flag: Armenia', - '🇦🇴': 'flag: Angola', - '🇦🇶': 'flag: Antarctica', - '🇦🇷': 'flag: Argentina', - '🇦🇸': 'flag: American Samoa', - '🇦🇹': 'flag: Austria', - '🇦🇺': 'flag: Australia', - '🇦🇼': 'flag: Aruba', - '🇦🇽': 'flag: Åland Islands', - '🇦🇿': 'flag: Azerbaijan', - '🇧🇦': 'flag: Bosnia & Herzegovina', - '🇧🇧': 'flag: Barbados', - '🇧🇩': 'flag: Bangladesh', - '🇧🇪': 'flag: Belgium', - '🇧🇫': 'flag: Burkina Faso', - '🇧🇬': 'flag: Bulgaria', - '🇧🇭': 'flag: Bahrain', - '🇧🇮': 'flag: Burundi', - '🇧🇯': 'flag: Benin', - '🇧🇱': 'flag: St. Barthélemy', - '🇧🇲': 'flag: Bermuda', - '🇧🇳': 'flag: Brunei', - '🇧🇴': 'flag: Bolivia', - '🇧🇶': 'flag: Caribbean Netherlands', - '🇧🇷': 'flag: Brazil', - '🇧🇸': 'flag: Bahamas', - '🇧🇹': 'flag: Bhutan', - '🇧🇻': 'flag: Bouvet Island', - '🇧🇼': 'flag: Botswana', - '🇧🇾': 'flag: Belarus', - '🇧🇿': 'flag: Belize', - '🇨🇦': 'flag: Canada', - '🇨🇨': 'flag: Cocos (Keeling) Islands', - '🇨🇩': 'flag: Congo - Kinshasa', - '🇨🇫': 'flag: Central African Republic', - '🇨🇬': 'flag: Congo - Brazzaville', - '🇨🇭': 'flag: Switzerland', - '🇨🇮': 'flag: Côte d’Ivoire', - '🇨🇰': 'flag: Cook Islands', - '🇨🇱': 'flag: Chile', - '🇨🇲': 'flag: Cameroon', - '🇨🇳': 'flag: China', - '🇨🇴': 'flag: Colombia', - '🇨🇵': 'flag: Clipperton Island', - '🇨🇷': 'flag: Costa Rica', - '🇨🇺': 'flag: Cuba', - '🇨🇻': 'flag: Cape Verde', - '🇨🇼': 'flag: Curaçao', - '🇨🇽': 'flag: Christmas Island', - '🇨🇾': 'flag: Cyprus', - '🇨🇿': 'flag: Czechia', - '🇩🇪': 'flag: Germany', - '🇩🇬': 'flag: Diego Garcia', - '🇩🇯': 'flag: Djibouti', - '🇩🇰': 'flag: Denmark', - '🇩🇲': 'flag: Dominica', - '🇩🇴': 'flag: Dominican Republic', - '🇩🇿': 'flag: Algeria', - '🇪🇦': 'flag: Ceuta & Melilla', - '🇪🇨': 'flag: Ecuador', - '🇪🇪': 'flag: Estonia', - '🇪🇬': 'flag: Egypt', - '🇪🇭': 'flag: Western Sahara', - '🇪🇷': 'flag: Eritrea', - '🇪🇸': 'flag: Spain', - '🇪🇹': 'flag: Ethiopia', - '🇪🇺': 'flag: European Union', - '🇫🇮': 'flag: Finland', - '🇫🇯': 'flag: Fiji', - '🇫🇰': 'flag: Falkland Islands', - '🇫🇲': 'flag: Micronesia', - '🇫🇴': 'flag: Faroe Islands', - '🇫🇷': 'flag: France', - '🇬🇦': 'flag: Gabon', - '🇬🇧': 'flag: United Kingdom', - '🇬🇩': 'flag: Grenada', - '🇬🇪': 'flag: Georgia', - '🇬🇫': 'flag: French Guiana', - '🇬🇬': 'flag: Guernsey', - '🇬🇭': 'flag: Ghana', - '🇬🇮': 'flag: Gibraltar', - '🇬🇱': 'flag: Greenland', - '🇬🇲': 'flag: Gambia', - '🇬🇳': 'flag: Guinea', - '🇬🇵': 'flag: Guadeloupe', - '🇬🇶': 'flag: Equatorial Guinea', - '🇬🇷': 'flag: Greece', - '🇬🇸': 'flag: South Georgia & South Sandwich Islands', - '🇬🇹': 'flag: Guatemala', - '🇬🇺': 'flag: Guam', - '🇬🇼': 'flag: Guinea-Bissau', - '🇬🇾': 'flag: Guyana', - '🇭🇰': 'flag: Hong Kong SAR China', - '🇭🇲': 'flag: Heard & McDonald Islands', - '🇭🇳': 'flag: Honduras', - '🇭🇷': 'flag: Croatia', - '🇭🇹': 'flag: Haiti', - '🇭🇺': 'flag: Hungary', - '🇮🇨': 'flag: Canary Islands', - '🇮🇩': 'flag: Indonesia', - '🇮🇪': 'flag: Ireland', - '🇮🇱': 'flag: Israel', - '🇮🇲': 'flag: Isle of Man', - '🇮🇳': 'flag: India', - '🇮🇴': 'flag: British Indian Ocean Territory', - '🇮🇶': 'flag: Iraq', - '🇮🇷': 'flag: Iran', - '🇮🇸': 'flag: Iceland', - '🇮🇹': 'flag: Italy', - '🇯🇪': 'flag: Jersey', - '🇯🇲': 'flag: Jamaica', - '🇯🇴': 'flag: Jordan', - '🇯🇵': 'flag: Japan', - '🇰🇪': 'flag: Kenya', - '🇰🇬': 'flag: Kyrgyzstan', - '🇰🇭': 'flag: Cambodia', - '🇰🇮': 'flag: Kiribati', - '🇰🇲': 'flag: Comoros', - '🇰🇳': 'flag: St. Kitts & Nevis', - '🇰🇵': 'flag: North Korea', - '🇰🇷': 'flag: South Korea', - '🇰🇼': 'flag: Kuwait', - '🇰🇾': 'flag: Cayman Islands', - '🇰🇿': 'flag: Kazakhstan', - '🇱🇦': 'flag: Laos', - '🇱🇧': 'flag: Lebanon', - '🇱🇨': 'flag: St. Lucia', - '🇱🇮': 'flag: Liechtenstein', - '🇱🇰': 'flag: Sri Lanka', - '🇱🇷': 'flag: Liberia', - '🇱🇸': 'flag: Lesotho', - '🇱🇹': 'flag: Lithuania', - '🇱🇺': 'flag: Luxembourg', - '🇱🇻': 'flag: Latvia', - '🇱🇾': 'flag: Libya', - '🇲🇦': 'flag: Morocco', - '🇲🇨': 'flag: Monaco', - '🇲🇩': 'flag: Moldova', - '🇲🇪': 'flag: Montenegro', - '🇲🇫': 'flag: St. Martin', - '🇲🇬': 'flag: Madagascar', - '🇲🇭': 'flag: Marshall Islands', - '🇲🇰': 'flag: North Macedonia', - '🇲🇱': 'flag: Mali', - '🇲🇲': 'flag: Myanmar (Burma)', - '🇲🇳': 'flag: Mongolia', - '🇲🇴': 'flag: Macao SAR China', - '🇲🇵': 'flag: Northern Mariana Islands', - '🇲🇶': 'flag: Martinique', - '🇲🇷': 'flag: Mauritania', - '🇲🇸': 'flag: Montserrat', - '🇲🇹': 'flag: Malta', - '🇲🇺': 'flag: Mauritius', - '🇲🇻': 'flag: Maldives', - '🇲🇼': 'flag: Malawi', - '🇲🇽': 'flag: Mexico', - '🇲🇾': 'flag: Malaysia', - '🇲🇿': 'flag: Mozambique', - '🇳🇦': 'flag: Namibia', - '🇳🇨': 'flag: New Caledonia', - '🇳🇪': 'flag: Niger', - '🇳🇫': 'flag: Norfolk Island', - '🇳🇬': 'flag: Nigeria', - '🇳🇮': 'flag: Nicaragua', - '🇳🇱': 'flag: Netherlands', - '🇳🇴': 'flag: Norway', - '🇳🇵': 'flag: Nepal', - '🇳🇷': 'flag: Nauru', - '🇳🇺': 'flag: Niue', - '🇳🇿': 'flag: New Zealand', - '🇴🇲': 'flag: Oman', - '🇵🇦': 'flag: Panama', - '🇵🇪': 'flag: Peru', - '🇵🇫': 'flag: French Polynesia', - '🇵🇬': 'flag: Papua New Guinea', - '🇵🇭': 'flag: Philippines', - '🇵🇰': 'flag: Pakistan', - '🇵🇱': 'flag: Poland', - '🇵🇲': 'flag: St. Pierre & Miquelon', - '🇵🇳': 'flag: Pitcairn Islands', - '🇵🇷': 'flag: Puerto Rico', - '🇵🇸': 'flag: Palestinian Territories', - '🇵🇹': 'flag: Portugal', - '🇵🇼': 'flag: Palau', - '🇵🇾': 'flag: Paraguay', - '🇶🇦': 'flag: Qatar', - '🇷🇪': 'flag: Réunion', - '🇷🇴': 'flag: Romania', - '🇷🇸': 'flag: Serbia', - '🇷🇺': 'flag: Russia', - '🇷🇼': 'flag: Rwanda', - '🇸🇦': 'flag: Saudi Arabia', - '🇸🇧': 'flag: Solomon Islands', - '🇸🇨': 'flag: Seychelles', - '🇸🇩': 'flag: Sudan', - '🇸🇪': 'flag: Sweden', - '🇸🇬': 'flag: Singapore', - '🇸🇭': 'flag: St. Helena', - '🇸🇮': 'flag: Slovenia', - '🇸🇯': 'flag: Svalbard & Jan Mayen', - '🇸🇰': 'flag: Slovakia', - '🇸🇱': 'flag: Sierra Leone', - '🇸🇲': 'flag: San Marino', - '🇸🇳': 'flag: Senegal', - '🇸🇴': 'flag: Somalia', - '🇸🇷': 'flag: Suriname', - '🇸🇸': 'flag: South Sudan', - '🇸🇹': 'flag: São Tomé & Príncipe', - '🇸🇻': 'flag: El Salvador', - '🇸🇽': 'flag: Sint Maarten', - '🇸🇾': 'flag: Syria', - '🇸🇿': 'flag: Eswatini', - '🇹🇦': 'flag: Tristan da Cunha', - '🇹🇨': 'flag: Turks & Caicos Islands', - '🇹🇩': 'flag: Chad', - '🇹🇫': 'flag: French Southern Territories', - '🇹🇬': 'flag: Togo', - '🇹🇭': 'flag: Thailand', - '🇹🇯': 'flag: Tajikistan', - '🇹🇰': 'flag: Tokelau', - '🇹🇱': 'flag: Timor-Leste', - '🇹🇲': 'flag: Turkmenistan', - '🇹🇳': 'flag: Tunisia', - '🇹🇴': 'flag: Tonga', - '🇹🇷': 'flag: Turkey', - '🇹🇹': 'flag: Trinidad & Tobago', - '🇹🇻': 'flag: Tuvalu', - '🇹🇼': 'flag: Taiwan', - '🇹🇿': 'flag: Tanzania', - '🇺🇦': 'flag: Ukraine', - '🇺🇬': 'flag: Uganda', - '🇺🇲': 'flag: U.S. Outlying Islands', - '🇺🇳': 'flag: United Nations', - '🇺🇸': 'flag: United States', - '🇺🇾': 'flag: Uruguay', - '🇺🇿': 'flag: Uzbekistan', - '🇻🇦': 'flag: Vatican City', - '🇻🇨': 'flag: St. Vincent & Grenadines', - '🇻🇪': 'flag: Venezuela', - '🇻🇬': 'flag: British Virgin Islands', - '🇻🇮': 'flag: U.S. Virgin Islands', - '🇻🇳': 'flag: Vietnam', - '🇻🇺': 'flag: Vanuatu', - '🇼🇫': 'flag: Wallis & Futuna', - '🇼🇸': 'flag: Samoa', - '🇽🇰': 'flag: Kosovo', - '🇾🇪': 'flag: Yemen', - '🇾🇹': 'flag: Mayotte', - '🇿🇦': 'flag: South Africa', - '🇿🇲': 'flag: Zambia', - '🇿🇼': 'flag: Zimbabwe', - '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 'flag: England', - '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 'flag: Scotland', - '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 'flag: Wales', -}; -const isEmoji = (str) => { - const regex = getRegex(); - const emojiMatches = str.match(regex); - const emojiString = emojiMatches ? emojiMatches.join('') : ''; - return !/\d/.test(str) && emojiString === str; -}; -const parseEmoji = (style, str, size = 16) => { - switch (style) { - case 'twemoji': - return twemoji.parse(str, { - folder: 'svg', - ext: '.svg', - attributes: () => ({ - width: `${size}px`, - height: `${size}px`, - }), - }); - case 'native': - return str; - default: - return null; - } -}; -/** - * Gets the shortcode for a given emoji by the name of the emoji. This function replaces - * spaces with underscores and removes colons. - * @param key String to replace with shortcode. - * @returns String with shortcode, or `undefined` if no shortcode exists. - */ -const getShortcode = (key) => { - var _a; - // Removable of colons is necessary for the flag shortcodes. - return (_a = shortNames[key]) === null || _a === void 0 ? void 0 : _a.replace(/\s/g, '_').replace(/:/g, '').toLowerCase(); -}; -var emoji = { - shortNames, - isEmoji, - getShortcode, - parseEmoji, - getRegex, -}; - -// Default obsidian file icon. -const DEFAULT_FILE_ICON = ''; -// Default obsidian folder icon. -const DEFAULT_FOLDER_ICON = ''; -/** - * Tries to read the file synchronously. - * @param file File that will be read. - * @returns A promise that will resolve to a string which is the content of the file. - */ -const readFileSync = (file) => __awaiter(void 0, void 0, void 0, function* () { - const content = yield new Promise((resolve) => { - const reader = new FileReader(); - reader.readAsText(file, 'UTF-8'); - reader.onload = (readerEvent) => resolve(readerEvent.target.result); - }); - return content; -}); -/** - * Gets all the currently opened files by getting the markdown leaves and then checking - * for the `file` property in the view. This also returns the leaf of the file. - * @param plugin Instance of the IconizePlugin. - * @returns An array of {@link FileWithLeaf} objects. - */ -const getAllOpenedFiles = (plugin) => { - return plugin.app.workspace - .getLeavesOfType('markdown') - .reduce((prev, curr) => { - const file = curr.view.file; - if (file) { - prev.push(Object.assign(Object.assign({}, file), { leaf: curr, pinned: false })); - } - return prev; - }, []); -}; -/** - * Gets the file item title element by either accessing `titleEl` or `selfEl`. - * @param fileItem FileItem which will be used to retrieve the title element from. - * @returns HTMLElement which is the title element. - */ -const getFileItemTitleEl = (fileItem) => { - var _a; - return (_a = fileItem.titleEl) !== null && _a !== void 0 ? _a : fileItem.selfEl; -}; -/** - * Gets the file item inner title element by either accessing `titleInnerEl` or `innerEl`. - * @param fileItem FileItem which will be used to retrieve the inner title element from. - * @returns HTMLElement which is the inner title element. - */ -const getFileItemInnerTitleEl = (fileItem) => { - var _a; - return (_a = fileItem.titleInnerEl) !== null && _a !== void 0 ? _a : fileItem.innerEl; -}; -/** - * A utility function which will add the icon to the icon pack and then extract the icon - * to the icon pack. - * @param plugin IconizePlugin that will be used for extracting the icon. - * @param iconNameWithPrefix String that will be used to add the icon to the icon pack. - */ -const saveIconToIconPack = (plugin, iconNameWithPrefix) => { - const iconNextIdentifier = nextIdentifier(iconNameWithPrefix); - const iconName = iconNameWithPrefix.substring(iconNextIdentifier); - const iconPrefix = iconNameWithPrefix.substring(0, iconNextIdentifier); - const possibleIcon = getSvgFromLoadedIcon(iconPrefix, iconName); - if (!possibleIcon) { - throw new Error(`Icon ${iconNameWithPrefix} could not be found.`); - } - const iconPackName = getIconPackNameByPrefix(iconPrefix); - if (iconPackName === LUCIDE_ICON_PACK_NAME && - !plugin.doesUseCustomLucideIconPack()) { - return; - } - const icon = getIconFromIconPack(iconPackName, iconPrefix, iconName); - extractIconToIconPack(plugin, icon, possibleIcon); -}; -/** - * A utility function which will remove the icon from the icon pack by removing the icon - * file from the icon pack directory. - * @param plugin IconizePlugin that will be used for removing the icon. - * @param iconNameWithPrefix String that will be used to remove the icon from the icon pack. - */ -const removeIconFromIconPack = (plugin, iconNameWithPrefix) => { - const identifier = nextIdentifier(iconNameWithPrefix); - const prefix = iconNameWithPrefix.substring(0, identifier); - const iconName = iconNameWithPrefix.substring(identifier); - const iconPackName = getIconPackNameByPrefix(prefix); - const duplicatedIcon = plugin.getDataPathByValue(iconNameWithPrefix); - if (!duplicatedIcon) { - removeIconFromIconPackDirectory(plugin, iconPackName, iconName); - } -}; -/** - * A utility function which will convert a string to a hexadecimal color. - * @param str String that will be converted to a hexadecimal color. - * @returns A string which is the hexadecimal color. - */ -const stringToHex = (str) => { - const validHex = str.replace(/[^0-9a-fA-F]/g, ''); - const hex = validHex.padStart(6, '0').substring(0, 6); - return `#${hex}`; -}; -/** - * A utility function which will check if a string is a hexadecimal color. - * @param str String that will be checked if it is a hexadecimal color. - * @param includeHash Boolean which will include the hash in the check. - * @returns A boolean which is true if the string is a hexadecimal color. - */ -const isHexadecimal = (str, includeHash = false) => { - const regex = new RegExp(`^${includeHash ? '#' : ''}[0-9A-Fa-f]{1,6}$`); - return regex.test(str); -}; - -// This library file does not include any other dependency and is a standalone file that -// only include utility functions for setting styles for nodes or icons. The only -// dependency is the `svg` library. -/** - * Sets the margin for a specific node. - * @param el Node where the margin will be set. - * @param margin Margin that will be applied to the node. - * @returns The modified node with the applied margin. - */ -const setMargin = (el, margin) => { - el.style.margin = `${margin.top}px ${margin.right}px ${margin.bottom}px ${margin.left}px`; - return el; -}; -/** - * Applies all stylings to the specified svg icon string and applies styling to the node - * (container). The styling to the specified element is only modified when it is an emoji - * or extra margin is defined in the settings. - * @param plugin Instance of the IconizePlugin. - * @param iconString SVG that will be used to apply the svg styles to. - * @param el Node for manipulating the style. - * @returns Icon svg string with the manipulate style attributes. - */ -const applyAll = (plugin, iconString, container) => { - iconString = svg.setFontSize(iconString, plugin.getSettings().fontSize); - container.style.color = plugin.getSettings().iconColor; - iconString = svg.colorize(iconString, plugin.getSettings().iconColor); - // Sets the margin of an element. - const margin = plugin.getSettings().extraMargin; - const normalizedMargin = { - top: margin.top !== undefined ? margin.top : 4, - right: margin.right !== undefined ? margin.right : 4, - left: margin.left !== undefined ? margin.left : 4, - bottom: margin.bottom !== undefined ? margin.bottom : 4, - }; - if (plugin.getSettings().extraMargin) { - setMargin(container, normalizedMargin); - } - if (emoji.isEmoji(iconString)) { - container.style.fontSize = `${plugin.getSettings().fontSize}px`; - container.style.lineHeight = `${plugin.getSettings().fontSize}px`; - } - return iconString; -}; -/** - * Refreshes all the styles of all the applied icons where a `.iconize-icon` - * class is defined. This function only modifies the styling of the node. - * @param plugin Instance of the IconizePlugin. - * @param applyStyles Function that is getting called when the icon node is found and - * typically applies all the styles to the icon. - */ -const refreshIconNodes = (plugin, applyStyles = applyAll) => { - const fileExplorers = plugin.app.workspace.getLeavesOfType('file-explorer'); - for (const fileExplorer of fileExplorers) { - Object.keys(plugin.getData()).forEach((path) => { - const fileItem = fileExplorer.view.fileItems[path]; - if (fileItem) { - const titleEl = getFileItemTitleEl(fileItem); - const iconNode = titleEl.querySelector('.iconize-icon'); - if (iconNode) { - const pathValue = plugin.getData()[path]; - const hasIndividualColor = typeof pathValue === 'object' && pathValue.iconColor; - iconNode.innerHTML = applyStyles(plugin, iconNode.innerHTML, iconNode); - if (hasIndividualColor) { - iconNode.style.color = pathValue.iconColor; - const colorizedInnerHtml = svg.colorize(iconNode.innerHTML, pathValue.iconColor); - iconNode.innerHTML = colorizedInnerHtml; - } - } - } - }); - } -}; -var style = { - applyAll, - setMargin, - refreshIconNodes, -}; - -/** - * Removes the `iconize-icon` icon node from the provided HTMLElement. - * @param el HTMLElement from which the icon node will be removed. - */ -const removeIconInNode = (el) => { - const iconNode = el.querySelector('.iconize-icon'); - if (!iconNode) { - return; - } - iconNode.remove(); -}; -/** - * Removes the 'iconize-icon' icon node from the HTMLElement corresponding - * to the specified file path. - * @param path File path for which the icon node will be removed. - */ -const removeIconInPath = (path, options) => { - var _a; - const node = (_a = options === null || options === void 0 ? void 0 : options.container) !== null && _a !== void 0 ? _a : document.querySelector(`[data-path="${path}"]`); - if (!node) { - logger.warn(`Element with data path not found (path: ${path})`); - return; - } - removeIconInNode(node); -}; -/** - * Sets an icon or emoji for an HTMLElement based on the specified icon name and color. - * The function manipulates the specified node inline. - * @param plugin Instance of the IconizePlugin. - * @param iconName Name of the icon or emoji to add. - * @param node HTMLElement to which the icon or emoji will be added. - * @param options Options for adjusting settings while the icon is being set. - */ -const setIconForNode = (plugin, iconName, node, options) => { - var _a, _b; - options !== null && options !== void 0 ? options : (options = {}); - (_a = options.shouldApplyAllStyles) !== null && _a !== void 0 ? _a : (options.shouldApplyAllStyles = true); - // Gets the possible icon based on the icon name. - const iconNextIdentifier = nextIdentifier(iconName); - const possibleIcon = getSvgFromLoadedIcon(iconName.substring(0, iconNextIdentifier), iconName.substring(iconNextIdentifier)); - if (possibleIcon) { - // The icon is possibly not an emoji. - let iconContent = (options === null || options === void 0 ? void 0 : options.shouldApplyAllStyles) - ? style.applyAll(plugin, possibleIcon, node) - : possibleIcon; - if (options === null || options === void 0 ? void 0 : options.color) { - node.style.color = options.color; - iconContent = svg.colorize(iconContent, options.color); - } - node.innerHTML = iconContent; - } - else { - const parsedEmoji = (_b = emoji.parseEmoji(plugin.getSettings().emojiStyle, iconName)) !== null && _b !== void 0 ? _b : iconName; - node.innerHTML = (options === null || options === void 0 ? void 0 : options.shouldApplyAllStyles) - ? style.applyAll(plugin, parsedEmoji, node) - : parsedEmoji; - } - node.setAttribute('title', iconName); -}; -/** - * Creates an icon node for the specified path and inserts it to the DOM. - * @param plugin Instance of the IconizePlugin. - * @param path Path for which the icon node will be created. - * @param iconName Name of the icon or emoji to add. - * @param color Optional color of the icon to add. - */ -const createIconNode = (plugin, path, iconName, options) => { - var _a; - // Get the container from the provided options or try to find the node that has the - // path from the document itself. - const node = (_a = options === null || options === void 0 ? void 0 : options.container) !== null && _a !== void 0 ? _a : document.querySelector(`[data-path="${path}"]`); - if (!node) { - logger.warn(`Element with data path not found (path: ${path})`); - return; - } - // Get the folder or file title node. - let titleNode = node.querySelector('.nav-folder-title-content'); - if (!titleNode) { - titleNode = node.querySelector('.nav-file-title-content'); - if (!titleNode) { - logger.warn(`Element with title node not found (path: ${path})`); - return; - } - } - let iconNode = node.querySelector('.iconize-icon'); - // If the icon is already set in the path, we do not need to create a new div element. - if (iconNode) { - setIconForNode(plugin, iconName, iconNode, { color: options === null || options === void 0 ? void 0 : options.color }); - } - else { - // Creates a new icon node and inserts it to the DOM. - iconNode = document.createElement('div'); - iconNode.setAttribute(config.ICON_ATTRIBUTE_NAME, iconName); - iconNode.classList.add('iconize-icon'); - setIconForNode(plugin, iconName, iconNode, { color: options === null || options === void 0 ? void 0 : options.color }); - node.insertBefore(iconNode, titleNode); - } -}; -/** - * Checks if the element has an icon node by checking if the element has a child with the - * class `iconize-icon`. - * @param element HTMLElement which will be checked if it has an icon. - * @returns Boolean whether the element has an icon node or not. - */ -const doesElementHasIconNode = (element) => { - return element.querySelector('.iconize-icon') !== null; -}; -/** - * Gets the icon name of the element if it has an icon node. - * @param element HTMLElement parent which includes a node with the icon. - * @returns String with the icon name if the element has an icon, `undefined` otherwise. - */ -const getIconFromElement = (element) => { - const iconNode = element.querySelector('.iconize-icon'); - const existingIcon = iconNode === null || iconNode === void 0 ? void 0 : iconNode.getAttribute(config.ICON_ATTRIBUTE_NAME); - return existingIcon; -}; -const getIconNodeFromPath = (path) => { - var _a; - return (_a = document - .querySelector(`[data-path="${path}"]`)) === null || _a === void 0 ? void 0 : _a.querySelector('[data-icon]'); -}; -var dom = { - setIconForNode, - createIconNode, - doesElementHasIconNode, - getIconFromElement, - getIconNodeFromPath, - removeIconInNode, - removeIconInPath, -}; - -class IconsPickerModal extends obsidian.FuzzySuggestModal { - constructor(app, plugin, path) { - super(app); - this.renderIndex = 0; - this.plugin = plugin; - this.path = path; - this.limit = 150; - const pluginRecentltyUsedItems = [ - ...plugin.getSettings().recentlyUsedIcons, - ]; - this.recentlyUsedItems = new Set(pluginRecentltyUsedItems.reverse().filter((iconName) => { - return doesIconExists(iconName) || emoji.isEmoji(iconName); - })); - this.resultContainerEl.classList.add('iconize-modal'); - } - onOpen() { - super.onOpen(); - } - onClose() { - const { contentEl } = this; - contentEl.empty(); - } - getItemText(item) { - return `${item.name} (${item.prefix})`; - } - getItems() { - const iconKeys = []; - if (this.inputEl.value.length === 0) { - this.renderIndex = 0; - this.recentlyUsedItems.forEach((iconName) => { - if (emoji.isEmoji(iconName)) { - iconKeys.push({ - name: emoji.shortNames[iconName], - prefix: 'Emoji', - displayName: iconName, - iconPackName: null, - }); - return; - } - const nextLetter = nextIdentifier(iconName); - const iconPrefix = iconName.substring(0, nextLetter); - const iconPackName = getIconPackNameByPrefix(iconPrefix); - iconKeys.push({ - name: iconName.substring(nextLetter), - prefix: iconPrefix, - displayName: iconName, - iconPackName: iconPackName, - }); - }); - } - for (const icon of getAllLoadedIconNames()) { - iconKeys.push({ - name: icon.name, - prefix: icon.prefix, - displayName: icon.prefix + icon.name, - iconPackName: icon.iconPackName, - }); - } - Object.entries(emoji.shortNames).forEach(([unicode, shortName]) => { - iconKeys.push({ - name: shortName, - prefix: 'Emoji', - displayName: unicode, - iconPackName: null, - }); - iconKeys.push({ - name: unicode, - prefix: 'Emoji', - displayName: unicode, - iconPackName: null, - }); - }); - return iconKeys; - } - onChooseItem(item) { - var _a; - const iconNameWithPrefix = typeof item === 'object' ? item.displayName : item; - dom.createIconNode(this.plugin, this.path, iconNameWithPrefix); - (_a = this.onSelect) === null || _a === void 0 ? void 0 : _a.call(this, iconNameWithPrefix); - this.plugin.addFolderIcon(this.path, item); - // Extracts the icon file to the icon pack. - if (typeof item === 'object' && !emoji.isEmoji(iconNameWithPrefix)) { - saveIconToIconPack(this.plugin, iconNameWithPrefix); - } - this.plugin.notifyPlugins(); - } - renderSuggestion(item, el) { - super.renderSuggestion(item, el); - // if (getAllIconPacks().length === 0) { - // this.resultContainerEl.style.display = 'block'; - // this.resultContainerEl.innerHTML = '
You need to create an icon pack.
'; - // return; - // } - // Render subheadlines for modal. - if (this.recentlyUsedItems.size !== 0 && this.inputEl.value.length === 0) { - if (this.renderIndex === 0) { - const subheadline = this.resultContainerEl.createDiv(); - subheadline.classList.add('iconize-subheadline'); - subheadline.innerText = 'Recently used Icons:'; - this.resultContainerEl.prepend(subheadline); - } - else if (this.renderIndex === this.recentlyUsedItems.size - 1) { - const subheadline = this.resultContainerEl.createDiv(); - subheadline.classList.add('iconize-subheadline'); - subheadline.innerText = 'All Icons:'; - this.resultContainerEl.append(subheadline); - } - } - if (item.item.name !== 'default') { - if (item.item.prefix === 'Emoji') { - const displayName = emoji.parseEmoji(this.plugin.getSettings().emojiStyle, item.item.displayName); - if (!displayName) { - return; - } - el.innerHTML = `
${el.innerHTML}
${displayName}
`; - } - else { - el.innerHTML = `
${el.innerHTML}
${getSvgFromLoadedIcon(item.item.prefix, item.item.name)}
`; - } - } - this.renderIndex++; - } -} - -var IconInTitlePosition; -(function (IconInTitlePosition) { - IconInTitlePosition["Above"] = "above"; - IconInTitlePosition["Inline"] = "inline"; -})(IconInTitlePosition || (IconInTitlePosition = {})); -const DEFAULT_SETTINGS = { - migrated: 2, - iconPacksPath: '.obsidian/icons', - fontSize: 16, - emojiStyle: 'native', - iconColor: null, - recentlyUsedIcons: [], - recentlyUsedIconsSize: 5, - rules: [], - extraMargin: { - top: 0, - right: 4, - bottom: 0, - left: 0, - }, - iconInTabsEnabled: false, - iconInTitleEnabled: false, - iconInTitlePosition: IconInTitlePosition.Above, - iconInFrontmatterEnabled: false, - iconInFrontmatterFieldName: 'icon', - iconColorInFrontmatterFieldName: 'iconColor', - iconsBackgroundCheckEnabled: false, - iconsInNotesEnabled: true, - iconsInLinksEnabled: true, - iconIdentifier: ':', - lucideIconPackType: 'native', - debugMode: false, - useInternalPlugins: false, -}; - -function migrate$5(plugin) { - return __awaiter(this, void 0, void 0, function* () { - // Migration for new syncing mechanism. - if (plugin.getSettings().migrated === 1) { - new obsidian.Notice('Please delete your old icon packs and redownload your icon packs to use the new syncing mechanism.', 20000); - plugin.getSettings().migrated++; - } - }); -} - -function migrate$4(plugin) { - return __awaiter(this, void 0, void 0, function* () { - // Migration for new order functionality of custom rules. - if (plugin.getSettings().migrated === 2) { - // Sorting alphabetically was the default behavior before. - plugin - .getSettings() - .rules.sort((a, b) => a.rule.localeCompare(b.rule)) - .forEach((rule, i) => { - rule.order = i; - }); - plugin.getSettings().migrated++; - } - }); -} - -class IconCache { - constructor() { - this.cache = new Map(); - this.set = (path, result) => { - this.cache.set(path, result); - }; - this.invalidate = (path) => { - this.cache.delete(path); - }; - this.clear = () => { - this.cache.clear(); - }; - this.get = (path) => { - var _a; - return (_a = this.cache.get(path)) !== null && _a !== void 0 ? _a : null; - }; - this.doesRecordExist = (path) => { - return this.get(path) !== null; - }; - if (IconCache.instance) { - throw new Error('Error: Instantiation failed: Use `IconCache.getInstance()` instead of new.'); - } - IconCache.instance = this; - } -} -IconCache.instance = new IconCache(); -IconCache.getInstance = () => { - return IconCache.instance; -}; - -/** - * Checks if the file type is equal to the `for` property of the custom rule. - * @param rule CustomRule that will be checked. - * @param fileType CustomRuleFileType that will be checked. Can be either `file` or `folder`. - * @returns Boolean whether the custom rule `for` matches the file type or not. - */ -const doesMatchFileType = (rule, fileType) => { - return (rule.for === 'everything' || - (rule.for === 'files' && fileType === 'file') || - (rule.for === 'folders' && fileType === 'folder')); -}; -/** - * Determines whether a given file or folder matches a specified custom rule. - * @param plugin Plugin instance. - * @param rule CustomRule to check against the file or folder. - * @param filePath String to check against the custom rule. - * @returns Promise that resolves to `true` if the file matches the rule, `false` otherwise. - */ -const isApplicable = (plugin, rule, filePath) => __awaiter(void 0, void 0, void 0, function* () { - const metadata = yield plugin.app.vault.adapter.stat(filePath); - if (!metadata) { - return false; - } - const fileType = metadata.type; - const doesMatch = doesMatchFileType(rule, fileType); - if (!doesMatch) { - return false; - } - return doesMatchPath(rule, filePath); -}); -/** - * Removes the icon from the custom rule from all the files and folders, if applicable. - * @param plugin IconizePlugin instance. - * @param rule CustomRule where the icons will be removed based on this rule. - */ -const removeFromAllFiles = (plugin, rule) => __awaiter(void 0, void 0, void 0, function* () { - const nodesWithIcon = document.querySelectorAll(`[${config.ICON_ATTRIBUTE_NAME}="${rule.icon}"]`); - for (let i = 0; i < nodesWithIcon.length; i++) { - const node = nodesWithIcon[i]; - // Parent element is the node which contains the data path. - const parent = node.parentElement; - if (!parent) { - continue; - } - const dataPath = parent.getAttribute('data-path'); - if (!dataPath) { - continue; - } - const fileType = (yield plugin.app.vault.adapter.stat(dataPath)).type; - if (doesMatchPath(rule, dataPath) && doesMatchFileType(rule, fileType)) { - dom.removeIconInNode(parent); - IconCache.getInstance().invalidate(dataPath); - } - } -}); -/** - * Gets all the custom rules sorted by their order property in ascending order. - * @param plugin IconizePlugin instance. - * @returns CustomRule array sorted by their order property in ascending order. - */ -const getSortedRules = (plugin) => { - return plugin.getSettings().rules.sort((a, b) => a.order - b.order); -}; -/** - * Tries to add all specific custom rule icons to all registered files and directories. - * It does that by calling the {@link add} function. Custom rules should have the lowest - * priority and will get ignored if an icon already exists in the file or directory. - * @param plugin IconizePlugin instance. - * @param rule CustomRule that will be applied, if applicable, to all files and folders. - */ -const addToAllFiles = (plugin, rule) => __awaiter(void 0, void 0, void 0, function* () { - const fileItems = yield getFileItems(plugin, rule); - for (const fileItem of fileItems) { - yield add$2(plugin, rule, fileItem.file, getFileItemTitleEl(fileItem)); - } -}); -/** - * Tries to add the icon of the custom rule to a file or folder. This function also checks - * if the file type matches the `for` property of the custom rule. - * @param plugin IconizePlugin instance. - * @param rule CustomRule that will be used to check if the rule is applicable to the file - * or directory. - * @param file TAbstractFile that will be used to possibly create the icon for. - * @param container HTMLElement where the icon will be added if the custom rules matches. - * @returns A promise that resolves to `true` if the icon was added, `false` otherwise. - */ -const add$2 = (plugin, rule, file, container) => __awaiter(void 0, void 0, void 0, function* () { - if (container && dom.doesElementHasIconNode(container)) { - return false; - } - // Checks if the file or directory already has an icon. - const hasIcon = plugin.getIconNameFromPath(file.path); - if (hasIcon) { - return false; - } - const doesMatch = yield isApplicable(plugin, rule, file.path); - if (doesMatch) { - IconCache.getInstance().set(file.path, { - iconNameWithPrefix: rule.icon, - inCustomRule: true, - }); - dom.createIconNode(plugin, file.path, rule.icon, { - color: rule.color, - container, - }); - return true; - } - return false; -}); -/** - * Determines whether a given rule exists in a given path. - * @param rule Rule to check for. - * @param path Path to check in. - * @returns True if the rule exists in the path, false otherwise. - */ -const doesMatchPath = (rule, path) => { - const toMatch = rule.useFilePath ? path : path.split('/').pop(); - try { - // Rule is in some sort of regex. - const regex = new RegExp(rule.rule); - if (toMatch.match(regex)) { - return true; - } - } - catch (_a) { - // Rule is not in some sort of regex, check for basic string match. - return toMatch.includes(rule.rule); - } - return false; -}; -/** - * Gets all the file items that can be applied to the specific custom rule. - * @param plugin Instance of IconizePlugin. - * @param rule Custom rule that will be checked for. - * @returns A promise that resolves to an array of file items that match the custom rule. - */ -const getFileItems = (plugin, rule) => __awaiter(void 0, void 0, void 0, function* () { - const result = []; - for (const fileExplorer of plugin.getRegisteredFileExplorers()) { - const files = Object.values(fileExplorer.fileItems || {}); - for (const fileItem of files) { - if (yield isApplicable(plugin, rule, fileItem.file.path)) { - result.push(fileItem); - } - } - } - return result; -}); -var customRule = { - getFileItems, - doesMatchPath, - doesMatchFileType, - getSortedRules, - removeFromAllFiles, - add: add$2, - addToAllFiles, - isApplicable, -}; - -function migrate$3(plugin) { - return __awaiter(this, void 0, void 0, function* () { - // Migration for inheritance to custom rule. - if (plugin.getSettings().migrated === 3) { - let hasRemovedInheritance = false; - for (const [key, value] of Object.entries(plugin.getData())) { - if (key === 'settings' || typeof value !== 'object') { - continue; - } - const folderData = value; - const inheritanceIcon = folderData.inheritanceIcon; - if (!inheritanceIcon) { - continue; - } - const folderIconName = folderData.iconName; - // Clean up old data. - if (folderData.iconColor && folderIconName) { - delete folderData.inheritanceIcon; - } - else if (folderIconName) { - delete plugin.getData()[key]; - plugin.getData()[key] = folderIconName; - } - else if (!folderIconName) { - delete plugin.getData()[key]; - } - const folderPath = key + '\\/[\\w\\d\\s]+'; - const newRule = { - icon: inheritanceIcon, - rule: `${folderPath}\\.(?:\\w+\\.)*\\w+`, - for: 'files', - order: 0, - useFilePath: true, - }; - // Reorder existing custom rules so that the new inheritance custom rule - // is at the top. - plugin.getSettings().rules.map((rule) => { - rule.order++; - }); - plugin.getSettings().rules.unshift(newRule); - // Apply the custom rule. - yield customRule.addToAllFiles(plugin, newRule); - hasRemovedInheritance = true; - } - if (hasRemovedInheritance) { - new obsidian.Notice(`[${config.PLUGIN_NAME}] Inheritance has been removed and replaced with custom rules.`); - } - plugin.getSettings().migrated++; - } - }); -} - -function migrate$2(plugin) { - return __awaiter(this, void 0, void 0, function* () { - if (plugin.getSettings().migrated === 4) { - if (plugin.getSettings().emojiStyle === 'none') { - plugin.getSettings().emojiStyle = 'native'; - } - plugin.getSettings().migrated++; - } - }); -} - -function migrate$1(plugin) { - return __awaiter(this, void 0, void 0, function* () { - if (plugin.getSettings().migrated === 5) { - yield deleteIconPack(plugin, LUCIDE_ICON_PACK_NAME); - plugin.getSettings().migrated++; - } - }); -} - -const migrate = (plugin) => __awaiter(void 0, void 0, void 0, function* () { - // eslint-disable-next-line - // @ts-ignore - Required because an older version of the plugin saved the `migrated` - // property as a boolean instead of a number. - if (plugin.getSettings().migrated === true) { - plugin.getSettings().migrated = 1; - } - yield migrate$5(plugin); - yield migrate$4(plugin); - yield migrate$3(plugin); - yield migrate$2(plugin); - yield migrate$1(plugin); - yield plugin.saveIconFolderData(); -}); - -class IconFolderSetting { - constructor(plugin, containerEl) { - this.plugin = plugin; - this.containerEl = containerEl; - } -} - -/** - * Gets the tab leaves of a specific file path by looping through all opened files and - * checking if the file path matches. - * @param plugin IconizePlugin instance. - * @param path String of the file path to get the tab leaf of. - * @returns TabHeaderLeaf array that includes all tab leaves of the file path. - */ -const getTabLeavesOfFilePath = (plugin, path) => { - const openedFiles = getAllOpenedFiles(plugin); - const openedFile = openedFiles.filter((openedFile) => openedFile.path === path); - const leaves = openedFile.map((openedFile) => openedFile.leaf); - return leaves; -}; -/** - * Adds an icon to the tab and its container. This function respects the - * custom rules and individually icon set. - * @param plugin IconizePlugin instance. - * @param filePath String file path to add the icon to. - * @param iconContainer HTMLElement where the icon will be added to. - * @param options AddOptions for the add function which can optionally be used. - */ -const add$1 = (plugin, filePath, iconContainer, options) => __awaiter(void 0, void 0, void 0, function* () { - var _a; - const iconColor = (_a = options === null || options === void 0 ? void 0 : options.iconColor) !== null && _a !== void 0 ? _a : plugin.getSettings().iconColor; - const data = Object.entries(plugin.getData()); - // Removes the `display: none` from the obsidian styling. - iconContainer.style.display = 'flex'; - // Only add the icon name manually when it is defined in the options. - if (options === null || options === void 0 ? void 0 : options.iconName) { - dom.setIconForNode(plugin, options.iconName, iconContainer, { - color: iconColor, - }); - // TODO: Refactor to include option to `insertIconToNode` function. - iconContainer.style.margin = null; - return; - } - // Add icons to tabs if a custom rule is applicable. - for (const rule of customRule.getSortedRules(plugin)) { - const isApplicable = yield customRule.isApplicable(plugin, rule, filePath); - if (isApplicable) { - dom.setIconForNode(plugin, rule.icon, iconContainer, { - color: rule.color, - }); - // TODO: Refactor to include option to `insertIconToNode` function. - iconContainer.style.margin = null; - break; - } - } - // Add icons to tabs if there is an icon set. - const iconData = data.find(([dataPath]) => dataPath === filePath); - if (!iconData) { - return; - } - const value = iconData[1]; - if (typeof value !== 'string' && typeof value !== 'object') { - return; - } - let iconName; - if (typeof value === 'object') { - const v = value; - if (v.iconName === null) { - return; - } - iconName = v.iconName; - } - else { - iconName = value; - } - dom.setIconForNode(plugin, iconName, iconContainer, { - color: iconColor, - shouldApplyAllStyles: true, - }); - // TODO: Refactor to include option to `insertIconToNode` function. - iconContainer.style.margin = null; -}); -/** - * Updates the icon in the tab and container by setting calling the `setIconForNode` - * function and removing the margin from the icon container. - * @param plugin IconizePlugin instance. - * @param iconName String of the icon name to update to. - * @param iconContainer HTMLElement where the icon is located and will be updated. - */ -const update = (plugin, iconName, iconContainer) => { - dom.setIconForNode(plugin, iconName, iconContainer); - // TODO: Refactor to include option to `insertIconToNode` function. - iconContainer.style.margin = null; -}; -/** - * Removes the icon from the tab and container by setting the `display` style property - * to `none`. Optionally, the icon can be replaced with the default obsidian icon. - * @param iconContainer HTMLElement where the icon is located and will be removed from. - * @param options RemoveOptions for the remove function which can optionally be used. - */ -const remove$1 = (iconContainer, options) => { - if (!(options === null || options === void 0 ? void 0 : options.replaceWithDefaultIcon)) { - // Removes the display of the icon container to remove the icons from the tabs. - iconContainer.style.display = 'none'; - } - else { - iconContainer.innerHTML = DEFAULT_FILE_ICON; - } -}; -var iconTabs = { - add: add$1, - update, - remove: remove$1, - getTabLeavesOfFilePath, -}; - -const checkMissingIcons = (plugin, data) => __awaiter(void 0, void 0, void 0, function* () { - const missingIcons = new Set(); - const allIcons = new Map(); - const getMissingIcon = (iconNameWithPrefix) => __awaiter(void 0, void 0, void 0, function* () { - const iconNextIdentifier = nextIdentifier(iconNameWithPrefix); - const iconName = iconNameWithPrefix.substring(iconNextIdentifier); - const iconPrefix = iconNameWithPrefix.substring(0, iconNextIdentifier); - const iconPackName = getIconPackNameByPrefix(iconPrefix); - if (iconPackName === LUCIDE_ICON_PACK_NAME && - !plugin.doesUseCustomLucideIconPack()) { - return; - } - const icon = getIconFromIconPack(iconPackName, iconPrefix, iconName); - if (!icon) { - logger.error(`Icon file with name ${iconNameWithPrefix} could not be found`); - return null; - } - const doesIconFileExists = yield plugin.app.vault.adapter.exists(`${getPath()}/${iconPackName}/${iconName}.svg`); - if (!doesIconFileExists) { - const possibleIcon = getSvgFromLoadedIcon(iconPrefix, iconName); - if (!possibleIcon) { - logger.error(`Icon SVG with name ${iconNameWithPrefix} could not be found`); - return null; - } - yield extractIconToIconPack(plugin, icon, possibleIcon); - return icon; - } - return null; - }); - for (const rule of plugin.getSettings().rules) { - if (!emoji.isEmoji(rule.icon)) { - allIcons.set(rule.icon, true); - const icon = yield getMissingIcon(rule.icon); - if (icon) { - missingIcons.add(icon); - } - } - } - for (const [_, value] of data) { - // Check for missing icon names. - let iconNameWithPrefix = value; - if (typeof value === 'object') { - iconNameWithPrefix = value.iconName; - } - if (iconNameWithPrefix && !emoji.isEmoji(iconNameWithPrefix)) { - allIcons.set(iconNameWithPrefix, true); - const icon = yield getMissingIcon(iconNameWithPrefix); - if (icon) { - missingIcons.add(icon); - } - } - } - // Show notice that background check is running. - if (missingIcons.size !== 0) { - new obsidian.Notice(`[${config.PLUGIN_NAME}] Background Check: found missing icons. Adding missing icons...`, 10000); - } - // Iterates over all the missing icons with its path and adds the icon to the node. - for (const icon of missingIcons) { - const normalizedName = getNormalizedName(icon.prefix + icon.name); - const nodesWithIcon = document.querySelectorAll(`[${config.ICON_ATTRIBUTE_NAME}="${normalizedName}"]`); - nodesWithIcon.forEach((node) => { - dom.setIconForNode(plugin, normalizedName, node); - }); - } - // Show notice that background check was finished. - if (missingIcons.size !== 0) { - new obsidian.Notice(`[${config.PLUGIN_NAME}] Background Check: added missing icons`, 10000); - } - // Remove all icon files that can not be found in the data. - for (const iconPack of getAllIconPacks()) { - // Checks if the icon pack exists. - const doesIconPackExist = yield plugin.app.vault.adapter.exists(`${getPath()}/${iconPack.name}`); - if (!doesIconPackExist) { - continue; - } - const iconFiles = yield plugin.app.vault.adapter.list(`${getPath()}/${iconPack.name}`); - for (const iconFilePath of iconFiles.files) { - const iconNameWithExtension = iconFilePath.split('/').pop(); - // Removes the file extension. - const iconName = iconNameWithExtension === null || iconNameWithExtension === void 0 ? void 0 : iconNameWithExtension.substring(0, iconNameWithExtension.length - 4); - const iconNameWithPrefix = iconPack.prefix + iconName; - const doesIconExist = allIcons.get(iconNameWithPrefix); - if (!doesIconExist) { - const path = `${getPath()}/${iconPack.name}/${iconName}.svg`; - const doesPathExist = yield plugin.app.vault.adapter.exists(path); - if (doesPathExist) { - logger.info(`Removing icon with path '${path}' because it is not used anymore`); - // Removes the icon file. - yield plugin.app.vault.adapter.remove(`${getPath()}/${iconPack.name}/${iconName}.svg`); - } - } - } - } -}); -/** - * This function adds all the possible icons to the corresponding nodes. It - * adds the icons, that are defined in the data as a basic string to the nodes - * and the custom rule icons. - * @param plugin Instance of IconizePlugin. - * @param data Data that will be used to add all the icons to the nodes. - * @param registeredFileExplorers A WeakSet of file explorers that are being used as a - * cache for already handled file explorers. - * @param callback Callback is being called whenever the icons are added to one file - * explorer. - */ -const addAll = (plugin, data, registeredFileExplorers, callback) => { - const fileExplorers = plugin.app.workspace.getLeavesOfType('file-explorer'); - for (const fileExplorer of fileExplorers) { - if (registeredFileExplorers.has(fileExplorer.view)) { - continue; - } - registeredFileExplorers.add(fileExplorer.view); - const setIcons = () => { - var _a, _b; - // Adds icons to already open file tabs. - if (plugin.getSettings().iconInTabsEnabled) { - for (const leaf of plugin.app.workspace.getLeavesOfType('markdown')) { - const filePath = (_b = (_a = leaf.view.file) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : leaf.view.getState().file; - if (typeof filePath === 'string') { - const tabHeaderLeaf = leaf; - const iconColor = plugin.getIconColor(filePath); - iconTabs.add(plugin, filePath, tabHeaderLeaf.tabHeaderInnerIconEl, { - iconColor, - }); - } - } - } - for (const [dataPath, value] of data) { - const fileItem = fileExplorer.view.fileItems[dataPath]; - if (fileItem) { - const titleEl = getFileItemTitleEl(fileItem); - const titleInnerEl = getFileItemInnerTitleEl(fileItem); - // Need to check this because refreshing the plugin will duplicate all the icons. - if (titleEl.children.length === 2 || titleEl.children.length === 1) { - const iconName = typeof value === 'string' ? value : value.iconName; - const iconColor = typeof value === 'string' ? undefined : value.iconColor; - if (iconName) { - // Removes a possible existing icon. - const existingIcon = titleEl.querySelector('.iconize-icon'); - if (existingIcon) { - existingIcon.remove(); - } - // Creates the new node with the icon inside. - const iconNode = titleEl.createDiv(); - iconNode.setAttribute(config.ICON_ATTRIBUTE_NAME, iconName); - iconNode.classList.add('iconize-icon'); - IconCache.getInstance().set(dataPath, { - iconNameWithPrefix: iconName, - }); - dom.setIconForNode(plugin, iconName, iconNode, { - color: iconColor, - }); - titleEl.insertBefore(iconNode, titleInnerEl); - } - } - } - } - // Callback function to register other events to this file explorer. - callback === null || callback === void 0 ? void 0 : callback(); - }; - if (obsidian.requireApiVersion('1.7.2')) { - // TODO: Remove loading deferred view to improve performance. - fileExplorer.loadIfDeferred().then(setIcons); - } - else { - setIcons(); - } - } - // Handles the custom rules. - for (const rule of customRule.getSortedRules(plugin)) { - customRule.addToAllFiles(plugin, rule); - } -}; -/** - * Gets the icon of a given path. This function returns the first occurrence of an icon. - * @param plugin Instance of the IconizePlugin. - * @param path Path to get the icon of. - * @returns The icon of the path if it exists, undefined otherwise. - */ -const getByPath = (plugin, path) => { - if (path === 'settings' || path === 'migrated') { - return undefined; - } - const value = plugin.getData()[path]; - if (typeof value === 'string') { - // If the value is a plain icon name, return it. - return value; - } - else if (typeof value === 'object') { - const v = value; - if (v.iconName !== null) { - return v.iconName; - } - } - // Tries to get the custom rule for the path and returns its icon if it exists. - const rule = customRule.getSortedRules(plugin).find((rule) => { - return customRule.doesMatchPath(rule, path); - }); - if (rule) { - return rule.icon; - } - return undefined; -}; -/** - * Gets all the icons with their paths as an object. - * @param plugin Instance of the IconizePlugin. - * @returns An object that consists of the path and the icon name for the data - * or custom rule. - */ -const getAllWithPath = (plugin) => { - const result = []; - Object.keys(plugin.getData()).forEach((path) => { - if (path === 'settings' || path === 'migrated') { - return; - } - const icon = getByPath(plugin, path); - if (icon && !emoji.isEmoji(icon)) { - result.push({ path, icon }); - } - }); - // Add all icons for the custom rules with the rule as the path. - for (const rule of plugin.getSettings().rules) { - if (!emoji.isEmoji(rule.icon)) { - result.push({ path: rule.rule, icon: rule.icon }); - } - } - return result; -}; -/** - * Returns the {@link Icon} for the given icon name. It is important, that the icon name - * contains the icon pack prefix. - * @param iconNameWithPrefix String that contains the icon pack prefix combined with the - * icon name. - * @returns Icon if it exists, `null` otherwise. - */ -const getIconByName = (iconNameWithPrefix) => { - const iconNextIdentifier = nextIdentifier(iconNameWithPrefix); - const iconName = iconNameWithPrefix.substring(iconNextIdentifier); - const iconPrefix = iconNameWithPrefix.substring(0, iconNextIdentifier); - const iconPackName = getIconPackNameByPrefix(iconPrefix); - const icon = getIconFromIconPack(iconPackName, iconPrefix, iconName); - if (!icon) { - return null; - } - return icon; -}; -/** - * Returns the {@link Icon} for the given path. - * @param plugin IconizePlugin instance. - * @param path String which is the path to get the icon of. - * @returns Icon or Emoji as string if it exists, `null` otherwise. - */ -const getIconByPath = (plugin, path) => { - const iconNameWithPrefix = getByPath(plugin, path); - if (!iconNameWithPrefix) { - return null; - } - if (emoji.isEmoji(iconNameWithPrefix)) { - return iconNameWithPrefix; - } - return getIconByName(iconNameWithPrefix); -}; -var icon = { - addAll, - getByPath, - getAllWithPath, - getIconByPath, - getIconByName, - checkMissingIcons, -}; - -class CustomIconPackSetting extends IconFolderSetting { - constructor(plugin, containerEl, refreshDisplay) { - super(plugin, containerEl); - this.refreshDisplay = refreshDisplay; - this.dragOverElement = document.createElement('div'); - this.dragOverElement.addClass('iconize-dragover-el'); - this.dragOverElement.style.display = 'hidden'; - this.dragOverElement.innerHTML = '

Drop to add icon.

'; - } - normalizeIconPackName(value) { - return value.toLowerCase().replace(/\s/g, '-'); - } - preventDefaults(event) { - event.preventDefault(); - event.stopPropagation(); - } - highlight(el) { - clearTimeout(this.closeTimer); - if (!this.dragTargetElement) { - el.appendChild(this.dragOverElement); - el.classList.add('iconize-dragover'); - this.dragTargetElement = el; - } - } - unhighlight(target, el) { - if (this.dragTargetElement && this.dragTargetElement !== target) { - this.dragTargetElement.removeChild(this.dragOverElement); - this.dragTargetElement.classList.remove('iconize-dragover'); - this.dragTargetElement = undefined; - } - clearTimeout(this.closeTimer); - this.closeTimer = setTimeout(() => { - if (this.dragTargetElement) { - el.removeChild(this.dragOverElement); - el.classList.remove('iconize-dragover'); - this.dragTargetElement = undefined; - } - }, 100); - } - display() { - new obsidian.Setting(this.containerEl) - .setName('Add custom icon pack') - .setDesc('Add a custom icon pack.') - .addText((text) => { - text.setPlaceholder('Your icon pack name'); - this.textComponent = text; - }) - .addButton((btn) => { - btn.setButtonText('Add icon pack'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - const name = this.textComponent.getValue(); - if (name.length === 0) { - return; - } - const normalizedName = this.normalizeIconPackName(this.textComponent.getValue()); - if (yield doesIconPackExist(this.plugin, normalizedName)) { - new obsidian.Notice('Icon pack already exists.'); - return; - } - yield createCustomIconPackDirectory(this.plugin, normalizedName); - this.textComponent.setValue(''); - this.refreshDisplay(); - new obsidian.Notice('Icon pack successfully created.'); - })); - }); - // Sorts lucide icon pack always to the top. - const iconPacks = [...getAllIconPacks()].sort((a, b) => { - if (a.name === LUCIDE_ICON_PACK_NAME) - return -1; - if (b.name === LUCIDE_ICON_PACK_NAME) - return 1; - return a.name.localeCompare(b.name); - }); - iconPacks.forEach((iconPack) => { - const isLucideIconPack = iconPack.name === LUCIDE_ICON_PACK_NAME; - const additionalLucideDescription = '(Native Pack has fewer icons but 100% Obsidian Sync support)'; - const iconPackSetting = new obsidian.Setting(this.containerEl) - .setName(`${iconPack.name} (${iconPack.prefix})`) - .setDesc(`Total icons: ${iconPack.icons.length}${isLucideIconPack ? ` ${additionalLucideDescription}` : ''}`); - // iconPackSetting.addButton((btn) => { - // btn.setIcon('broken-link'); - // btn.setTooltip('Try to fix icon pack'); - // btn.onClick(async () => { - // new Notice('Try to fix icon pack...'); - // getIconPack(iconPack.name).icons = []; - // const icons = await getFilesInDirectory(this.plugin, `${getPath()}/${iconPack.name}`); - // for (let i = 0; i < icons.length; i++) { - // const filePath = icons[i]; - // const fileName = filePath.split('/').pop(); - // const file = await this.plugin.app.vault.adapter.read(filePath); - // const iconContent = file - // .replace(/stroke="#fff"/g, 'stroke="currentColor"') - // .replace(/fill="#fff"/g, 'fill="currentColor"'); - // await this.plugin.app.vault.adapter.write(filePath, iconContent); - // await normalizeFileName(this.plugin, filePath); - // addIconToIconPack(iconPack.name, fileName, iconContent); - // } - // new Notice('...tried to fix icon pack'); - // // Refreshes the DOM. - // Object.entries(this.plugin.getData()).forEach(async ([k, v]) => { - // const doesPathExist = await this.plugin.app.vault.adapter.exists(k, true); - // if (doesPathExist && typeof v === 'string') { - // // dom.removeIconInPath(k); - // dom.createIconNode(this.plugin, k, v); - // } - // }); - // }); - // }); - if (isLucideIconPack) { - iconPackSetting.addDropdown((dropdown) => { - dropdown.addOptions({ - native: 'Native', - custom: 'Custom', - none: 'None', - }); - dropdown.setValue(this.plugin.getSettings().lucideIconPackType); - dropdown.onChange((value) => __awaiter(this, void 0, void 0, function* () { - dropdown.setDisabled(true); - new obsidian.Notice('Changing icon packs...'); - this.plugin.getSettings().lucideIconPackType = value; - yield this.plugin.saveIconFolderData(); - if (value === 'native' || value === 'none') { - yield removeCustomLucideIconPack(this.plugin); - addLucideIconsPack(this.plugin); - } - else { - yield addCustomLucideIconPack(this.plugin); - yield icon.checkMissingIcons(this.plugin, Object.entries(this.plugin.getData())); - } - dropdown.setDisabled(false); - new obsidian.Notice('Done. This change requires a restart of Obsidian'); - })); - }); - return; - } - iconPackSetting.addButton((btn) => { - btn.setIcon('plus'); - btn.setTooltip('Add an icon'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - const fileSelector = document.createElement('input'); - fileSelector.setAttribute('type', 'file'); - fileSelector.setAttribute('multiple', 'multiple'); - fileSelector.setAttribute('accept', '.svg'); - fileSelector.click(); - fileSelector.onchange = (e) => __awaiter(this, void 0, void 0, function* () { - const target = e.target; - for (let i = 0; i < target.files.length; i++) { - const file = target.files[i]; - const content = yield readFileSync(file); - yield createFile(this.plugin, iconPack.name, file.name, content); - addIconToIconPack(iconPack.name, file.name, content); - iconPackSetting.setDesc(`Total icons: ${iconPack.icons.length} (added: ${file.name})`); - } - new obsidian.Notice('Icons successfully added.'); - }); - })); - }); - iconPackSetting.addButton((btn) => { - btn.setIcon('trash'); - btn.setTooltip('Remove the icon pack'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - yield deleteIconPack(this.plugin, iconPack.name); - this.refreshDisplay(); - new obsidian.Notice('Icon pack successfully deleted.'); - })); - }); - ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((event) => { - iconPackSetting.settingEl.addEventListener(event, this.preventDefaults, false); - }); - ['dragenter', 'dragover'].forEach((event) => { - iconPackSetting.settingEl.addEventListener(event, () => this.highlight(iconPackSetting.settingEl), false); - }); - ['dragleave', 'drop'].forEach((event) => { - iconPackSetting.settingEl.addEventListener(event, (event) => this.unhighlight(event.currentTarget, iconPackSetting.settingEl), false); - }); - iconPackSetting.settingEl.addEventListener('drop', (event) => __awaiter(this, void 0, void 0, function* () { - const files = event.dataTransfer.files; - let successful = false; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (file.type !== 'image/svg+xml') { - new obsidian.Notice(`File ${file.name} is not a SVG file.`); - continue; - } - successful = true; - const content = yield readFileSync(file); - yield createFile(this.plugin, iconPack.name, file.name, content); - addIconToIconPack(iconPack.name, file.name, content); - iconPackSetting.setDesc(`Total icons: ${iconPack.icons.length} (added: ${file.name})`); - } - if (successful) { - new obsidian.Notice('Icons successfully added.'); - } - }), false); - }); - } -} - -class CustomIconRuleSetting extends IconFolderSetting { - constructor(plugin, containerEl, app, refreshDisplay) { - super(plugin, containerEl); - this.app = app; - this.refreshDisplay = refreshDisplay; - } - /** - * Updates all the open files based on the custom rule that was specified. - * @param rule Rule that will be used to update all the icons for all opened files. - * @param remove Whether to remove the icons that are applicable to the rule or not. - */ - updateIconTabs(rule_1, remove_1) { - return __awaiter(this, arguments, void 0, function* (rule, remove, cachedPaths = []) { - if (this.plugin.getSettings().iconInTabsEnabled) { - for (const openedFile of getAllOpenedFiles(this.plugin)) { - if (cachedPaths.includes(openedFile.path)) { - continue; - } - const applicable = yield customRule.isApplicable(this.plugin, rule, openedFile.path); - if (!applicable) { - continue; - } - const leaf = openedFile.leaf; - if (remove) { - iconTabs.remove(leaf.tabHeaderInnerIconEl, { - replaceWithDefaultIcon: true, - }); - } - else { - iconTabs.add(this.plugin, openedFile.path, leaf.tabHeaderInnerIconEl, { - iconName: rule.icon, - iconColor: rule.color, - }); - } - } - } - }); - } - createDescriptionEl(container, text) { - const description = container.createEl('p', { - text, - cls: 'setting-item-description', - }); - description.style.marginBottom = 'var(--size-2-2)'; - } - display() { - new obsidian.Setting(this.containerEl) - .setName('Add icon rule') - .setDesc('Will add the icon based on the defined rule (as a plain string or in regex format).') - .addText((text) => { - text.onChange((value) => { - this.chooseIconBtn.setDisabled(value.length === 0); - this.chooseIconBtn.buttonEl.style.cursor = - value.length === 0 ? 'not-allowed' : 'default'; - this.chooseIconBtn.buttonEl.style.opacity = - value.length === 0 ? '50%' : '100%'; - }); - text.setPlaceholder('regex or simple string'); - this.textComponent = text; - }) - .addButton((btn) => { - btn.setDisabled(true); - btn.setButtonText('Choose icon'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - if (this.textComponent.getValue().length === 0) { - return; - } - const modal = new IconsPickerModal(this.app, this.plugin, ''); - modal.onChooseItem = (item) => __awaiter(this, void 0, void 0, function* () { - const icon = getNormalizedName(typeof item === 'object' ? item.displayName : item); - const rule = { - rule: this.textComponent.getValue(), - icon, - for: 'everything', - order: this.plugin.getSettings().rules.length, - }; - this.plugin.getSettings().rules = [ - ...this.plugin.getSettings().rules, - rule, - ]; - yield this.plugin.saveIconFolderData(); - this.refreshDisplay(); - new obsidian.Notice('Icon rule added.'); - this.textComponent.setValue(''); - saveIconToIconPack(this.plugin, rule.icon); - yield customRule.addToAllFiles(this.plugin, rule); - this.updateIconTabs(rule, false); - }); - modal.open(); - })); - this.chooseIconBtn = btn; - }); - this.plugin.getSettings().rules.forEach((rule) => { - // Keeping track of the old rule so that we can get a reference to it for old values. - const oldRule = Object.assign({}, rule); - const settingRuleEl = new obsidian.Setting(this.containerEl) - .setName(rule.rule) - .setDesc(`Icon: ${rule.icon}`); - const currentOrder = rule.order; - /** - * Re-orders the custom rule based on the value that is passed in. - * @param valueForReorder Number that will be used to determine whether to swap the - * custom rule with the next rule or the previous rule. - */ - const orderCustomRules = (valueForReorder) => __awaiter(this, void 0, void 0, function* () { - const otherRule = this.plugin.getSettings().rules[currentOrder + valueForReorder]; - // Swap the current rule with the next rule. - otherRule.order = otherRule.order - valueForReorder; - rule.order = currentOrder + valueForReorder; - // Refreshes the DOM. - yield customRule.removeFromAllFiles(this.plugin, oldRule); - yield this.plugin.saveIconFolderData(); - const addedPaths = []; - for (const fileExplorer of this.plugin.getRegisteredFileExplorers()) { - const files = Object.values(fileExplorer.fileItems || {}); - for (const rule of customRule.getSortedRules(this.plugin)) { - // Removes the icon tabs from all opened files. - this.updateIconTabs(rule, true, addedPaths); - // Adds the icon tabs to all opened files. - this.updateIconTabs(rule, false, addedPaths); - for (const fileItem of files) { - if (addedPaths.includes(fileItem.file.path)) { - continue; - } - const added = yield customRule.add(this.plugin, rule, fileItem.file, getFileItemTitleEl(fileItem)); - if (added) { - addedPaths.push(fileItem.file.path); - } - } - } - } - this.refreshDisplay(); - }); - // Add the move down custom rule button to re-order the custom rule. - settingRuleEl.addExtraButton((btn) => { - const isFirstOrder = currentOrder === 0; - btn.setDisabled(isFirstOrder); - btn.extraSettingsEl.style.cursor = isFirstOrder - ? 'not-allowed' - : 'default'; - btn.extraSettingsEl.style.opacity = isFirstOrder ? '50%' : '100%'; - btn.setIcon('arrow-up'); - btn.setTooltip('Prioritize the custom rule'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - yield orderCustomRules(-1); - })); - }); - // Add the move up custom rule button to re-order the custom rule. - settingRuleEl.addExtraButton((btn) => { - const isLastOrder = currentOrder === this.plugin.getSettings().rules.length - 1; - btn.setDisabled(isLastOrder); - btn.extraSettingsEl.style.cursor = isLastOrder - ? 'not-allowed' - : 'default'; - btn.extraSettingsEl.style.opacity = isLastOrder ? '50%' : '100%'; - btn.setIcon('arrow-down'); - btn.setTooltip('Deprioritize the custom rule'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - yield orderCustomRules(1); - })); - }); - // Add the edit custom rule button. - settingRuleEl.addButton((btn) => { - btn.setIcon('pencil'); - btn.setTooltip('Edit the custom rule'); - btn.onClick(() => { - var _a, _b; - // Create modal and its children elements. - const modal = new obsidian.Modal(this.plugin.app); - modal.contentEl.style.display = 'block'; - modal.modalEl.classList.add('iconize-custom-modal'); - modal.titleEl.setText('Edit custom rule'); - // Create the input for the rule. - this.createDescriptionEl(modal.contentEl, 'Regex or simple string'); - const input = new obsidian.TextComponent(modal.contentEl); - input.setValue(rule.rule); - input.onChange((value) => __awaiter(this, void 0, void 0, function* () { - rule.rule = value; - })); - const useFilePathContainer = modal.contentEl.createDiv(); - useFilePathContainer.style.display = 'flex'; - useFilePathContainer.style.alignItems = 'center'; - useFilePathContainer.style.justifyContent = 'space-between'; - useFilePathContainer.style.marginTop = 'var(--size-4-5)'; - const useFilePathDescription = useFilePathContainer.createEl('p', { - text: 'Include folders and files that are part of the path.', - cls: 'setting-item-description', - }); - useFilePathDescription.style.margin = '0'; - useFilePathDescription.style.marginBottom = 'var(--size-2-2)'; - new obsidian.ToggleComponent(useFilePathContainer) - .setValue(rule.useFilePath === true) - .onChange((value) => { - rule.useFilePath = value; - }); - // Create the toggle for changing the rule type. - const ruleTypeContainer = modal.contentEl.createDiv(); - ruleTypeContainer.style.display = 'flex'; - ruleTypeContainer.style.alignItems = 'center'; - ruleTypeContainer.style.justifyContent = 'space-between'; - ruleTypeContainer.style.marginTop = 'var(--size-4-5)'; - const ruleTypeDescription = ruleTypeContainer.createEl('p', { - text: 'Where the custom rule gets applied to.', - cls: 'setting-item-description', - }); - ruleTypeDescription.style.margin = '0'; - ruleTypeDescription.style.marginBottom = 'var(--size-2-2)'; - const ruleTypeButton = new obsidian.ButtonComponent(ruleTypeContainer); - const setButtonContent = (isFor) => { - if (isFor === 'folders') { - ruleTypeButton.setIcon('folder'); - } - else if (isFor === 'files') { - ruleTypeButton.setIcon('document'); - } - else { - ruleTypeButton.setIcon('documents'); - } - ruleTypeButton.setTooltip(`Icon applicable to: ${isFor}`); - }; - setButtonContent((_a = rule.for) !== null && _a !== void 0 ? _a : 'everything'); - ruleTypeButton.onClick(() => __awaiter(this, void 0, void 0, function* () { - var _a; - const isFor = (_a = rule.for) !== null && _a !== void 0 ? _a : 'everything'; - this.updateIconTabs(rule, true); - yield customRule.removeFromAllFiles(this.plugin, Object.assign(Object.assign({}, rule), { for: isFor })); - if (isFor === 'folders') { - rule.for = 'everything'; - } - else if (isFor === 'files') { - rule.for = 'folders'; - } - else { - rule.for = 'files'; - } - setButtonContent(rule.for); - })); - // Create the change icon button with icon preview. - this.createDescriptionEl(modal.contentEl, 'Custom rule icon'); - const iconContainer = modal.contentEl.createDiv(); - iconContainer.style.display = 'flex'; - iconContainer.style.alignItems = 'center'; - iconContainer.style.justifyContent = 'space-between'; - const iconEl = iconContainer.createDiv(); - const iconPreviewEl = iconEl.createDiv(); - dom.setIconForNode(this.plugin, rule.icon, iconPreviewEl); - iconEl.style.display = 'flex'; - iconEl.style.alignItems = 'center'; - iconEl.style.justifyContent = 'space-between'; - iconEl.style.margin = null; - iconPreviewEl.innerHTML = svg.setFontSize(iconPreviewEl.innerHTML, 20); - const iconNameEl = iconEl.createEl('div', { - cls: 'setting-item-description', - }); - iconNameEl.style.paddingTop = '0'; - iconNameEl.style.marginLeft = 'var(--size-2-2)'; - iconNameEl.innerText = rule.icon; - const changeIconBtn = new obsidian.ButtonComponent(iconContainer); - changeIconBtn.setButtonText('Change icon'); - changeIconBtn.onClick(() => __awaiter(this, void 0, void 0, function* () { - const modal = new IconsPickerModal(this.app, this.plugin, rule.icon); - modal.onChooseItem = (item) => __awaiter(this, void 0, void 0, function* () { - const icon = typeof item === 'object' ? item.displayName : item; - rule.icon = icon; - dom.setIconForNode(this.plugin, rule.icon, iconPreviewEl); - iconPreviewEl.innerHTML = svg.setFontSize(iconPreviewEl.innerHTML, 20); - iconNameEl.innerText = getNormalizedName(rule.icon); - }); - modal.open(); - })); - // Create the color picker for the rule. - this.createDescriptionEl(modal.contentEl, 'Color of the icon'); - const colorContainer = modal.contentEl.createDiv(); - colorContainer.style.display = 'flex'; - colorContainer.style.alignItems = 'center'; - colorContainer.style.justifyContent = 'space-between'; - const colorPicker = new obsidian.ColorComponent(colorContainer) - .setValue((_b = rule.color) !== null && _b !== void 0 ? _b : '#000000') - .onChange((value) => { - rule.color = value; - }); - const defaultColorButton = new obsidian.ButtonComponent(colorContainer); - defaultColorButton.setTooltip('Set color to the default one'); - defaultColorButton.setButtonText('Default'); - defaultColorButton.onClick(() => { - colorPicker.setValue('#000000'); - rule.color = undefined; - }); - // Create the save button. - const button = new obsidian.ButtonComponent(modal.contentEl); - button.buttonEl.style.marginTop = 'var(--size-4-4)'; - button.buttonEl.style.float = 'right'; - button.setButtonText('Save Changes'); - button.onClick(() => __awaiter(this, void 0, void 0, function* () { - if (!emoji.isEmoji(oldRule.icon)) { - // Tries to remove the previously used icon from the icon pack. - removeIconFromIconPack(this.plugin, oldRule.icon); - } - if (!emoji.isEmoji(rule.icon)) { - // Tries to add the newly used icon to the icon pack. - saveIconToIconPack(this.plugin, rule.icon); - rule.icon = getNormalizedName(rule.icon); - } - this.refreshDisplay(); - new obsidian.Notice('Custom rule updated.'); - // Refresh the DOM. - yield customRule.removeFromAllFiles(this.plugin, oldRule); - this.updateIconTabs(rule, true); - this.plugin.getSettings().rules.forEach((rule) => __awaiter(this, void 0, void 0, function* () { - yield customRule.addToAllFiles(this.plugin, rule); - this.updateIconTabs(rule, false); - })); - yield this.plugin.saveIconFolderData(); - modal.close(); - })); - modal.open(); - }); - }); - // Add the delete custom rule button. - settingRuleEl.addButton((btn) => { - btn.setIcon('trash'); - btn.setTooltip('Remove the custom rule'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - const newRules = this.plugin - .getSettings() - .rules.filter((r) => rule.rule !== r.rule || - rule.color !== r.color || - rule.icon !== r.icon || - rule.for !== r.for); - this.plugin.getSettings().rules = newRules; - yield this.plugin.saveIconFolderData(); - this.refreshDisplay(); - new obsidian.Notice('Custom rule deleted.'); - yield customRule.removeFromAllFiles(this.plugin, rule); - removeIconFromIconPack(this.plugin, rule.icon); - this.updateIconTabs(rule, true); - const previousRules = this.plugin - .getSettings() - .rules.filter((r) => rule.for === r.for); - previousRules.forEach((previousRule) => __awaiter(this, void 0, void 0, function* () { - yield customRule.addToAllFiles(this.plugin, previousRule); - this.updateIconTabs(previousRule, false); - })); - })); - }); - }); - } -} - -const getTitleIcon = (leaf) => { - return leaf.querySelector(`.${config.TITLE_ICON_CLASS}`); -}; -const add = (plugin, inlineTitleEl, svgElement, options) => { - var _a; - if (!inlineTitleEl.parentElement) { - return; - } - if (options === null || options === void 0 ? void 0 : options.fontSize) { - svgElement = svg.setFontSize(svgElement, options.fontSize); - } - let titleIcon = getTitleIcon(inlineTitleEl.parentElement); - if (!titleIcon) { - titleIcon = document.createElement('div'); - } - const isInline = plugin.getSettings().iconInTitlePosition === IconInTitlePosition.Inline; - if (isInline) { - titleIcon.style.display = 'inline-block'; - titleIcon.style.removeProperty('margin-inline'); - titleIcon.style.removeProperty('width'); - } - else { - titleIcon.style.display = 'block'; - titleIcon.style.width = 'var(--line-width)'; - titleIcon.style.marginInline = '0'; - } - titleIcon.classList.add(config.TITLE_ICON_CLASS); - // Checks if the passed element is an emoji. - if (emoji.isEmoji(svgElement) && options.fontSize) { - svgElement = - (_a = emoji.parseEmoji(plugin.getSettings().emojiStyle, svgElement, options.fontSize)) !== null && _a !== void 0 ? _a : svgElement; - titleIcon.style.fontSize = `${options.fontSize}px`; - } - titleIcon.innerHTML = svgElement; - let wrapperElement = inlineTitleEl.parentElement; - // Checks the parent and selects the correct wrapper element. - // This should only happen in the beginning. - if (wrapperElement && - !wrapperElement.classList.contains(config.INLINE_TITLE_WRAPPER_CLASS)) { - wrapperElement = wrapperElement.querySelector(`.${config.INLINE_TITLE_WRAPPER_CLASS}`); - } - // Whenever there is no correct wrapper element, we create one. - if (!wrapperElement) { - wrapperElement = inlineTitleEl.parentElement.createDiv(); - wrapperElement.classList.add(config.INLINE_TITLE_WRAPPER_CLASS); - } - // Avoiding adding the same nodes together when changing the title. - if (wrapperElement !== inlineTitleEl.parentElement) { - inlineTitleEl.parentElement.prepend(wrapperElement); - } - if (isInline) { - wrapperElement.style.display = 'flex'; - wrapperElement.style.alignItems = 'flex-start'; - const inlineTitlePaddingTop = getComputedStyle(inlineTitleEl, null).getPropertyValue('padding-top'); - titleIcon.style.paddingTop = inlineTitlePaddingTop; - if (emoji.isEmoji(svgElement)) { - titleIcon.style.transform = 'translateY(-9%)'; - } - else { - titleIcon.style.transform = 'translateY(9%)'; - } - } - else { - wrapperElement.style.display = 'block'; - titleIcon.style.transform = 'translateY(9%)'; - } - wrapperElement.append(titleIcon); - wrapperElement.append(inlineTitleEl); -}; -const updateStyle = (inlineTitleEl, options) => { - if (!inlineTitleEl.parentElement) { - return; - } - const titleIcon = getTitleIcon(inlineTitleEl.parentElement); - if (!titleIcon) { - return; - } - if (options.fontSize) { - if (!emoji.isEmoji(titleIcon.innerHTML)) { - titleIcon.innerHTML = svg.setFontSize(titleIcon.innerHTML, options.fontSize); - } - else { - titleIcon.style.fontSize = `${options.fontSize}px`; - } - } -}; -/** - * Hides the title icon from the provided HTMLElement. - * @param contentEl HTMLElement to hide the title icon from. - */ -const hide = (inlineTitleEl) => { - if (!inlineTitleEl.parentElement) { - return; - } - const titleIconContainer = getTitleIcon(inlineTitleEl.parentElement); - if (!titleIconContainer) { - return; - } - titleIconContainer.style.display = 'none'; -}; -const remove = (inlineTitleEl) => { - if (!inlineTitleEl.parentElement) { - return; - } - const titleIconContainer = getTitleIcon(inlineTitleEl.parentElement); - if (!titleIconContainer) { - return; - } - titleIconContainer.remove(); -}; -var titleIcon = { - add, - updateStyle, - hide, - remove, -}; - -// Cache for font size -let cachedFontSize = null; -let fontSizeCacheTime = 0; -const calculateFontTextSize = () => { - var _a; - // get cached font size if available - const now = Date.now(); - if (cachedFontSize !== null && now - fontSizeCacheTime < 2000) { - return cachedFontSize; - } - let fontSize = parseFloat((_a = getComputedStyle(document.body).getPropertyValue('--font-text-size')) !== null && _a !== void 0 ? _a : '0'); - if (!fontSize) { - fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); - } - // set font size cache - cachedFontSize = fontSize; - fontSizeCacheTime = now; - return fontSize; -}; -const calculateInlineTitleSize = () => { - const fontSize = calculateFontTextSize(); - const inlineTitleSizeValue = getComputedStyle(document.body).getPropertyValue('--inline-title-size'); - const unit = inlineTitleSizeValue.replace(/[\d.]/g, ''); - let inlineTitleSize = parseFloat(inlineTitleSizeValue); - if (unit === 'px') { - inlineTitleSize /= 16; - } - return fontSize * inlineTitleSize; -}; -const isHeader = (value) => { - return /^h[1-6]$/.test(value); -}; -const getHTMLHeaderByToken = (header) => { - for (let i = 1; i <= 6; i++) { - if (header === `header-${i}`) { - return `h${i}`; - } - } - return null; -}; -const calculateHeaderSize = (header) => { - var _a; - const fontSize = calculateFontTextSize(); - const htmlHeader = (_a = getHTMLHeaderByToken(header)) !== null && _a !== void 0 ? _a : header; - const headerComputedStyle = getComputedStyle(document.body).getPropertyValue(`--${htmlHeader}-size`); - let headerSize = parseFloat(headerComputedStyle); - if (isPx(headerComputedStyle)) { - headerSize = pxToRem(headerSize, fontSize); - } - // If there is some `calc` operation going on, it has to be evaluated. - if (headerComputedStyle.includes('calc')) { - const temp = document.createElement('div'); - temp.style.setProperty('font-size', `var(--${htmlHeader}-size)`); - document.body.appendChild(temp); - const computedStyle = window.getComputedStyle(temp); - const computedValue = computedStyle.getPropertyValue('font-size'); - headerSize = parseFloat(computedValue); - if (isPx(computedValue)) { - headerSize = pxToRem(headerSize, fontSize); - } - document.body.removeChild(temp); - } - return fontSize * headerSize; -}; -const pxToRem = (px, baseSize = 16) => { - return px / baseSize; -}; -const isPx = (value) => { - return /^-?\d+(\.\d+)?px$/.test(value); -}; - -class EmojiStyleSetting extends IconFolderSetting { - display() { - const emojiStyle = new obsidian.Setting(this.containerEl) - .setName('Emoji style') - .setDesc('Change the style of your emojis.'); - emojiStyle.addDropdown((dropdown) => { - dropdown.addOption('native', 'Native'); - dropdown.addOption('twemoji', 'Twemoji'); - dropdown.setValue(this.plugin.getSettings().emojiStyle); - dropdown.onChange((value) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().emojiStyle = value; - this.updateDOM(); - yield this.plugin.saveIconFolderData(); - })); - }); - } - updateDOM() { - for (const fileExplorer of this.plugin.getRegisteredFileExplorers()) { - const fileItems = Object.entries(fileExplorer.fileItems || {}); - for (const [path, _] of fileItems) { - let iconName = this.plugin.getData()[path]; - if (!iconName) { - continue; - } - const data = this.plugin.getData()[path]; - if (typeof data === 'object') { - const data = this.plugin.getData()[path]; - if (data.iconName) { - iconName = data.iconName; - } - } - if (emoji.isEmoji(iconName)) { - dom.createIconNode(this.plugin, path, iconName); - if (this.plugin.getSettings().iconInTabsEnabled) { - const tabLeaves = iconTabs.getTabLeavesOfFilePath(this.plugin, path); - for (const tabLeaf of tabLeaves) { - iconTabs.update(this.plugin, iconName, tabLeaf.tabHeaderInnerIconEl); - } - } - if (this.plugin.getSettings().iconInTitleEnabled) { - for (const openedFile of getAllOpenedFiles(this.plugin)) { - const activeView = openedFile.leaf.view; - if (activeView instanceof obsidian.MarkdownView && - openedFile.path === path) { - titleIcon.add(this.plugin, activeView.inlineTitleEl, iconName, { - fontSize: calculateInlineTitleSize(), - }); - } - } - } - } - } - } - for (const rule of customRule.getSortedRules(this.plugin)) { - customRule.addToAllFiles(this.plugin, rule); - } - } -} - -/** - * Helper function that refreshes the style of all the icons that are defined - * or in a custom rule involved. - * @param plugin Instance of the IconizePlugin. - */ -const refreshStyleOfIcons = (plugin) => __awaiter(void 0, void 0, void 0, function* () { - // Refreshes the icon style for all normally added icons. - style.refreshIconNodes(plugin); - // Refreshes the icon style for all custom icon rules, when the color of the rule is - // not defined. - for (const rule of customRule.getSortedRules(plugin)) { - const fileItems = yield customRule.getFileItems(plugin, rule); - for (const fileItem of fileItems) { - const titleEl = getFileItemTitleEl(fileItem); - const iconNode = titleEl.querySelector('.iconize-icon'); - let iconContent = iconNode.innerHTML; - iconContent = style.applyAll(plugin, iconContent, iconNode); - if (rule.color) { - iconContent = svg.colorize(iconContent, rule.color); - iconNode.style.color = rule.color; - } - iconNode.innerHTML = iconContent; - } - } -}); -var helper = { - refreshStyleOfIcons, -}; - -class ExtraMarginSetting extends IconFolderSetting { - display() { - var _a, _b; - const extraMarginSetting = new obsidian.Setting(this.containerEl) - .setName('Extra margin (in pixels)') - .setDesc('Change the margin of the icons.') - .setClass('iconize-setting'); - const extraMarginDropdown = new obsidian.DropdownComponent(extraMarginSetting.controlEl).addOptions({ - top: 'Top', - right: 'Right', - bottom: 'Bottom', - left: 'Left', - }); - const extraMarginSlider = new obsidian.SliderComponent(extraMarginSetting.controlEl) - .setLimits(-24, 24, 1) - .setDynamicTooltip() - .setValue((_b = (_a = this.plugin.getSettings().extraMargin) === null || _a === void 0 ? void 0 : _a.top) !== null && _b !== void 0 ? _b : 2) - .onChange((val) => __awaiter(this, void 0, void 0, function* () { - const dropdownValue = extraMarginDropdown.getValue(); - if (this.plugin.getSettings().extraMargin) { - this.plugin.getSettings().extraMargin[dropdownValue] = val; - } - else { - this.plugin.getSettings().extraMargin = { - [dropdownValue]: val, - }; - } - yield this.plugin.saveIconFolderData(); - helper.refreshStyleOfIcons(this.plugin); - })); - extraMarginDropdown.onChange((val) => { - var _a; - if (this.plugin.getSettings().extraMargin) { - extraMarginSlider.setValue((_a = this.plugin.getSettings().extraMargin[val]) !== null && _a !== void 0 ? _a : 2); - } - else { - extraMarginSlider.setValue(2); - } - }); - extraMarginSetting.components.push(extraMarginDropdown, extraMarginSlider); - } -} - -class ResetButtonComponent extends obsidian.ButtonComponent { - constructor(contentEl) { - super(contentEl); - this.contentEl = contentEl; - this.setTooltip('Restore default'); - this.setIcon('rotate-ccw'); - this.render(); - } - render() { - this.buttonEl.classList.add('clickable-icon'); - this.buttonEl.classList.add('extra-setting-button'); - } -} - -const DEFAULT_VALUE = DEFAULT_SETTINGS.iconColor; -class IconColorSetting extends IconFolderSetting { - display() { - var _a; - const setting = new obsidian.Setting(this.containerEl) - .setName('Icon color') - .setDesc('Change the color of the displayed icons.'); - new ResetButtonComponent(setting.controlEl).onClick(() => __awaiter(this, void 0, void 0, function* () { - colorPicker.setValue(DEFAULT_VALUE); - this.plugin.getSettings().iconColor = null; - // Custom saving to not save the color black in the data. - yield this.plugin.saveIconFolderData(); - helper.refreshStyleOfIcons(this.plugin); - })); - const colorPicker = new obsidian.ColorComponent(setting.controlEl) - .setValue((_a = this.plugin.getSettings().iconColor) !== null && _a !== void 0 ? _a : DEFAULT_VALUE) - .onChange((value) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().iconColor = value; - yield this.plugin.saveIconFolderData(); - helper.refreshStyleOfIcons(this.plugin); - })); - } -} - -const values = { - min: 10, - max: 64, - default: DEFAULT_SETTINGS.fontSize, - step: 1, -}; -class IconFontSizeSetting extends IconFolderSetting { - display() { - const setting = new obsidian.Setting(this.containerEl) - .setName('Icon font size (in pixels)') - .setDesc('Change the font size of the displayed icons.'); - new ResetButtonComponent(setting.controlEl).onClick(() => { - this.slider.setValue(values.default); - }); - setting.addSlider((slider) => { - var _a; - this.slider = slider; - slider - .setLimits(values.min, values.max, values.step) - .setDynamicTooltip() - .setValue((_a = this.plugin.getSettings().fontSize) !== null && _a !== void 0 ? _a : DEFAULT_SETTINGS.fontSize) - .onChange((val) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().fontSize = val; - yield this.plugin.saveIconFolderData(); - helper.refreshStyleOfIcons(this.plugin); - })); - }); - } -} - -class IconPacksPathSetting extends IconFolderSetting { - display() { - const iconPacksPathSetting = new obsidian.Setting(this.containerEl) - .setName('Icon packs folder path') - .setDesc('Change the default icon packs folder path.'); - iconPacksPathSetting.addText((text) => { - this.iconPacksSettingTextComp = text; - text.setValue(this.plugin.getSettings().iconPacksPath); - }); - iconPacksPathSetting.addButton((btn) => { - btn.setButtonText('Save'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - const newPath = this.iconPacksSettingTextComp.getValue(); - const oldPath = this.plugin.getSettings().iconPacksPath; - if (oldPath === this.iconPacksSettingTextComp.getValue()) { - return; - } - new obsidian.Notice('Saving in progress...'); - setPath(newPath); - yield createDefaultDirectory(this.plugin); - yield moveIconPackDirectories(this.plugin, oldPath, newPath); - this.plugin.getSettings().iconPacksPath = newPath; - yield this.plugin.saveIconFolderData(); - new obsidian.Notice('...saved successfully'); - })); - }); - } -} - -class IconPacksBackgroundChecker extends IconFolderSetting { - display() { - new obsidian.Setting(this.containerEl) - .setName('Icons background check') - .setDesc('Check in the background on every load of Obsidian, if icons are missing and it will try to add them to the specific icon pack.') - .addToggle((toggle) => { - toggle - .setValue(this.plugin.getSettings().iconsBackgroundCheckEnabled) - .onChange((enabled) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().iconsBackgroundCheckEnabled = enabled; - yield this.plugin.saveIconFolderData(); - if (enabled) { - new obsidian.Notice('You need to reload Obsidian for this to take effect.', 10000); - } - })); - }); - } -} - -class IconPackBrowserModal extends obsidian.FuzzySuggestModal { - constructor(app, plugin) { - super(app); - this.plugin = plugin; - this.resultContainerEl.classList.add('iconize-browse-modal'); - this.inputEl.placeholder = 'Select to download icon pack'; - } - // eslint-disable-next-line - onAddedIconPack() { } - onOpen() { - super.onOpen(); - } - onClose() { - this.contentEl.empty(); - } - getItemText(item) { - const prefix = createIconPackPrefix(item.name); - return `${item.displayName} (${prefix})`; - } - getItems() { - const predefinedIconPacks = Object.values(iconPacks$1); - const allIconPacks = getAllIconPacks(); - return predefinedIconPacks.filter((iconPack) => allIconPacks.find((ip) => iconPack.name === ip.name) === undefined); - } - onChooseItem(item, _event) { - return __awaiter(this, void 0, void 0, function* () { - new obsidian.Notice(`Adding ${item.displayName}...`); - const arrayBuffer = yield downloadZipFile(item.downloadLink); - yield createZipFile(this.plugin, `${item.name}.zip`, arrayBuffer); - yield registerIconPack(item.name, arrayBuffer); - new obsidian.Notice(`...${item.displayName} added`); - this.onAddedIconPack(); - }); - } - renderSuggestion(item, el) { - super.renderSuggestion(item, el); - el.innerHTML = `
${el.innerHTML}
`; - } -} - -class PredefinedIconPacksSetting extends IconFolderSetting { - constructor(plugin, containerEl, app, refreshDisplay) { - super(plugin, containerEl); - this.app = app; - this.refreshDisplay = refreshDisplay; - } - display() { - new obsidian.Setting(this.containerEl) - .setName('Add predefined icon pack') - .setDesc('Add a predefined icon pack that is officially supported.') - .addButton((btn) => { - btn.setButtonText('Browse icon packs'); - btn.onClick(() => { - const modal = new IconPackBrowserModal(this.app, this.plugin); - modal.onAddedIconPack = () => { - this.refreshDisplay(); - }; - modal.open(); - }); - }); - } -} - -class RecentlyUsedIconsSetting extends IconFolderSetting { - display() { - new obsidian.Setting(this.containerEl) - .setName('Recently used icons limit') - .setDesc('Change the limit for the recently used icons displayed in the icon selection modal.') - .addSlider((slider) => { - var _a; - slider - .setLimits(1, 25, 1) - .setDynamicTooltip() - .setValue((_a = this.plugin.getSettings().recentlyUsedIconsSize) !== null && _a !== void 0 ? _a : DEFAULT_SETTINGS.recentlyUsedIconsSize) - .onChange((val) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().recentlyUsedIconsSize = val; - yield this.plugin.checkRecentlyUsedIcons(); - yield this.plugin.saveIconFolderData(); - })); - }); - } -} - -class ToggleIconInTabs extends IconFolderSetting { - display() { - new obsidian.Setting(this.containerEl) - .setName('Toggle icon in tabs') - .setDesc('Toggles the visibility of an icon for a file in the tab bar.') - .addToggle((toggle) => { - toggle - .setValue(this.plugin.getSettings().iconInTabsEnabled) - .onChange((enabled) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().iconInTabsEnabled = enabled; - yield this.plugin.saveIconFolderData(); - // Updates the already opened files. - this.plugin.app.workspace - .getLeavesOfType('markdown') - .forEach((leaf) => { - const file = leaf.view.file; - if (file) { - const tabHeaderLeaf = leaf; - if (enabled) { - // Adds the icons to already opened files. - iconTabs.add(this.plugin, file.path, tabHeaderLeaf.tabHeaderInnerIconEl); - } - else { - // Removes the icons from already opened files. - iconTabs.remove(tabHeaderLeaf.tabHeaderInnerIconEl); - } - } - }); - })); - }); - } -} - -class ToggleIconInTitle extends IconFolderSetting { - updateLeaves(options) { - this.plugin.app.workspace.getLeavesOfType('markdown').forEach((leaf) => { - const view = leaf.view; - if (view instanceof obsidian.MarkdownView) { - const foundIcon = icon.getIconByPath(this.plugin, view.file.path); - if (foundIcon && options.enabled) { - if (options.removeBeforeReAdd) { - // Remove the icon before re-adding it. This is needed to update the DOM because - // the icon node will be inserted in the beginning inline title node. - titleIcon.remove(view.contentEl); - } - const content = typeof foundIcon === 'string' ? foundIcon : foundIcon.svgElement; - titleIcon.add(this.plugin, view.inlineTitleEl, content, { - fontSize: calculateInlineTitleSize(), - }); - } - else { - titleIcon.remove(view.contentEl); - } - } - }); - } - display() { - new obsidian.Setting(this.containerEl) - .setName('Toggle icon in title') - .setDesc('Toggles the visibility of an icon above the title of a file.') - .addDropdown((dropdown) => { - this.dropdown = dropdown; - dropdown.setDisabled(!this.plugin.getSettings().iconInTitleEnabled); - dropdown.addOptions({ - above: 'Above title', - inline: 'Next to title', - }); - dropdown.setValue(this.plugin.getSettings().iconInTitlePosition); - dropdown.onChange((value) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().iconInTitlePosition = - value; - yield this.plugin.saveIconFolderData(); - this.updateLeaves({ enabled: true, removeBeforeReAdd: true }); - })); - }) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.getSettings().iconInTitleEnabled) - .onChange((enabled) => __awaiter(this, void 0, void 0, function* () { - if (this.dropdown) { - this.dropdown.setDisabled(!enabled); - } - this.plugin.getSettings().iconInTitleEnabled = enabled; - yield this.plugin.saveIconFolderData(); - this.updateLeaves({ enabled }); - })); - }); - } -} - -class FrontmatterOptions extends IconFolderSetting { - display() { - new obsidian.Setting(this.containerEl) - .setName('Use icon in frontmatter') - .setDesc('Toggles whether to set the icon based on the frontmatter property `icon`.') - .addToggle((toggle) => { - toggle - .setValue(this.plugin.getSettings().iconInFrontmatterEnabled) - .onChange((enabled) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().iconInFrontmatterEnabled = enabled; - yield this.plugin.saveIconFolderData(); - })); - }); - new obsidian.Setting(this.containerEl) - .setName('Frontmatter icon field name') - .setDesc('Sets the name of the frontmatter field which contains the icon.') - .addText((text) => { - this.iconFieldNameTextComp = text; - text.setValue(this.plugin.getSettings().iconInFrontmatterFieldName); - }) - .addButton((button) => { - button.setButtonText('Save'); - button.onClick(() => __awaiter(this, void 0, void 0, function* () { - const newValue = this.iconFieldNameTextComp.getValue(); - const oldValue = this.plugin.getSettings().iconInFrontmatterFieldName; - if (newValue === oldValue) { - return; - } - this.plugin.getSettings().iconInFrontmatterFieldName = newValue; - yield this.plugin.saveIconFolderData(); - new obsidian.Notice('...saved successfully'); - })); - }); - new obsidian.Setting(this.containerEl) - .setName('Frontmatter icon color field name') - .setDesc('Sets the name of the frontmatter field which contains the icon color.') - .addText((text) => { - this.iconColorFieldNameTextComp = text; - text.setValue(this.plugin.getSettings().iconColorInFrontmatterFieldName); - }) - .addButton((button) => { - button.setButtonText('Save'); - button.onClick(() => __awaiter(this, void 0, void 0, function* () { - const newValue = this.iconColorFieldNameTextComp.getValue(); - const oldValue = this.plugin.getSettings().iconColorInFrontmatterFieldName; - if (newValue === oldValue) { - return; - } - this.plugin.getSettings().iconColorInFrontmatterFieldName = newValue; - yield this.plugin.saveIconFolderData(); - new obsidian.Notice('...saved successfully'); - })); - }); - new obsidian.Setting(this.containerEl) - .setName('Refresh icons from frontmatter') - .setDesc('Sets the icon and color for each note in the vault based on the frontmatter properties. WARNING: This will change any manually set icons to the one defined in the frontmatter. IF A NOTE HAS NO FRONTMATTER, THE CURRENT ICON WILL BE REMOVED. Please restart Obsidian after this completes to see the changes.') - .addButton((btn) => { - btn.setButtonText('Refresh').onClick(() => __awaiter(this, void 0, void 0, function* () { - var _a, _b; - if (!this.plugin.getSettings().iconInFrontmatterEnabled) { - new obsidian.Notice(`[${config.PLUGIN_NAME}] Please enable "Use icon in frontmatter".`); - return; - } - new obsidian.Notice(`[${config.PLUGIN_NAME}] Refreshing icons from frontmatter, please wait...`); - const files = this.plugin.app.vault.getMarkdownFiles(); - for (const file of files) { - const fileCache = this.plugin.app.metadataCache.getFileCache(file); - const frontmatterIconKey = this.plugin.getSettings().iconInFrontmatterFieldName; - const frontmatterIconColorKey = this.plugin.getSettings().iconColorInFrontmatterFieldName; - const iconName = (_a = fileCache.frontmatter) === null || _a === void 0 ? void 0 : _a[frontmatterIconKey]; - let iconColor = (_b = fileCache.frontmatter) === null || _b === void 0 ? void 0 : _b[frontmatterIconColorKey]; - if (!iconName) { - yield this.plugin.removeFolderIcon(file.path); - continue; - } - if (typeof iconName !== 'string') { - const message = `${file.path}\nFrontmatter property type \`${frontmatterIconKey}\` has to be of type \`text\`.`; - logger.warn(message); - new obsidian.Notice(`[${config.PLUGIN_NAME}]\n${message}`); - continue; - } - this.plugin.addFolderIcon(file.path, iconName); - if (!iconColor) { - yield this.plugin.removeIconColor(file.path); - continue; - } - if (typeof iconColor !== 'string') { - const message = `${file.path}\nFrontmatter property type \`${frontmatterIconColorKey}\` has to be of type \`text\`.`; - logger.warn(message); - new obsidian.Notice(`[${config.PLUGIN_NAME}]\n${message}`); - continue; - } - iconColor = isHexadecimal(iconColor) - ? stringToHex(iconColor) - : iconColor; - this.plugin.addIconColor(file.path, iconColor); - } - new obsidian.Notice(`[${config.PLUGIN_NAME}] Refreshed icons from frontmatter. Please restart Obsidian to see the changes.`); - })); - }); - } -} - -class ToggleIconsInEditor extends IconFolderSetting { - display() { - new obsidian.Setting(this.containerEl) - .setName('Toggle icons while editing notes') - .setDesc('Toggles whether you are able to add and see icons in your notes and editor (e.g., ability to have :LiSofa: as an icon in your notes).') - .addToggle((toggle) => { - toggle - .setValue(this.plugin.getSettings().iconsInNotesEnabled) - .onChange((enabled) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().iconsInNotesEnabled = enabled; - yield this.plugin.saveIconFolderData(); - new obsidian.Notice(`[${config.PLUGIN_NAME}] Obsidian has to be restarted for this change to take effect.`); - })); - }); - } -} - -class ToggleIconsInLinks extends IconFolderSetting { - display() { - new obsidian.Setting(this.containerEl) - .setName('Toggle icons in links') - .setDesc('Toggles whether you are able to see icons in the links to other notes') - .addToggle((toggle) => { - toggle - .setValue(this.plugin.getSettings().iconsInLinksEnabled) - .onChange((enabled) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().iconsInLinksEnabled = enabled; - yield this.plugin.saveIconFolderData(); - new obsidian.Notice(`[${config.PLUGIN_NAME}] Obsidian has to be restarted for this change to take effect.`); - })); - }); - } -} - -class IconIdentifierSetting extends IconFolderSetting { - display() { - const setting = new obsidian.Setting(this.containerEl) - .setName('Icon identifier') - .setDesc('Change the icon identifier used in notes.') - .setClass('iconize-setting'); - setting.addText((text) => { - this.textComp = text; - text.setValue(this.plugin.getSettings().iconIdentifier); - }); - setting.addButton((btn) => { - btn.setButtonText('Save'); - btn.onClick(() => __awaiter(this, void 0, void 0, function* () { - const newIdentifier = this.textComp.getValue(); - const oldIdentifier = this.plugin.getSettings().iconIdentifier; - if (newIdentifier === oldIdentifier) { - return; - } - this.plugin.getSettings().iconIdentifier = newIdentifier; - yield this.plugin.saveIconFolderData(); - new obsidian.Notice('...saved successfully'); - })); - }); - } -} - -class DebugMode extends IconFolderSetting { - display() { - new obsidian.Setting(this.containerEl) - .setName('Toggle Debug Mode') - .setDesc('Toggle debug mode to see more detailed logs in the console. Do not touch this unless you know what you are doing.') - .addToggle((toggle) => { - toggle - .setValue(this.plugin.getSettings().debugMode) - .onChange((enabled) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().debugMode = enabled; - yield this.plugin.saveIconFolderData(); - })); - }); - } -} - -class UseInternalPlugins extends IconFolderSetting { - display() { - new obsidian.Setting(this.containerEl) - .setName('EXPERIMENTAL: Use internal plugins') - .setDesc('Toggles whether to try to add icons to the bookmark and outline internal plugins.') - .addToggle((toggle) => { - toggle - .setValue(this.plugin.getSettings().useInternalPlugins) - .onChange((enabled) => __awaiter(this, void 0, void 0, function* () { - this.plugin.getSettings().useInternalPlugins = enabled; - yield this.plugin.saveIconFolderData(); - new obsidian.Notice(`[${config.PLUGIN_NAME}] Obsidian has to be restarted for this change to take effect.`); - })); - }); - } -} - -class IconFolderSettings extends obsidian.PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - display() { - const { plugin, containerEl, app } = this; - containerEl.empty(); - containerEl.createEl('h1', { text: 'General' }); - new RecentlyUsedIconsSetting(plugin, containerEl).display(); - new IconPacksPathSetting(plugin, containerEl).display(); - new IconPacksBackgroundChecker(plugin, containerEl).display(); - new EmojiStyleSetting(plugin, containerEl).display(); - new IconIdentifierSetting(plugin, containerEl).display(); - new UseInternalPlugins(plugin, containerEl).display(); - new DebugMode(plugin, containerEl).display(); - containerEl.createEl('h3', { text: 'Visibility of icons' }); - new ToggleIconInTabs(plugin, containerEl).display(); - new ToggleIconInTitle(plugin, containerEl).display(); - new FrontmatterOptions(plugin, containerEl).display(); - new ToggleIconsInEditor(plugin, containerEl).display(); - new ToggleIconsInLinks(plugin, containerEl).display(); - containerEl.createEl('h1', { - text: 'Icon customization for files/folders', - }); - new IconFontSizeSetting(plugin, containerEl).display(); - new IconColorSetting(plugin, containerEl).display(); - new ExtraMarginSetting(plugin, containerEl).display(); - containerEl.createEl('h1', { text: 'Custom icon rules' }); - new CustomIconRuleSetting(plugin, containerEl, app, () => this.display()).display(); - containerEl.createEl('h1', { text: 'Icon packs' }); - new PredefinedIconPacksSetting(plugin, containerEl, app, () => this.display()).display(); - new CustomIconPackSetting(plugin, containerEl, () => this.display()).display(); - } -} - -function around(obj, factories) { - const removers = Object.keys(factories).map(key => around1(obj, key, factories[key])); - return removers.length === 1 ? removers[0] : function () { removers.forEach(r => r()); }; -} -function around1(obj, method, createWrapper) { - const original = obj[method], hadOwn = obj.hasOwnProperty(method); - let current = createWrapper(original); - // Let our wrapper inherit static props from the wrapping method, - // and the wrapping method, props from the original method - if (original) - Object.setPrototypeOf(current, original); - Object.setPrototypeOf(wrapper, current); - obj[method] = wrapper; - // Return a callback to allow safe removal - return remove; - function wrapper(...args) { - // If we have been deactivated and are no longer wrapped, remove ourselves - if (current === original && obj[method] === wrapper) - remove(); - return current.apply(this, args); - } - function remove() { - // If no other patches, just do a direct removal - if (obj[method] === wrapper) { - if (hadOwn) - obj[method] = original; - else - delete obj[method]; - } - if (current === original) - return; - // Else pass future calls through, and remove wrapper from the prototype chain - current = original; - Object.setPrototypeOf(wrapper, original || Function); - } -} - -class InternalPluginInjector { - constructor(plugin) { - this.plugin = plugin; - } - get fileExplorers() { - return this.plugin.app.workspace.getLeavesOfType('file-explorer'); - } - onMount() { } -} - -/** - * @deprecated After obsidian 1.2.6 in favor of the bookmarks plugin. - */ -class StarredInternalPlugin extends InternalPluginInjector { - constructor(plugin) { - super(plugin); - } - get starred() { - return this.plugin.app.internalPlugins.getPluginById('starred'); - } - get enabled() { - return this.plugin.app.internalPlugins.getPluginById('starred').enabled; - } - get leaf() { - const leaf = this.plugin.app.workspace.getLeavesOfType('starred'); - if (!leaf) { - return undefined; - } - if (leaf.length === 1) { - return leaf[0].view; - } - return undefined; - } - setIcon(filePath, node) { - const iconName = icon.getByPath(this.plugin, filePath); - const iconNode = node.querySelector('.nav-file-icon'); - if (!iconNode || !iconName) { - return; - } - dom.setIconForNode(this.plugin, iconName, iconNode); - } - computeNodesWithPath(callback) { - const { itemLookup, containerEl } = this.leaf; - const navFileEls = containerEl.querySelectorAll('.nav-file'); - navFileEls.forEach((navFileEl) => { - const lookupFile = itemLookup.get(navFileEl); - if (!lookupFile) { - return; - } - callback(navFileEl, lookupFile.path); - }); - } - onMount() { - const nodesWithPath = {}; - this.computeNodesWithPath((node, filePath) => { - nodesWithPath[filePath] = node; - }); - Object.entries(nodesWithPath).forEach(([filePath, node]) => this.setIcon(filePath, node)); - } - register() { - if (!this.plugin.app.internalPlugins.getPluginById('file-explorer').enabled) { - console.info(`[${config.PLUGIN_NAME}/Starred] Skipping starred internal plugin registration because file-explorer is not enabled.`); - return; - } - if (!this.enabled) { - console.info(`[${config.PLUGIN_NAME}/Starred] Skipping starred internal plugin registration because it's not enabled.`); - return; - } - // eslint-disable-next-line - const self = this; - this.plugin.register(around(this.starred.instance, { - addItem: function (next) { - return function (file) { - next.call(this, file); - self.onMount(); - }; - }, - removeItem: function (next) { - return function (file) { - next.call(this, file); - self.onMount(); - }; - }, - })); - } -} - -class BookmarkInternalPlugin extends InternalPluginInjector { - constructor(plugin) { - super(plugin); - } - get bookmark() { - return this.plugin.app.internalPlugins.getPluginById('bookmarks'); - } - get enabled() { - return this.plugin.app.internalPlugins.getPluginById('bookmarks').enabled; - } - get leaf() { - const leaf = this.plugin.app.workspace.getLeavesOfType('bookmarks'); - if (!leaf) { - return undefined; - } - if (leaf.length === 1) { - return leaf[0]; - } - return undefined; - } - setIconOrRemove(filePath, node) { - var _a; - const iconName = icon.getByPath(this.plugin, filePath); - let iconNode = node.querySelector('.tree-item-icon'); - if (!iconName) { - if (iconNode) { - // Reset the icon to the default obsidian icon. - const items = this.bookmark.instance.items; - const item = items.find((item) => item.path === filePath); - if ((item === null || item === void 0 ? void 0 : item.type) === 'file') { - iconNode.innerHTML = DEFAULT_FILE_ICON; - } - else if ((item === null || item === void 0 ? void 0 : item.type) === 'folder') { - iconNode.innerHTML = DEFAULT_FOLDER_ICON; - } - } - return; - } - // If the icon node is not defined, then we need to recreate it. - if (!iconNode) { - // Get the tree-item-self element where the original icon is set. - const treeItemSelf = node.querySelector('.tree-item-self'); - if (!treeItemSelf) { - return; - } - iconNode = node.createDiv({ cls: 'tree-item-icon' }); - // Prepends the icon to the tree-item-self element as a first child. - treeItemSelf.prepend(iconNode); - } - const defaultMargin = iconNode.style.margin; - const iconColor = (_a = this.plugin.getIconColor(filePath)) !== null && _a !== void 0 ? _a : this.plugin.getSettings().iconColor; - dom.setIconForNode(this.plugin, iconName, iconNode, { color: iconColor }); - // Reset the margin to the default value to prevent overlapping with the text. - iconNode.style.margin = defaultMargin; - } - computeNodesWithPath(callback) { - if (!this.leaf || !this.leaf.view) { - return; - } - /** - * Retrieves the lookup item from the bookmark plugin and calls the callback with the - * element and the path of the item. - * @param item BookmarkItem object which can be a folder or a file. - * @param itemDoms WeakMap of the bookmark plugin which contains the lookup item. - */ - const retrieveLookupItem = (item, itemDoms) => { - const lookupItem = itemDoms.get(item); - if (!lookupItem) { - return; - } - if (item.items) { - // If the item is a folder, then we need to retrieve all the items inside it. - for (const subItem of item.items) { - retrieveLookupItem(subItem, itemDoms); - } - } - // If the item is a `file` or a `folder` (not of type `group`), then we can call the callback. - if (item.type === 'file' || item.type === 'folder') { - callback(lookupItem.el, item.path); - } - }; - const { itemDoms } = this.leaf.view; - // Retrieves all the items of the bookmark plugin which areo objects. - const items = this.bookmark.instance.items; - items.forEach((item) => { - retrieveLookupItem(item, itemDoms); - }); - } - onMount() { - var _a; - const setBookmarkIcon = () => { - const nodesWithPath = {}; - this.computeNodesWithPath((node, filePath) => { - nodesWithPath[filePath] = node; - }); - Object.entries(nodesWithPath).forEach(([filePath, node]) => this.setIconOrRemove(filePath, node)); - }; - if (obsidian.requireApiVersion('1.7.2')) { - // TODO: Might improve the performance here. - (_a = this.leaf) === null || _a === void 0 ? void 0 : _a.loadIfDeferred().then(setBookmarkIcon); - } - else { - setBookmarkIcon(); - } - } - register() { - if (!this.plugin.app.internalPlugins.getPluginById('file-explorer').enabled) { - console.info(`[${config.PLUGIN_NAME}/Bookmarks] Skipping bookmark internal plugin registration because file-explorer is not enabled.`); - return; - } - if (!this.enabled) { - console.info(`[${config.PLUGIN_NAME}/Bookmarks] Skipping bookmark internal plugin registration because it's not enabled.`); - return; - } - // eslint-disable-next-line - const self = this; - this.plugin.register(around(this.bookmark.instance, { - addItem: function (next) { - return function (...args) { - next.call(this, ...args); - // TODO: Remove in the future, I could not think of a better way to do this. - setTimeout(() => { - self.onMount(); - }, 1000); - }; - }, - removeItem: function (next) { - return function (...args) { - next.call(this, ...args); - self.onMount(); - }; - }, - })); - } -} - -// TODO: Optimize the code to reduce the number of iterations and improve the -// performance. -const createIconShortcodeRegex = (plugin) => { - return new RegExp(`(${plugin.getSettings().iconIdentifier})((\\w{1,64}:\\d{17,18})|(\\w{1,64}))(${plugin.getSettings().iconIdentifier})`, 'g'); -}; -const createTreeWalker = (plugin, root) => { - return document.createTreeWalker(root, NodeFilter.SHOW_ALL, { - acceptNode: function (node) { - if (node.nodeName === 'CODE') { - return NodeFilter.FILTER_REJECT; - } - else if (node.nodeName === '#text') { - if (node.nodeValue && - (emoji.getRegex().test(node.nodeValue) || - createIconShortcodeRegex(plugin).test(node.nodeValue))) { - return NodeFilter.FILTER_ACCEPT; - } - else { - return NodeFilter.FILTER_REJECT; - } - } - return NodeFilter.FILTER_SKIP; - }, - }); -}; -const checkForTextNodes = (treeWalker, match, cb) => { - let currentNode = treeWalker.currentNode; - while (currentNode) { - if (currentNode.nodeType === Node.TEXT_NODE) { - const text = currentNode; - const textNodes = [...Array.from(text.parentElement.childNodes)].filter((n) => n instanceof Text); - for (const text of textNodes) { - for (const code of [...text.wholeText.matchAll(match)] - .sort((a, b) => b.index - a.index) - .map((arr) => ({ text: arr[0], index: arr.index }))) { - if (!text.textContent) { - continue; - } - cb(text, code); - } - } - } - currentNode = treeWalker.nextNode(); - } -}; -const processIconInTextMarkdown = (plugin, element) => { - // Ignore if codeblock - const codeElement = element.querySelector('pre > code'); - const callOut = element.querySelector('.callout'); - if (codeElement && !callOut) { - return; - } - const iconTreeWalker = createTreeWalker(plugin, element); - const iconShortcodeRegex = createIconShortcodeRegex(plugin); - const iconIdentifierLength = plugin.getSettings().iconIdentifier.length; - checkForTextNodes(iconTreeWalker, iconShortcodeRegex, (text, code) => { - var _a, _b; - const shortcode = code.text; - const iconName = shortcode.slice(iconIdentifierLength, shortcode.length - iconIdentifierLength); - const iconObject = icon.getIconByName(iconName); - if (iconObject) { - const toReplace = text.splitText(code.index); - const rootSpan = createSpan({ - cls: 'cm-iconize-icon', - attr: { - 'aria-label': iconName, - 'data-icon': iconName, - 'aria-hidden': 'true', - }, - }); - rootSpan.style.display = 'inline-flex'; - rootSpan.style.transform = 'translateY(13%)'; - const parentElement = toReplace.parentElement; - const tagName = (_b = (_a = parentElement === null || parentElement === void 0 ? void 0 : parentElement.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : ''; - let fontSize = calculateFontTextSize(); - if (isHeader(tagName)) { - fontSize = calculateHeaderSize(tagName); - const svgElement = svg.setFontSize(iconObject.svgElement, fontSize); - rootSpan.innerHTML = svgElement; - } - else { - const svgElement = svg.setFontSize(iconObject.svgElement, fontSize); - rootSpan.innerHTML = svgElement; - } - parentElement === null || parentElement === void 0 ? void 0 : parentElement.insertBefore(rootSpan, toReplace); - toReplace.textContent = toReplace.wholeText.substring(code.text.length); - // Set the font size to its parent font size if defined. - // We do this after that to not freeze the insertion while iterating over the tree. - // We are also updating the size after the animation because the styling won't be set - // in the first place. - requestAnimationFrame(() => { - const parentFontSize = parseFloat(getComputedStyle(parentElement).fontSize); - if (!isNaN(parentFontSize)) { - rootSpan.innerHTML = svg.setFontSize(rootSpan.innerHTML, parentFontSize); - } - }); - } - }); - const emojiTreeWalker = createTreeWalker(plugin, element); - checkForTextNodes(emojiTreeWalker, emoji.getRegex(), (text, code) => { - var _a, _b, _c, _d; - if (!emoji.isEmoji(code.text)) { - return; - } - if (plugin.getSettings().emojiStyle === 'twemoji') { - const toReplace = text.splitText(code.index); - const tagName = (_c = (_b = (_a = toReplace.parentElement) === null || _a === void 0 ? void 0 : _a.tagName) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : ''; - let fontSize = calculateFontTextSize(); - if (isHeader(tagName)) { - fontSize = calculateHeaderSize(tagName); - } - const emojiValue = emoji.parseEmoji(plugin.getSettings().emojiStyle, code.text, fontSize); - if (!emojiValue) { - return; - } - const emojiNode = createSpan(); - emojiNode.innerHTML = emojiValue; - (_d = toReplace.parentElement) === null || _d === void 0 ? void 0 : _d.insertBefore(emojiNode, toReplace); - toReplace.textContent = toReplace.wholeText.substring(code.text.length); - } - }); -}; - -const processIconInLinkMarkdown = (plugin, element, ctx) => { - const linkElements = element.querySelectorAll('a'); - if (!linkElements || linkElements.length === 0) { - return; - } - linkElements.forEach((linkElement) => { - var _a, _b, _c, _d, _e; - // Skip if the link element e.g., is a tag. - if (!linkElement.hasAttribute('data-href')) { - return; - } - const linkHref = linkElement.getAttribute('href'); - if (!linkHref) { - logger.warn('Link element does not have an `href` attribute'); - return; - } - const file = plugin.app.metadataCache.getFirstLinkpathDest(linkHref, ctx.sourcePath); - if (!file) { - logger.warn('Link element does not have a linkpath to a file'); - return; - } - const path = file.path; - const iconValue = icon.getIconByPath(plugin, path); - if (!iconValue) { - return; - } - let fontSize = calculateFontTextSize(); - const tagName = (_c = (_b = (_a = linkElement.parentElement) === null || _a === void 0 ? void 0 : _a.tagName) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : ''; - if (isHeader(tagName)) { - fontSize = calculateHeaderSize(tagName); - } - const iconName = typeof iconValue === 'string' - ? iconValue - : iconValue.prefix + iconValue.name; - const rootSpan = createSpan({ - cls: 'iconize-icon-in-link', - attr: { - title: iconName, - 'aria-label': iconName, - 'data-icon': iconName, - 'aria-hidden': 'true', - }, - }); - rootSpan.style.color = - (_d = plugin.getIconColor(path)) !== null && _d !== void 0 ? _d : plugin.getSettings().iconColor; - if (emoji.isEmoji(iconName)) { - const parsedEmoji = (_e = emoji.parseEmoji(plugin.getSettings().emojiStyle, iconName, fontSize)) !== null && _e !== void 0 ? _e : iconName; - rootSpan.style.transform = 'translateY(0)'; - rootSpan.innerHTML = parsedEmoji; - } - else { - let svgEl = icon.getIconByName(iconName).svgElement; - svgEl = svg.setFontSize(svgEl, fontSize); - if (svgEl) { - rootSpan.style.transform = 'translateY(20%)'; - rootSpan.innerHTML = svgEl; - } - } - linkElement.prepend(rootSpan); - }); -}; - -const TREE_ITEM_CLASS = 'tree-item-self'; -const TREE_ITEM_INNER = 'tree-item-inner'; -class OutlineInternalPlugin extends InternalPluginInjector { - constructor(plugin) { - super(plugin); - } - onMount() { - // TODO: Might improve the performance here. - } - register() { - if (!this.enabled) { - logger.info('Skipping internal plugin registration because it is not enabled.', LoggerPrefix.Outline); - return; - } - const updateTreeItems = () => { - var _a, _b, _c; - if (!((_b = (_a = this.leaf) === null || _a === void 0 ? void 0 : _a.view) === null || _b === void 0 ? void 0 : _b.tree)) { - return; - } - const treeItems = Array.from(this.leaf.view.tree.containerEl.querySelectorAll(`.${TREE_ITEM_CLASS}`)); - for (const treeItem of treeItems) { - const treeItemInner = treeItem.querySelector(`.${TREE_ITEM_INNER}`); - let text = treeItemInner === null || treeItemInner === void 0 ? void 0 : treeItemInner.getText(); - if (!text) { - continue; - } - const iconShortcodeRegex = createIconShortcodeRegex(this.plugin); - const iconIdentifierLength = this.plugin.getSettings().iconIdentifier.length; - let trimmedLength = 0; - for (const code of [...text.matchAll(iconShortcodeRegex)] - .sort((a, b) => a.index - b.index) - .map((arr) => ({ text: arr[0], index: arr.index }))) { - const shortcode = code.text; - const iconName = shortcode.slice(iconIdentifierLength, shortcode.length - iconIdentifierLength); - const iconObject = icon.getIconByName(iconName); - if (iconObject) { - const startIndex = code.index - trimmedLength; - const endIndex = code.index + code.text.length - trimmedLength; - const str = text.substring(0, startIndex) + text.substring(endIndex); - const iconSpan = createSpan({ - cls: 'cm-iconize-icon', - attr: { - 'aria-label': iconName, - 'data-icon': iconName, - 'aria-hidden': 'true', - }, - }); - const fontSize = parseFloat((_c = getComputedStyle(document.body).getPropertyValue('--nav-item-size')) !== null && _c !== void 0 ? _c : '16'); - const svgElement = svg.setFontSize(iconObject.svgElement, fontSize); - iconSpan.style.display = 'inline-flex'; - iconSpan.style.transform = 'translateY(13%)'; - iconSpan.innerHTML = svgElement; - treeItemInner.innerHTML = treeItemInner.innerHTML.replace(shortcode, iconSpan.outerHTML); - text = str; - trimmedLength += code.text.length; - } - } - } - }; - const setOutlineIcons = () => { - this.plugin.getEventEmitter().once('allIconsLoaded', () => { - updateTreeItems(); - const callback = (mutations) => { - mutations.forEach((mutation) => { - if (mutation.type !== 'childList') { - return; - } - const addedNodes = mutation.addedNodes; - if (addedNodes.length === 0) { - return; - } - updateTreeItems(); - }); - if (!this.enabled) { - observer.disconnect(); - } - }; - const observer = new MutationObserver(callback); - observer.observe(this.leaf.view.tree.containerEl, { - childList: true, - subtree: true, - }); - }); - }; - if (obsidian.requireApiVersion('1.7.2')) { - // TODO: Might improve the performance here. - this.leaf.loadIfDeferred().then(setOutlineIcons); - } - else { - setOutlineIcons(); - } - } - get leaf() { - const leaf = this.plugin.app.workspace.getLeavesOfType('outline'); - if (!leaf) { - logger.log('`leaf` in outline is undefined', LoggerPrefix.Outline); - return undefined; - } - if (leaf.length === 0) { - logger.log('`leaf` length in outline is 0', LoggerPrefix.Outline); - return undefined; - } - return leaf[0]; - } - get outline() { - return this.plugin.app.internalPlugins.getPluginById('outline'); - } - get enabled() { - return this.plugin.app.internalPlugins.getPluginById('outline').enabled; - } -} - -class SuggestionIcon extends obsidian.EditorSuggest { - constructor(app, plugin) { - super(app); - this.plugin = plugin; - } - onTrigger(cursor, editor) { - // Isolate shortcode starting position closest to the cursor. - const shortcodeStart = editor - .getLine(cursor.line) - .substring(0, cursor.ch) - .lastIndexOf(this.plugin.getSettings().iconIdentifier); - // `onTrigger` needs to return `null` as soon as possible to save processing performance. - if (shortcodeStart === -1) { - return null; - } - // Regex for checking if the shortcode is not done yet. - const regex = new RegExp(`^(${this.plugin.getSettings().iconIdentifier})\\w+$`, 'g'); - const regexOngoingShortcode = editor - .getLine(cursor.line) - .substring(shortcodeStart, cursor.ch) - .match(regex); - if (regexOngoingShortcode === null) { - return null; - } - const startingIndex = editor - .getLine(cursor.line) - .indexOf(regexOngoingShortcode[0]); - return { - start: { - line: cursor.line, - ch: startingIndex, - }, - end: { - line: cursor.line, - ch: startingIndex + regexOngoingShortcode[0].length, - }, - query: regexOngoingShortcode[0], - }; - } - getSuggestions(context) { - const queryLowerCase = context.query - .substring(this.plugin.getSettings().iconIdentifier.length) - .toLowerCase(); - // Store all icons corresponding to the current query. - const iconsNameArray = getAllLoadedIconNames() - .filter((iconObject) => { - const name = iconObject.prefix.toLowerCase() + iconObject.name.toLowerCase(); - return name.toLowerCase().includes(queryLowerCase); - }) - .map((iconObject) => iconObject.prefix + iconObject.name); - // Store all emojis correspoding to the current query - parsing whitespaces and - // colons for shortcodes compatibility. - const emojisNameArray = Object.keys(emoji.shortNames).filter((e) => { var _a; return (_a = emoji.getShortcode(e)) === null || _a === void 0 ? void 0 : _a.includes(queryLowerCase); }); - return [...iconsNameArray, ...emojisNameArray]; - } - renderSuggestion(value, el) { - const iconObject = icon.getIconByName(value); - el.style.display = 'flex'; - el.style.alignItems = 'center'; - el.style.gap = '0.25rem'; - if (iconObject) { - // Suggest an icon. - el.innerHTML = `${iconObject.svgElement} ${value}`; - } - else { - // Suggest an emoji - display its shortcode version. - const shortcode = emoji.getShortcode(value); - if (shortcode) { - el.innerHTML = `${value} ${shortcode}`; - } - } - } - selectSuggestion(value) { - const isEmoji = emoji.isEmoji(value.replace(/_/g, ' ')); - if (!isEmoji) { - saveIconToIconPack(this.plugin, value); - } - // Replace query with iconNameWithPrefix or emoji unicode directly. - const updatedValue = isEmoji - ? value - : `${this.plugin.getSettings().iconIdentifier}${value}${this.plugin.getSettings().iconIdentifier}`; - this.context.editor.replaceRange(updatedValue, this.context.start, this.context.end); - } -} - -class IconInTextWidget extends view.WidgetType { - constructor(plugin, id) { - super(); - this.plugin = plugin; - this.id = id; - this.start = -1; - this.end = -1; - } - setPosition(start, end) { - this.start = start; - this.end = end; - } - eq(other) { - return other instanceof IconInTextWidget && other.id === this.id; - } - getSize(view) { - let fontSize = calculateFontTextSize(); - const line = view.state.doc.lineAt(this.end); - const headerMatch = line.text.match(/^#{1,6}\s/); - if (headerMatch && headerMatch[0].trim()) { - const mapping = { - '#': 'h1', - '##': 'h2', - '###': 'h3', - '####': 'h4', - '#####': 'h5', - '######': 'h6', - }; - const header = mapping[headerMatch[0].trim()]; - fontSize = calculateHeaderSize(header); - } - return fontSize; - } - toDOM(view) { - const wrap = createSpan({ - cls: 'cm-iconize-icon', - attr: { - 'aria-label': this.id, - 'data-icon': this.id, - 'aria-hidden': 'true', - }, - }); - const foundIcon = icon.getIconByName(this.id); - const fontSize = this.getSize(view); - if (foundIcon) { - const svgElement = svg.setFontSize(foundIcon.svgElement, fontSize); - wrap.style.display = 'inline-flex'; - wrap.style.transform = 'translateY(13%)'; - wrap.innerHTML = svgElement; - } - else if (emoji.isEmoji(this.id)) { - wrap.innerHTML = emoji.parseEmoji(this.plugin.getSettings().emojiStyle, this.id, fontSize); - } - else { - wrap.append(`${this.plugin.getSettings().iconIdentifier}${this.id}${this.plugin.getSettings().iconIdentifier}`); - } - return wrap; - } - ignoreEvent() { - return false; - } -} - -class IconInLinkWidget extends view.WidgetType { - constructor(plugin, iconData, path, headerType) { - super(); - this.plugin = plugin; - this.iconData = iconData; - this.path = path; - this.headerType = headerType; - } - toDOM() { - var _a; - const iconNode = document.createElement('span'); - const iconName = typeof this.iconData === 'string' - ? this.iconData - : this.iconData.prefix + this.iconData.name; - iconNode.style.color = - (_a = this.plugin.getIconColor(this.path)) !== null && _a !== void 0 ? _a : this.plugin.getSettings().iconColor; - iconNode.setAttribute('title', iconName); - iconNode.classList.add('iconize-icon-in-link'); - if (typeof this.iconData === 'string') { - iconNode.style.transform = 'translateY(0)'; - } - let innerHTML = typeof this.iconData === 'string' - ? this.iconData - : this.iconData.svgElement; - let fontSize = calculateFontTextSize(); - if (this.headerType) { - fontSize = calculateHeaderSize(this.headerType); - } - if (emoji.isEmoji(innerHTML)) { - innerHTML = emoji.parseEmoji(this.plugin.getSettings().emojiStyle, innerHTML, fontSize); - } - else { - innerHTML = svg.setFontSize(innerHTML, fontSize); - } - iconNode.innerHTML = innerHTML; - return iconNode; - } - ignoreEvent() { - return true; - } -} - -const buildLinkDecorations = (view$1, plugin) => { - const builder = new state.RangeSetBuilder(); - const mdView = view$1.state.field(obsidian.editorInfoField); - for (const { from, to } of view$1.visibleRanges) { - language.syntaxTree(view$1.state).iterate({ - from, - to, - enter: (node) => { - const tokenProps = node.type.prop(language.tokenClassNodeProp); - if (tokenProps) { - const props = new Set(tokenProps.split(' ')); - const isLink = props.has('hmd-internal-link'); - const headerType = [ - 'header-1', - 'header-2', - 'header-3', - 'header-4', - 'header-5', - 'header-6', - ].find((header) => props.has(header)); - if (isLink) { - let linkText = view$1.state.doc.sliceString(node.from, node.to); - linkText = linkText.split('#')[0]; - const file = plugin.app.metadataCache.getFirstLinkpathDest(linkText, mdView.file.basename); - if (file) { - const possibleIcon = icon.getIconByPath(plugin, file.path); - if (possibleIcon) { - const iconDecoration = view.Decoration.widget({ - widget: new IconInLinkWidget(plugin, possibleIcon, file.path, headerType), - }); - builder.add(node.from, node.from, iconDecoration); - } - } - } - } - }, - }); - } - return builder.finish(); -}; - -const buildTextDecorations = (view$1, plugin) => { - const ranges = []; - const iconInfo = view$1.state.field(plugin.positionField); - for (const { from, to } of view$1.visibleRanges) { - iconInfo.between(from - 1, to + 1, (from, to, { iconId }) => { - ranges.push([iconId, from, to]); - }); - } - return view.Decoration.set(ranges.map(([code, from, to]) => { - const widget = new IconInTextWidget(plugin, code); - widget.setPosition(from, to); - if (view$1.state.field(obsidian.editorLivePreviewField)) { - return view.Decoration.replace({ - widget, - side: -1, - }).range(from, to); - } - return view.Decoration.widget({ - widget, - side: -1, - }).range(to); - }), true); -}; - -const buildIconInTextPlugin = (plugin) => { - return view.ViewPlugin.fromClass(class IconPlugin { - constructor(view) { - this.plugin = plugin; - this.decorations = buildTextDecorations(view, plugin); - } - update(update) { - this.decorations = buildTextDecorations(update.view, this.plugin); - } - }, { - decorations: (v) => v.decorations, - provide: (plugin) => view.EditorView.atomicRanges.of((view$1) => { - const value = view$1.plugin(plugin); - return value ? value.decorations : view.Decoration.none; - }), - }); -}; - -const buildIconInLinksPlugin = (plugin) => { - return view.ViewPlugin.fromClass(class { - constructor(view) { - this.plugin = plugin; - this.decorations = buildLinkDecorations(view, plugin); - } - destroy() { } - update(update) { - if (update.docChanged || update.viewportChanged) { - this.decorations = buildLinkDecorations(update.view, this.plugin); - } - } - }, { - decorations: (v) => v.decorations, - }); -}; - -// TODO: Optimize the code to reduce the number of iterations and improve the -// performance. -function checkForSourceMode(plugin) { - let isSourceMode = false; - // Iterate over all leaves to check if any is in source mode - plugin.app.workspace.iterateAllLeaves((leaf) => { - var _a; - if (!isSourceMode && leaf.view.getViewType() === 'markdown') { - if ((_a = leaf.getViewState().state) === null || _a === void 0 ? void 0 : _a.source) { - isSourceMode = true; - } - } - }); - return isSourceMode; -} -class IconPosition extends state.RangeValue { - constructor(text) { - super(); - this.text = text; - } - get iconId() { - return this.text; - } - eq(other) { - return other instanceof IconPosition && other.text === this.text; - } -} -/** - * Builds a position field for the editor state. This field will track the - * positions of the icons in the document. - **/ -const buildPositionField = (plugin) => { - /** - * Checks the ranges of the icons in the document. If the range is not - * excluded, the range is added to the range set. If the range is excluded, - * the range is removed from the range set. - * @param state EditorState to get the ranges from. - * @param excludeFrom Number to exclude from the ranges. - * @param excludeTo Number to exclude to the ranges. - * @param updateRange Function callback to update the range. - */ - const checkRanges = (state, excludeFrom, excludeTo, updateRange) => { - const isSourceMode = checkForSourceMode(plugin); - const text = state.doc.sliceString(0, state.doc.length); - const identifier = plugin.getSettings().iconIdentifier; - const regex = new RegExp(`(${identifier})((\\w{1,64}:\\d{17,18})|(\\w{1,64}))(${identifier})`, 'g'); - for (const { 0: rawCode, index: offset } of text.matchAll(regex)) { - const iconName = rawCode.substring(identifier.length, rawCode.length - identifier.length); - if (!icon.getIconByName(iconName)) { - continue; - } - const from = offset; - const to = offset + rawCode.length; - if (!isNodeInRangeAccepted(state, from, to)) { - continue; - } - if (offset < excludeFrom || offset > excludeTo) { - updateRange(from, to, new IconPosition(iconName), isSourceMode); - continue; - } - updateRange(from, to, new IconPosition(iconName), true); - } - for (const { 0: emojiName, index: offset } of text.matchAll(emoji.getRegex())) { - if (!emoji.isEmoji(emojiName)) { - continue; - } - const from = offset; - const to = offset + emojiName.length; - if (!isNodeInRangeAccepted(state, from, to)) { - continue; - } - if (offset < excludeFrom || offset > excludeTo) { - updateRange(from, to, new IconPosition(emojiName), isSourceMode); - continue; - } - updateRange(from, to, new IconPosition(emojiName), true); - } - }; - const isNodeInRangeAccepted = (state, from, to) => { - let isRangeAccepted = true; - language.syntaxTree(state).iterate({ - from, - to, - enter: ({ type }) => { - var _a; - if (type.name === 'Document') { - return; - } - const allowedNodeTypes = [ - 'header', - 'strong', - 'em', - 'quote', - 'link', - 'list-1', - 'list-2', - 'list-3', - 'highlight', - 'footref', - 'comment', - 'link-alias', - ]; - const excludedNodeTypes = [ - 'formatting', - 'hmd-codeblock', - 'inline-code', - 'hr', - ]; - const nodeProps = (_a = type.prop(language.tokenClassNodeProp)) !== null && _a !== void 0 ? _a : ''; - const s = new Set(nodeProps.split(' ')); - if (excludedNodeTypes.some((t) => s.has(t)) && - allowedNodeTypes.every((t) => !s.has(t))) { - isRangeAccepted = false; - } - }, - }); - return isRangeAccepted; - }; - return state.StateField.define({ - create: (state$1) => { - const rangeSet = new state.RangeSetBuilder(); - const changedLines = []; - checkRanges(state$1, -1, -1, (from, to, iconPosition) => { - changedLines.push({ from, to, iconPosition }); - }); - changedLines.sort((a, b) => a.from - b.from); - for (const { from, to, iconPosition } of changedLines) { - rangeSet.add(from, to, iconPosition); - } - return rangeSet.finish(); - }, - update: (rangeSet, transaction) => { - const newRanges = []; - if (!transaction.docChanged) { - if (transaction.selection) { - const from = transaction.selection.ranges[0].from; - const to = transaction.selection.ranges[0].to; - const lineEnd = transaction.state.doc.lineAt(to).length; - const lineStart = transaction.state.doc.lineAt(from).from; - // Checks the ranges of the icons in the document except for the - // excluded line start and end. - checkRanges(transaction.state, lineStart, lineStart + lineEnd, (from, to, value, removed) => { - rangeSet = rangeSet.update({ - filterFrom: from, - filterTo: to, - filter: () => false, - }); - if (!removed) { - newRanges.push(value.range(from, to)); - } - }); - } - else { - checkRanges(transaction.state, -1, -1, (from, to, value, removed) => { - rangeSet = rangeSet.update({ - filterFrom: from, - filterTo: to, - filter: () => false, - }); - if (!removed) { - newRanges.push(value.range(from, to)); - } - }); - } - newRanges.sort((a, b) => a.from - b.from); - rangeSet = rangeSet.update({ add: newRanges }); - return rangeSet; - } - rangeSet = rangeSet.map(transaction.changes); - const changedLines = []; - transaction.changes.iterChangedRanges((_f, _t, from, to) => { - changedLines.push([ - transaction.state.doc.lineAt(from).number, - transaction.state.doc.lineAt(to).number, - ]); - }); - for (const [start, end] of changedLines) { - const from = transaction.state.doc.line(start).from; - const to = transaction.state.doc.line(end).to; - rangeSet = rangeSet.update({ - filterFrom: from, - filterTo: to, - filter: () => false, - }); - const lineEnd = transaction.state.doc.line(end).length; - const lineStart = transaction.state.doc.line(end).from; - // Checks the ranges of the icons in the document except for the excluded - // line start and end. - checkRanges(transaction.state, lineStart, lineStart + lineEnd, (from, to, value, removed) => { - if (!removed) { - newRanges.push(value.range(from, to)); - } - }); - } - newRanges.sort((a, b) => a.from - b.from); - rangeSet = rangeSet.update({ add: newRanges }); - return rangeSet; - }, - }); -}; - -class ChangeColorModal extends obsidian.Modal { - constructor(app, plugin, path) { - var _a; - super(app); - this.plugin = plugin; - this.path = path; - this.usedColor = this.plugin.getIconColor(this.path); - this.contentEl.style.display = 'block'; - this.modalEl.classList.add('iconize-custom-modal'); - this.titleEl.setText('Change color'); - const description = this.contentEl.createEl('p', { - text: 'Select a color for this icon', - cls: 'setting-item-description', - }); - description.style.marginBottom = 'var(--size-2-2)'; - const colorContainer = this.contentEl.createDiv(); - colorContainer.style.display = 'flex'; - colorContainer.style.alignItems = 'center'; - colorContainer.style.justifyContent = 'space-between'; - const colorPicker = new obsidian.ColorComponent(colorContainer) - .setValue((_a = this.usedColor) !== null && _a !== void 0 ? _a : '#000000') - .onChange((value) => { - this.usedColor = value; - }); - const defaultColorButton = new obsidian.ButtonComponent(colorContainer); - defaultColorButton.setTooltip('Set color to the default one'); - defaultColorButton.setButtonText('Reset'); - defaultColorButton.onClick(() => { - colorPicker.setValue('#000000'); - this.usedColor = undefined; - }); - // Save button. - const button = new obsidian.ButtonComponent(this.contentEl); - button.buttonEl.style.marginTop = 'var(--size-4-4)'; - button.buttonEl.style.float = 'right'; - button.setButtonText('Save Changes'); - button.onClick(() => __awaiter(this, void 0, void 0, function* () { - var _a; - new obsidian.Notice('Color of icon changed.'); - if (this.usedColor) { - this.plugin.addIconColor(this.path, this.usedColor); - } - else { - this.plugin.removeIconColor(this.path); - } - // Refresh the DOM. - const iconNode = dom.getIconNodeFromPath(this.path); - iconNode.style.color = (_a = this.usedColor) !== null && _a !== void 0 ? _a : null; - const colorizedInnerHtml = svg.colorize(iconNode.innerHTML, this.usedColor); - iconNode.innerHTML = colorizedInnerHtml; - this.close(); - })); - } - onOpen() { - super.onOpen(); - } - onClose() { - const { contentEl } = this; - contentEl.empty(); - } -} - -class EventEmitter { - constructor() { - this.listeners = {}; - } - on(type, listener, priority = 0) { - var _a, _b; - var _c; - (_a = (_c = this.listeners)[type]) !== null && _a !== void 0 ? _a : (_c[type] = []); - (_b = this.listeners[type]) === null || _b === void 0 ? void 0 : _b.push({ listener, once: false, priority }); - this.sortListeners(type); - } - once(type, listener, priority = 0) { - var _a, _b; - var _c; - (_a = (_c = this.listeners)[type]) !== null && _a !== void 0 ? _a : (_c[type] = []); - (_b = this.listeners[type]) === null || _b === void 0 ? void 0 : _b.push({ listener, once: true, priority }); - this.sortListeners(type); - } - off(type, listener) { - var _a; - if (!this.listeners[type]) { - return; - } - this.listeners[type] = (_a = this.listeners[type]) === null || _a === void 0 ? void 0 : _a.filter((entry) => entry.listener !== listener); - } - emit(type, payload) { - const listeners = this.listeners[type]; - if (!listeners) { - return; - } - const event = { payload }; - listeners.slice().forEach((entry) => { - entry.listener(event); - if (entry.once) { - this.off(type, entry.listener); - } - }); - } - sortListeners(type) { - var _a; - if (this.listeners[type]) { - (_a = this.listeners[type]) === null || _a === void 0 ? void 0 : _a.sort((a, b) => b.priority - a.priority); - } - } -} - -function getApi(plugin) { - return { - getEventEmitter: () => plugin.getEventEmitter(), - getIconByName: (iconNameWithPrefix) => icon.getIconByName(iconNameWithPrefix), - setIconForNode: (iconName, node, color) => dom.setIconForNode(plugin, iconName, node, { color }), - saveIconToIconPack: (iconNameWithPrefix) => saveIconToIconPack(plugin, iconNameWithPrefix), - removeIconFromIconPack: (iconNameWithPrefix) => removeIconFromIconPack(plugin, iconNameWithPrefix), - getIconsFromIconPack: getIconsFromIconPack, - getAllIconPacks: getAllIconPacks, - doesElementHasIconNode: dom.doesElementHasIconNode, - getIconFromElement: dom.getIconFromElement, - removeIconInNode: dom.removeIconInNode, - removeIconInPath: dom.removeIconInPath, - util: { - dom, - svg, - }, - version: { - get current() { - return plugin.manifest.version; - }, - }, - }; -} - -class IconizePlugin extends obsidian.Plugin { - constructor() { - super(...arguments); - this.registeredFileExplorers = new Set(); - this.modifiedInternalPlugins = []; - this.positionField = buildPositionField(this); - this.frontmatterCache = new Set(); - this.eventEmitter = new EventEmitter(); - this.api = getApi(this); - } - onload() { - return __awaiter(this, void 0, void 0, function* () { - console.log(`loading ${config.PLUGIN_NAME}`); - yield this.loadIconFolderData(); - logger.toggleLogging(this.getSettings().debugMode); - setPath(this.getSettings().iconPacksPath); - if (this.getSettings().useInternalPlugins) { - // Registers all modified internal plugins. - // Only adds star plugin for obsidian under v0.12.6. - if (!obsidian.requireApiVersion('0.12.6')) { - this.modifiedInternalPlugins.push(new StarredInternalPlugin(this)); - } - else if (obsidian.requireApiVersion('1.2.0')) { - this.modifiedInternalPlugins.push(new BookmarkInternalPlugin(this)); - } - this.modifiedInternalPlugins.push(new OutlineInternalPlugin(this)); - } - yield createDefaultDirectory(this); - yield this.checkRecentlyUsedIcons(); - yield migrate(this); - const usedIconNames = icon.getAllWithPath(this).map((value) => value.icon); - if (!this.doesUseCustomLucideIconPack()) { - addLucideIconsPack(this); - } - yield loadUsedIcons(this, usedIconNames); - this.app.workspace.onLayoutReady(() => this.handleChangeLayout()); - this.addCommand({ - id: 'iconize:set-icon-for-file', - name: 'Set icon for file', - hotkeys: [ - { - modifiers: ['Mod', 'Shift'], - key: 'j', - }, - ], - editorCallback: (editor) => __awaiter(this, void 0, void 0, function* () { - var _a; - const file = (_a = editor.editorComponent) === null || _a === void 0 ? void 0 : _a.file; - if (!file) { - logger.warn(`'editor.editorComponent?.file' is undefined for file: ${file}`); - return; - } - const modal = new IconsPickerModal(this.app, this, file.path); - modal.open(); - modal.onSelect = (iconName) => { - IconCache.getInstance().set(file.path, { - iconNameWithPrefix: iconName, - }); - // Update icon in tab when setting is enabled. - if (this.getSettings().iconInTabsEnabled) { - const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path); - for (const tabLeaf of tabLeaves) { - iconTabs.update(this, iconName, tabLeaf.tabHeaderInnerIconEl); - } - } - // Update icon in title when setting is enabled. - if (this.getSettings().iconInTitleEnabled) { - this.addIconInTitle(iconName); - } - }; - }), - }); - this.registerEvent( - // Registering file menu event for listening to file pinning and unpinning. - this.app.workspace.on('file-menu', (menu, file) => { - // I've researched other ways of doing this. However, there is no other way to listen to file pinning and unpinning. - menu.onHide(() => { - const path = file.path; - if (this.getSettings().iconInTabsEnabled) { - for (const openedFile of getAllOpenedFiles(this)) { - if (openedFile.path === path) { - const possibleIcon = IconCache.getInstance().get(path); - if (!possibleIcon) { - return; - } - const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path); - for (const tabLeaf of tabLeaves) { - // Add timeout to ensure that the default icon is already set. - setTimeout(() => { - iconTabs.add(this, file.path, tabLeaf.tabHeaderInnerIconEl); - }, 5); - } - } - } - } - }); - })); - this.registerEvent(this.app.workspace.on('layout-change', () => this.handleChangeLayout())); - this.registerEvent(this.app.workspace.on('file-menu', (menu, file) => { - const addIconMenuItem = (item) => { - item.setTitle('Change icon'); - item.setIcon('hashtag'); - item.onClick(() => { - const modal = new IconsPickerModal(this.app, this, file.path); - modal.open(); - modal.onSelect = (iconName) => { - IconCache.getInstance().set(file.path, { - iconNameWithPrefix: iconName, - }); - // Update icon in tab when setting is enabled. - if (this.getSettings().iconInTabsEnabled) { - const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path); - for (const tabLeaf of tabLeaves) { - iconTabs.update(this, iconName, tabLeaf.tabHeaderInnerIconEl); - } - } - // Update icon in title when setting is enabled. - if (this.getSettings().iconInTitleEnabled) { - this.addIconInTitle(iconName); - } - }; - }); - }; - const removeIconMenuItem = (item) => { - item.setTitle('Remove icon'); - item.setIcon('trash'); - item.onClick(() => __awaiter(this, void 0, void 0, function* () { - yield this.removeSingleIcon(file); - })); - }; - const changeColorOfIcon = (item) => { - item.setTitle('Change color of icon'); - item.setIcon('palette'); - item.onClick(() => { - const modal = new ChangeColorModal(this.app, this, file.path); - modal.open(); - }); - }; - menu.addItem(addIconMenuItem); - const filePathData = this.getData()[file.path]; - const hasNestedIcon = typeof filePathData === 'object' && - filePathData.iconName !== null; - // Only add remove icon menu item when the file path exists in the data. - // We do not want to show this menu item for e.g. custom rules. - if (filePathData && - (typeof filePathData === 'string' || hasNestedIcon)) { - const icon = typeof filePathData === 'string' - ? filePathData - : filePathData.iconName; - if (!emoji.isEmoji(icon)) { - menu.addItem(changeColorOfIcon); - } - menu.addItem(removeIconMenuItem); - } - })); - // deleting event - this.registerEvent(this.app.vault.on('delete', (file) => { - const path = file.path; - this.removeFolderIcon(path); - })); - // renaming event - this.registerEvent(this.app.vault.on('rename', (file, oldPath) => { - // Check if the file was moved and had an icon before. - const dataPoint = this.data[oldPath]; - if (dataPoint && oldPath !== 'settings') { - const iconNameWithPrefix = typeof dataPoint === 'object' - ? dataPoint.iconName - : dataPoint; - dom.createIconNode(this, file.path, iconNameWithPrefix); - } - this.renameFolder(file.path, oldPath); - })); - if (this.getSettings().iconsInNotesEnabled) { - this.registerMarkdownPostProcessor((el) => processIconInTextMarkdown(this, el)); - this.registerEditorSuggest(new SuggestionIcon(this.app, this)); - this.registerEditorExtension([ - this.positionField, - buildIconInTextPlugin(this), - ]); - } - if (this.getSettings().iconsInLinksEnabled) { - this.registerMarkdownPostProcessor((el, ctx) => processIconInLinkMarkdown(this, el, ctx)); - this.registerEditorExtension([ - this.positionField, - buildIconInLinksPlugin(this), - ]); - } - this.addSettingTab(new IconFolderSettings(this.app, this)); - }); - } - notifyPlugins() { - this.modifiedInternalPlugins.forEach((internalPlugin) => { - if (internalPlugin.enabled) { - internalPlugin.onMount(); - } - }); - } - removeSingleIcon(file) { - return __awaiter(this, void 0, void 0, function* () { - this.removeFolderIcon(file.path); - dom.removeIconInPath(file.path); - IconCache.getInstance().invalidate(file.path); - this.notifyPlugins(); - let didUpdate = false; - // Refreshes the icon tab and title icon for custom rules. - for (const rule of customRule.getSortedRules(this)) { - const applicable = yield customRule.isApplicable(this, rule, file.path); - if (applicable) { - customRule.add(this, rule, file); - this.addIconInTitle(rule.icon); - const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path); - for (const tabLeaf of tabLeaves) { - iconTabs.add(this, file.path, tabLeaf.tabHeaderInnerIconEl, { - iconName: rule.icon, - }); - } - didUpdate = true; - break; - } - } - // Only remove icon above titles and icon in tabs if no custom rule was found. - if (!didUpdate) { - // Refreshes icons above title and icons in tabs. - for (const openedFile of getAllOpenedFiles(this)) { - if (this.getSettings().iconInTitleEnabled) { - titleIcon.remove(openedFile.leaf.view.inlineTitleEl); - } - if (this.getSettings().iconInTabsEnabled) { - const leaf = openedFile.leaf; - iconTabs.remove(leaf.tabHeaderInnerIconEl, { - replaceWithDefaultIcon: true, - }); - } - } - } - }); - } - handleChangeLayout() { - // Transform data that are objects to single strings. - const data = Object.entries(this.data); - this.modifiedInternalPlugins.forEach((internalPlugin) => { - if (internalPlugin.enabled) { - internalPlugin.onMount(); - internalPlugin.register(); - } - }); - icon.addAll(this, data, this.registeredFileExplorers, () => { - // After initialization of the icon packs, checks the vault for missing icons and - // adds them. - initIconPacks(this).then(() => __awaiter(this, void 0, void 0, function* () { - if (this.getSettings().iconsBackgroundCheckEnabled) { - const data = Object.entries(this.data); - yield icon.checkMissingIcons(this, data); - resetPreloadedIcons(); - } - this.eventEmitter.emit('allIconsLoaded'); - })); - if (this.getSettings().iconInFrontmatterEnabled) { - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile) { - this.frontmatterCache.add(activeFile.path); - } - } - // Adds the title icon to the active leaf view. - if (this.getSettings().iconInTitleEnabled) { - for (const openedFile of getAllOpenedFiles(this)) { - const iconName = icon.getByPath(this, openedFile.path); - const activeView = openedFile.leaf.view; - if (activeView instanceof obsidian.MarkdownView && iconName) { - let possibleIcon = iconName; - if (!emoji.isEmoji(iconName)) { - const iconNextIdentifier = nextIdentifier(iconName); - possibleIcon = getSvgFromLoadedIcon(iconName.substring(0, iconNextIdentifier), iconName.substring(iconNextIdentifier)); - } - if (possibleIcon) { - titleIcon.add(this, activeView.inlineTitleEl, possibleIcon, { - fontSize: calculateInlineTitleSize(), - }); - } - } - } - } - // Register rename event for adding icons with custom rules to the DOM - // when file was moved to another directory. - this.registerEvent(this.app.vault.on('rename', (file, oldPath) => __awaiter(this, void 0, void 0, function* () { - const sortedRules = customRule.getSortedRules(this); - // Removes possible icons from the renamed file. - sortedRules.forEach((rule) => { - if (customRule.doesMatchPath(rule, oldPath)) { - dom.removeIconInPath(file.path); - } - }); - // Adds possible icons to the renamed file. - sortedRules.forEach((rule) => { - if (customRule.doesMatchPath(rule, oldPath)) { - return; - } - customRule.add(this, rule, file, undefined); - }); - // Updates icon tabs for the renamed file. - for (const rule of customRule.getSortedRules(this)) { - const applicable = yield customRule.isApplicable(this, rule, file.path); - if (!applicable) { - continue; - } - const openedFiles = getAllOpenedFiles(this); - const openedFile = openedFiles.find((openedFile) => openedFile.path === file.path); - if (openedFile) { - const leaf = openedFile.leaf; - iconTabs.update(this, rule.icon, leaf.tabHeaderInnerIconEl); - } - break; - } - }))); - // Register `layout-change` event for adding icons to tabs when moving a pane or - // enabling reading mode. - this.registerEvent(this.app.workspace.on('layout-change', () => { - var _a, _b; - if (this.getSettings().iconInTitleEnabled) { - const activeView = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView); - if (activeView) { - const file = activeView.file; - const view = activeView.leaf.view.currentMode - .view; - const iconNameWithPrefix = icon.getByPath(this, file.path); - if (!iconNameWithPrefix) { - titleIcon.hide(view.inlineTitleEl); - return; - } - let foundIcon = iconNameWithPrefix; - if (!emoji.isEmoji(foundIcon)) { - foundIcon = (_a = icon.getIconByName(iconNameWithPrefix)) === null || _a === void 0 ? void 0 : _a.svgElement; - // Check for preloaded icons if no icon was found when the start up was faster - // than the loading of the icons. - if (!foundIcon && getPreloadedIcons().length > 0) { - foundIcon = (_b = getPreloadedIcons().find((icon) => icon.prefix + icon.name === iconNameWithPrefix)) === null || _b === void 0 ? void 0 : _b.svgElement; - } - } - if (foundIcon) { - // Removes the node because the editor markdown content is being rerendered - // when the content mode changes back to editing. - titleIcon.remove(view.inlineTitleEl); - titleIcon.add(this, view.inlineTitleEl, foundIcon, { - fontSize: calculateInlineTitleSize(), - }); - } - } - } - if (!this.getSettings().iconInTabsEnabled) { - return; - } - for (const openedFile of getAllOpenedFiles(this)) { - const leaf = openedFile.leaf; - const iconColor = this.getIconColor(leaf.view.file.path); - iconTabs.add(this, openedFile.path, leaf.tabHeaderInnerIconEl, { - iconColor, - }); - } - })); - // Register `file-open` event for adding icon to title. - this.registerEvent(this.app.workspace.on('file-open', (file) => { - var _a, _b; - if (!this.getSettings().iconInTitleEnabled) { - return; - } - for (const openedFile of getAllOpenedFiles(this)) { - if (!file || !openedFile || openedFile.path !== file.path) { - continue; - } - const leaf = openedFile.leaf.view; - const iconNameWithPrefix = icon.getByPath(this, file.path); - if (!iconNameWithPrefix) { - titleIcon.hide(leaf.inlineTitleEl); - return; - } - let foundIcon = iconNameWithPrefix; - if (!emoji.isEmoji(foundIcon)) { - foundIcon = (_a = icon.getIconByName(iconNameWithPrefix)) === null || _a === void 0 ? void 0 : _a.svgElement; - // Check for preloaded icons if no icon was found when the start up was faster - // than the loading of the icons. - if (!foundIcon && getPreloadedIcons().length > 0) { - foundIcon = (_b = getPreloadedIcons().find((icon) => icon.prefix + icon.name === iconNameWithPrefix)) === null || _b === void 0 ? void 0 : _b.svgElement; - } - } - if (foundIcon) { - titleIcon.add(this, leaf.inlineTitleEl, foundIcon, { - fontSize: calculateInlineTitleSize(), - }); - } - else { - titleIcon.hide(leaf.inlineTitleEl); - } - } - })); - // Register event for frontmatter icon registration. - this.registerEvent(this.app.metadataCache.on('resolve', (file) => __awaiter(this, void 0, void 0, function* () { - if (!this.getSettings().iconInFrontmatterEnabled) { - return; - } - const fileCache = this.app.metadataCache.getFileCache(file); - const iconFrontmatterName = this.getSettings().iconInFrontmatterFieldName; - const iconColorFrontmatterName = this.getSettings().iconColorInFrontmatterFieldName; - if (fileCache === null || fileCache === void 0 ? void 0 : fileCache.frontmatter) { - const { [iconFrontmatterName]: newIconName, [iconColorFrontmatterName]: newIconColor, } = fileCache.frontmatter; - // If `icon` property is empty, we will remove it from the data and remove the icon. - if (!newIconName) { - if (this.frontmatterCache.has(file.path)) { - yield this.removeSingleIcon(file); - this.frontmatterCache.delete(file.path); - } - return; - } - if (typeof newIconName !== 'string') { - new obsidian.Notice(`[${config.PLUGIN_NAME}] Frontmatter property type \`icon\` has to be of type \`text\`.`); - return; - } - if (newIconColor && typeof newIconColor !== 'string') { - new obsidian.Notice(`[${config.PLUGIN_NAME}] Frontmatter property type \`iconColor\` has to be of type \`text\`.`); - return; - } - let iconColor = newIconColor; - if (isHexadecimal(iconColor)) { - iconColor = stringToHex(iconColor); - } - const cachedIcon = IconCache.getInstance().get(file.path); - if (newIconName === (cachedIcon === null || cachedIcon === void 0 ? void 0 : cachedIcon.iconNameWithPrefix) && - iconColor === (cachedIcon === null || cachedIcon === void 0 ? void 0 : cachedIcon.iconColor)) { - return; - } - this.frontmatterCache.add(file.path); - try { - if (!emoji.isEmoji(newIconName)) { - saveIconToIconPack(this, newIconName); - } - } - catch (e) { - logger.warn(`Something went wrong while saving icon to icon pack (error: ${e})`); - new obsidian.Notice(e.message); - return; - } - dom.createIconNode(this, file.path, newIconName, { - color: iconColor, - }); - this.addFolderIcon(file.path, newIconName); - this.addIconColor(file.path, iconColor); - IconCache.getInstance().set(file.path, { - iconNameWithPrefix: newIconName, - iconColor, - }); - // Update icon in tab when setting is enabled. - if (this.getSettings().iconInTabsEnabled) { - const tabLeaves = iconTabs.getTabLeavesOfFilePath(this, file.path); - for (const tabLeaf of tabLeaves) { - iconTabs.update(this, newIconName, tabLeaf.tabHeaderInnerIconEl); - } - } - // Update icon in title when setting is enabled. - if (this.getSettings().iconInTitleEnabled) { - this.addIconInTitle(newIconName); - } - } - }))); - // Register active leaf change event for adding icon of file to tab. - this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => { - if (!this.getSettings().iconInTabsEnabled) { - return; - } - // TODO: Maybe change in the future to a more optimal solution. - // Fixes a problem when the file was clicked twice in the same tab. - // See https://github.com/FlorianWoelki/obsidian-iconize/issues/208. - if (leaf.view.getViewType() === 'file-explorer') { - for (const openedFile of getAllOpenedFiles(this)) { - const leaf = openedFile.leaf; - const iconColor = this.getIconColor(leaf.view.file.path); - iconTabs.add(this, openedFile.path, leaf.tabHeaderInnerIconEl, { - iconColor, - }); - } - return; - } - if (leaf.view.getViewType() !== 'markdown') { - return; - } - const tabHeaderLeaf = leaf; - if (tabHeaderLeaf.view.file) { - const iconColor = this.getIconColor(tabHeaderLeaf.view.file.path); - iconTabs.add(this, tabHeaderLeaf.view.file.path, tabHeaderLeaf.tabHeaderInnerIconEl, { - iconColor, - }); - } - })); - this.registerEvent(this.app.workspace.on('css-change', () => { - for (const openedFile of getAllOpenedFiles(this)) { - const activeView = openedFile.leaf.view; - if (activeView instanceof obsidian.MarkdownView) { - titleIcon.updateStyle(activeView.inlineTitleEl, { - fontSize: calculateInlineTitleSize(), - }); - } - } - })); - }); - } - addIconInTitle(iconName) { - var _a; - for (const openedFile of getAllOpenedFiles(this)) { - const activeView = openedFile.leaf.view; - if (activeView instanceof obsidian.MarkdownView) { - let possibleIcon = iconName; - if (!emoji.isEmoji(iconName)) { - possibleIcon = (_a = icon.getIconByName(iconName)) === null || _a === void 0 ? void 0 : _a.svgElement; - } - if (possibleIcon) { - titleIcon.add(this, activeView.inlineTitleEl, possibleIcon, { - fontSize: calculateInlineTitleSize(), - }); - } - } - } - } - onunload() { - console.log('unloading obsidian-icon-folder'); - } - renameFolder(newPath, oldPath) { - if (!this.data[oldPath] || newPath === oldPath) { - return; - } - Object.defineProperty(this.data, newPath, Object.getOwnPropertyDescriptor(this.data, oldPath)); - delete this.data[oldPath]; - this.saveIconFolderData(); - } - addIconColor(path, iconColor) { - const pathData = this.getData()[path]; - if (typeof pathData === 'string') { - this.getData()[path] = { - iconName: pathData, - iconColor, - }; - } - else { - pathData.iconColor = iconColor; - } - this.saveIconFolderData(); - } - getIconColor(path) { - const pathData = this.getData()[path]; - if (!pathData) { - return undefined; - } - if (typeof pathData === 'string') { - return undefined; - } - return pathData.iconColor; - } - removeIconColor(path) { - const pathData = this.getData()[path]; - if (typeof pathData === 'string') { - return; - } - const currentValue = pathData; - this.getData()[path] = currentValue.iconName; - this.saveIconFolderData(); - } - removeFolderIcon(path) { - if (!this.data[path]) { - return; - } - // Saves the icon name with prefix to remove it from the icon pack directory later. - const iconData = this.data[path]; - delete this.data[path]; - // Removes the icon from the icon pack directory if it is not used as an icon somewhere - // else. - if (iconData) { - let iconNameWithPrefix = iconData; - if (typeof iconData === 'object') { - iconNameWithPrefix = iconData.iconName; - } - else { - iconNameWithPrefix = iconData; - } - if (!emoji.isEmoji(iconNameWithPrefix)) { - removeIconFromIconPack(this, iconNameWithPrefix); - } - } - //this.addIconsToSearch(); - this.saveIconFolderData(); - } - addFolderIcon(path, icon) { - const iconName = getNormalizedName(typeof icon === 'object' ? icon.displayName : icon); - this.data[path] = iconName; - // Update recently used icons. - if (!this.getSettings().recentlyUsedIcons.includes(iconName)) { - if (this.getSettings().recentlyUsedIcons.length >= - this.getSettings().recentlyUsedIconsSize) { - this.getSettings().recentlyUsedIcons = - this.getSettings().recentlyUsedIcons.slice(0, this.getSettings().recentlyUsedIconsSize - 1); - } - this.getSettings().recentlyUsedIcons.unshift(iconName); - this.checkRecentlyUsedIcons(); - } - //this.addIconsToSearch(); - this.saveIconFolderData(); - } - getSettings() { - return this.data.settings; - } - loadIconFolderData() { - return __awaiter(this, void 0, void 0, function* () { - const data = yield this.loadData(); - if (data) { - Object.entries(DEFAULT_SETTINGS).forEach(([k, v]) => { - if (data.settings[k] === undefined) { - data.settings[k] = v; - } - }); - } - this.data = Object.assign({ settings: Object.assign({}, DEFAULT_SETTINGS) }, {}, data); - }); - } - saveIconFolderData() { - return __awaiter(this, void 0, void 0, function* () { - yield this.saveData(this.data); - }); - } - checkRecentlyUsedIcons() { - return __awaiter(this, void 0, void 0, function* () { - if (this.getSettings().recentlyUsedIcons.length > - this.getSettings().recentlyUsedIconsSize) { - this.getSettings().recentlyUsedIcons = - this.getSettings().recentlyUsedIcons.slice(0, this.getSettings().recentlyUsedIconsSize); - yield this.saveIconFolderData(); - } - }); - } - getEventEmitter() { - return this.eventEmitter; - } - getData() { - return this.data; - } - getIconNameFromPath(path) { - if (typeof this.getData()[path] === 'object') { - return this.getData()[path].iconName; - } - return this.getData()[path]; - } - getRegisteredFileExplorers() { - return this.registeredFileExplorers; - } - doesUseCustomLucideIconPack() { - return this.getSettings().lucideIconPackType === 'custom'; - } - doesUseNativeLucideIconPack() { - return this.getSettings().lucideIconPackType === 'native'; - } - /** - * Returns a possible data path by the given value. This function checks for - * direct icon and custom rules. - * @param value String that will be used to find the data path. - * @returns String that is the data path or `undefined` if no data path was found. - */ - getDataPathByValue(value) { - return Object.entries(this.data).find(([k, v]) => { - if (typeof v === 'string') { - if (value === v) { - return k; - } - } - else if (typeof v === 'object') { - // Check for custom rules. - if (k === 'settings') { - // `rules` are defined in the settings object. - const rules = v.rules; - return rules.find((rule) => rule.icon === value); - } - v = v; - if (value === v.iconName) { - return k; - } - } - }); - } -} - -module.exports = IconizePlugin; - - -/* nosourcemap */ \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/manifest.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/manifest.json deleted file mode 100644 index 6e916c3..0000000 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "obsidian-icon-folder", - "name": "Iconize", - "version": "2.14.7", - "minAppVersion": "0.9.12", - "description": "Add icons to anything you desire in Obsidian, including files, folders, and text.", - "author": "Florian Woelki", - "authorUrl": "https://florianwoelki.com/", - "isDesktopOnly": false -} diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/styles.css b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/styles.css deleted file mode 100644 index a74fdd4..0000000 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-icon-folder/styles.css +++ /dev/null @@ -1,120 +0,0 @@ -.iconize-inline-title-wrapper { - width: var(--line-width); - max-width: var(--max-width); - margin-inline: var(--content-margin); -} - -.iconize-title-icon { - max-width: var(--max-width); - margin-right: var(--size-4-2); -} - -.iconize-icon-in-link { - transform: translateY(20%); - margin-right: var(--size-2-2); - display: inline-flex; -} - -.iconize-icon { - border: 1px solid transparent; - margin: 0px 4px 0px 0px; - display: flex; - align-self: center; - margin: auto 0; -} - -.nav-folder-title, -.nav-file-title { - align-items: center; -} - -.iconize-setting input[type='color'] { - margin: 0 6px; -} - -.iconize-modal.prompt-results { - margin: 0; - overflow-y: auto; - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); -} - -.prompt .iconize-subheadline { - margin-top: 12px; - font-size: 12px; - color: gray; - grid-column-start: 1; - grid-column-end: 6; -} - -@media (max-width: 640px) { - .iconize-modal.prompt-results { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - .prompt .iconize-subheadline { - grid-column-end: 4; - } -} - -.iconize-modal.prompt-results .suggestion-item { - cursor: pointer; - white-space: pre-wrap; - display: flex; - justify-content: flex-end; - align-items: center; - flex-direction: column-reverse; - text-align: center; - font-size: 13px; - color: var(--text-muted); - padding: 16px 8px; - line-break: auto; - word-break: break-word; - line-height: 1.3; -} - -.iconize-modal.prompt-results .suggestion-item.suggestion-item__center { - justify-content: center; -} - -.iconize-icon-preview { - font-size: 22px; -} - -.iconize-icon-preview img { - width: 16px; - height: 16px; -} - -.iconize-icon-preview svg { - width: 24px; - height: 24px; - color: currentColor; - margin-bottom: 4px; -} - -.iconize-dragover { - position: relative; -} - -.iconize-dragover-el { - position: absolute; - width: 100%; - height: 100%; - color: var(--text-normal); - background-color: var(--background-secondary-alt); - display: flex; - align-items: center; - justify-content: center; -} - -/* Custom rule modal. */ -.iconize-custom-modal .modal-content { - display: flex; - align-items: center; - justify-content: center; -} - -.iconize-custom-modal .modal-content input { - width: 100%; - margin-right: 0.5rem; -} diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/data.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/data.json new file mode 100644 index 0000000..1d3785b --- /dev/null +++ b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/data.json @@ -0,0 +1,10 @@ +{ + "findText": "\\n{3,}", + "replaceText": "\\n\\n", + "useRegEx": true, + "selOnly": false, + "caseInsensitive": false, + "processLineBreak": false, + "processTab": false, + "prefillFind": false +} \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/main.js b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/main.js new file mode 100644 index 0000000..71658fc --- /dev/null +++ b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/main.js @@ -0,0 +1,322 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ROLLUP +if you want to view the source visit the plugins github repository +*/ + +'use strict'; + +var obsidian = require('obsidian'); + +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +const DEFAULT_SETTINGS = { + findText: '', + replaceText: '', + useRegEx: true, + selOnly: false, + caseInsensitive: false, + processLineBreak: false, + processTab: false, + prefillFind: false +}; +// logThreshold: 0 ... only error messages +// 9 ... verbose output +const logThreshold = 9; +const logger = (logString, logLevel = 0) => { if (logLevel <= logThreshold) + console.log('RegexFiRe: ' + logString); }; +class RegexFindReplacePlugin extends obsidian.Plugin { + onload() { + return __awaiter(this, void 0, void 0, function* () { + logger('Loading Plugin...', 9); + yield this.loadSettings(); + this.addSettingTab(new RegexFindReplaceSettingTab(this.app, this)); + this.addCommand({ + id: 'obsidian-regex-replace', + name: 'Find and Replace using regular expressions', + editorCallback: (editor) => { + new FindAndReplaceModal(this.app, editor, this.settings, this).open(); + }, + }); + }); + } + onunload() { + logger('Bye!', 9); + } + loadSettings() { + return __awaiter(this, void 0, void 0, function* () { + logger('Loading Settings...', 6); + this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData()); + logger(' findVal: ' + this.settings.findText, 6); + logger(' replaceText: ' + this.settings.replaceText, 6); + logger(' caseInsensitive: ' + this.settings.caseInsensitive, 6); + logger(' processLineBreak: ' + this.settings.processLineBreak, 6); + }); + } + saveSettings() { + return __awaiter(this, void 0, void 0, function* () { + yield this.saveData(this.settings); + }); + } +} +class FindAndReplaceModal extends obsidian.Modal { + constructor(app, editor, settings, plugin) { + super(app); + this.editor = editor; + this.settings = settings; + this.plugin = plugin; + } + onOpen() { + const { contentEl, titleEl, editor, modalEl } = this; + modalEl.addClass('find-replace-modal'); + titleEl.setText('Regex Find/Replace'); + const rowClass = 'row'; + const divClass = 'div'; + const noSelection = editor.getSelection() === ''; + let regexFlags = 'gm'; + if (this.settings.caseInsensitive) + regexFlags = regexFlags.concat('i'); + logger('No text selected?: ' + noSelection, 9); + const addTextComponent = (label, placeholder, postfix = '') => { + const containerEl = document.createElement(divClass); + containerEl.addClass(rowClass); + const targetEl = document.createElement(divClass); + targetEl.addClass('input-wrapper'); + const labelEl = document.createElement(divClass); + labelEl.addClass('input-label'); + labelEl.setText(label); + const labelEl2 = document.createElement(divClass); + labelEl2.addClass('postfix-label'); + labelEl2.setText(postfix); + containerEl.appendChild(labelEl); + containerEl.appendChild(targetEl); + containerEl.appendChild(labelEl2); + const component = new obsidian.TextComponent(targetEl); + component.setPlaceholder(placeholder); + contentEl.append(containerEl); + return [component, labelEl2]; + }; + const addToggleComponent = (label, tooltip, hide = false) => { + const containerEl = document.createElement(divClass); + containerEl.addClass(rowClass); + const targetEl = document.createElement(divClass); + targetEl.addClass(rowClass); + const component = new obsidian.ToggleComponent(targetEl); + component.setTooltip(tooltip); + const labelEl = document.createElement(divClass); + labelEl.addClass('check-label'); + labelEl.setText(label); + containerEl.appendChild(labelEl); + containerEl.appendChild(targetEl); + if (!hide) + contentEl.appendChild(containerEl); + return component; + }; + // Create input fields + const findRow = addTextComponent('Find:', 'e.g. (.*)', '/' + regexFlags); + const findInputComponent = findRow[0]; + const findRegexFlags = findRow[1]; + const replaceRow = addTextComponent('Replace:', 'e.g. $1', this.settings.processLineBreak ? '\\n=LF' : ''); + const replaceWithInputComponent = replaceRow[0]; + // Create and show regular expression toggle switch + const regToggleComponent = addToggleComponent('Use regular expressions', 'If enabled, regular expressions in the find field are processed as such, and regex groups might be addressed in the replace field'); + // Update regex-flags label if regular expressions are enabled or disabled + regToggleComponent.onChange(regNew => { + if (regNew) { + findRegexFlags.setText('/' + regexFlags); + } + else { + findRegexFlags.setText(''); + } + }); + // Create and show selection toggle switch only if any text is selected + const selToggleComponent = addToggleComponent('Replace only in selection', 'If enabled, replaces only occurances in the currently selected text', noSelection); + // Create Buttons + const buttonContainerEl = document.createElement(divClass); + buttonContainerEl.addClass(rowClass); + const submitButtonTarget = document.createElement(divClass); + submitButtonTarget.addClass('button-wrapper'); + submitButtonTarget.addClass(rowClass); + const cancelButtonTarget = document.createElement(divClass); + cancelButtonTarget.addClass('button-wrapper'); + cancelButtonTarget.addClass(rowClass); + const submitButtonComponent = new obsidian.ButtonComponent(submitButtonTarget); + const cancelButtonComponent = new obsidian.ButtonComponent(cancelButtonTarget); + cancelButtonComponent.setButtonText('Cancel'); + cancelButtonComponent.onClick(() => { + logger('Action cancelled.', 8); + this.close(); + }); + submitButtonComponent.setButtonText('Replace All'); + submitButtonComponent.setCta(); + submitButtonComponent.onClick(() => { + let resultString = 'No match'; + let scope = ''; + const searchString = findInputComponent.getValue(); + let replaceString = replaceWithInputComponent.getValue(); + const selectedText = editor.getSelection(); + if (searchString === '') { + new obsidian.Notice('Nothing to search for!'); + return; + } + // Replace line breaks in find-field if option is enabled + if (this.settings.processLineBreak) { + logger('Replacing linebreaks in replace-field', 9); + logger(' old: ' + replaceString, 9); + replaceString = replaceString.replace(/\\n/gm, '\n'); + logger(' new: ' + replaceString, 9); + } + // Replace line breaks in find-field if option is enabled + if (this.settings.processTab) { + logger('Replacing tabs in replace-field', 9); + logger(' old: ' + replaceString, 9); + replaceString = replaceString.replace(/\\t/gm, '\t'); + logger(' new: ' + replaceString, 9); + } + // Check if regular expressions should be used + if (regToggleComponent.getValue()) { + logger('USING regex with flags: ' + regexFlags, 8); + const searchRegex = new RegExp(searchString, regexFlags); + if (!selToggleComponent.getValue()) { + logger(' SCOPE: Full document', 9); + const documentText = editor.getValue(); + const rresult = documentText.match(searchRegex); + if (rresult) { + editor.setValue(documentText.replace(searchRegex, replaceString)); + resultString = `Made ${rresult.length} replacement(s) in document`; + } + } + else { + logger(' SCOPE: Selection', 9); + const rresult = selectedText.match(searchRegex); + if (rresult) { + editor.replaceSelection(selectedText.replace(searchRegex, replaceString)); + resultString = `Made ${rresult.length} replacement(s) in selection`; + } + } + } + else { + logger('NOT using regex', 8); + let nrOfHits = 0; + if (!selToggleComponent.getValue()) { + logger(' SCOPE: Full document', 9); + scope = 'selection'; + const documentText = editor.getValue(); + const documentSplit = documentText.split(searchString); + nrOfHits = documentSplit.length - 1; + editor.setValue(documentSplit.join(replaceString)); + } + else { + logger(' SCOPE: Selection', 9); + scope = 'document'; + const selectedSplit = selectedText.split(searchString); + nrOfHits = selectedSplit.length - 1; + editor.replaceSelection(selectedSplit.join(replaceString)); + } + resultString = `Made ${nrOfHits} replacement(s) in ${scope}`; + } + // Saving settings (find/replace text and toggle switch states) + this.settings.findText = searchString; + this.settings.replaceText = replaceString; + this.settings.useRegEx = regToggleComponent.getValue(); + this.settings.selOnly = selToggleComponent.getValue(); + this.plugin.saveData(this.settings); + this.close(); + new obsidian.Notice(resultString); + }); + // Apply settings + regToggleComponent.setValue(this.settings.useRegEx); + selToggleComponent.setValue(this.settings.selOnly); + replaceWithInputComponent.setValue(this.settings.replaceText); + // Check if the prefill find option is enabled and the selection does not contain linebreaks + if (this.settings.prefillFind && editor.getSelection().indexOf('\n') < 0 && !noSelection) { + logger('Found selection without linebreaks and option is enabled -> fill', 9); + findInputComponent.setValue(editor.getSelection()); + selToggleComponent.setValue(false); + } + else { + logger('Restore find text', 9); + findInputComponent.setValue(this.settings.findText); + } + // Add button row to dialog + buttonContainerEl.appendChild(submitButtonTarget); + buttonContainerEl.appendChild(cancelButtonTarget); + contentEl.appendChild(buttonContainerEl); + // If no text is selected, disable selection-toggle-switch + if (noSelection) + selToggleComponent.setValue(false); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} +class RegexFindReplaceSettingTab extends obsidian.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl('h4', { text: 'Regular Expression Settings' }); + new obsidian.Setting(containerEl) + .setName('Case Insensitive') + .setDesc('When using regular expressions, apply the \'/i\' modifier for case insensitive search)') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.caseInsensitive) + .onChange((value) => __awaiter(this, void 0, void 0, function* () { + logger('Settings update: caseInsensitive: ' + value); + this.plugin.settings.caseInsensitive = value; + yield this.plugin.saveSettings(); + }))); + containerEl.createEl('h4', { text: 'General Settings' }); + new obsidian.Setting(containerEl) + .setName('Process \\n as line break') + .setDesc('When \'\\n\' is used in the replace field, a \'line break\' will be inserted accordingly') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.processLineBreak) + .onChange((value) => __awaiter(this, void 0, void 0, function* () { + logger('Settings update: processLineBreak: ' + value); + this.plugin.settings.processLineBreak = value; + yield this.plugin.saveSettings(); + }))); + new obsidian.Setting(containerEl) + .setName('Prefill Find Field') + .setDesc('Copy the currently selected text (if any) into the \'Find\' text field. This setting is only applied if the selection does not contain linebreaks') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.prefillFind) + .onChange((value) => __awaiter(this, void 0, void 0, function* () { + logger('Settings update: prefillFind: ' + value); + this.plugin.settings.prefillFind = value; + yield this.plugin.saveSettings(); + }))); + } +} + +module.exports = RegexFindReplacePlugin; + + +/* nosourcemap */ \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/manifest.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/manifest.json new file mode 100644 index 0000000..2300a3c --- /dev/null +++ b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/manifest.json @@ -0,0 +1,10 @@ +{ + "author": "Martin Eder", + "authorUrl": "https://www.gruware.org", + "description": "Find and replace text using regular expressions.", + "id": "obsidian-regex-replace", + "isDesktopOnly": false, + "minAppVersion": "0.12.17", + "name": "Regex Find/Replace", + "version": "1.2.0" +} diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/styles.css b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/styles.css new file mode 100644 index 0000000..b6a7e51 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-regex-replace/styles.css @@ -0,0 +1,51 @@ +.find-replace-modal .modal-content .row { + display: flex; + align-items: center; + width: 100%; + margin-top: 8px; +} + + +.find-replace-modal .modal-content .row .input-label { + display: flex; + justify-content: flex-end; + height: 100%; +} + + +.find-replace-modal .modal-content .row .postfix-label { + display: flex; + height: 100%; + margin-left: 8px; + justify-content: flex-end; + color: gray; +} + + +.find-replace-modal .modal-content .row .check-label { + display: flex; + justify-content: flex-end; + width: 100%; + height: 100%; + margin-right: 8px; +} + + +.find-replace-modal .modal-content .row .input-wrapper { + display: flex; + align-items: center; + height: 100%; + width: 100%; + margin: 0; +} + + +.find-replace-modal .modal-content .row .input-wrapper input { + width: 100%; +} + + +.find-replace-modal .modal-content .button-wrapper { + justify-content: center; + margin-top: 16px; +} diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-style-settings/data.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-style-settings/data.json index 86300de..575320c 100644 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-style-settings/data.json +++ b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian-style-settings/data.json @@ -38,7 +38,15 @@ "Editor@@line-hover-indicator": true, "Appearance-light@@card-layout-open-light": true, "phycat-layout@@layout-style": "layout-cards", - "phycat-colors@@light-color-scheme": "theme-light-sakura", + "phycat-colors@@light-color-scheme": "theme-light-sky", "phycat-colors@@dark-color-scheme": "theme-dark-radiation", - "phycat-layout@@rainbow-folders": true + "phycat-layout@@rainbow-folders": true, + "phycat-headings@@h1-light-color": "#FF5050", + "phycat-headings@@h2-light-color": "#ED931D", + "phycat-headings@@h3-light-color": "#EDE92B", + "phycat-headings@@h4-light-color": "#4AF620", + "phycat-headings@@h5-light-color": "#33FDFC", + "phycat-headings@@h6-light-color": "#CF2AF7", + "phycat-headings@@h1-alignment": "h1-align-left", + "phycat-layout@@bg-mix-percent": 91 } \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian42-brat/data.json b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian42-brat/data.json index 1cc2c48..cab179a 100644 --- a/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian42-brat/data.json +++ b/mengyanote-backend/data/mengyanote/.obsidian/plugins/obsidian42-brat/data.json @@ -4,7 +4,7 @@ "themesList": [ { "repo": "sumruler/obsidian-theme-phycat", - "lastUpdate": "38061840" + "lastUpdate": "38104268" } ], "updateAtStartup": true, diff --git a/mengyanote-backend/data/mengyanote/.obsidian/snippets/md.css b/mengyanote-backend/data/mengyanote/.obsidian/snippets/md.css new file mode 100644 index 0000000..c7db56e --- /dev/null +++ b/mengyanote-backend/data/mengyanote/.obsidian/snippets/md.css @@ -0,0 +1,4 @@ +.markdown-preview-view p { + margin-top: 2px; + margin-bottom: 2px; +} \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/.obsidian/workspace.json b/mengyanote-backend/data/mengyanote/.obsidian/workspace.json index 399d536..bc38e2c 100644 --- a/mengyanote-backend/data/mengyanote/.obsidian/workspace.json +++ b/mengyanote-backend/data/mengyanote/.obsidian/workspace.json @@ -4,25 +4,41 @@ "type": "split", "children": [ { - "id": "f6c05057a0ef99fa", + "id": "df67c67fc6eede4f", "type": "tabs", "children": [ { - "id": "1bf536b33f2e3608", + "id": "d2bcc713b2622c69", "type": "leaf", "state": { "type": "markdown", "state": { - "file": "项目策划/萌芽小店.md", + "file": "计算机网络/计算机网络期末考试综合题简单论述.md", + "mode": "source", + "source": true, + "backlinks": false + }, + "icon": "lucide-file", + "title": "计算机网络期末考试综合题简单论述" + } + }, + { + "id": "2e06118e2383ad3d", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "临时/cursor半价.md", "mode": "source", "source": false, "backlinks": false }, "icon": "lucide-file", - "title": "萌芽小店" + "title": "cursor半价" } } - ] + ], + "currentTab": 1 } ], "direction": "vertical" @@ -79,7 +95,7 @@ } ], "direction": "horizontal", - "width": 300 + "width": 304.5 }, "right": { "id": "a00fc03fc4170f30", @@ -95,13 +111,13 @@ "state": { "type": "outline", "state": { - "file": "项目策划/萌芽小店.md", + "file": "临时/cursor半价.md", "followCursor": false, "showSearch": false, "searchQuery": "" }, "icon": "lucide-list", - "title": "萌芽小店 的大纲" + "title": "cursor半价 的大纲" } } ] @@ -120,47 +136,47 @@ "daily-notes:打开/创建今天的日记": false } }, - "active": "1bf536b33f2e3608", + "active": "2e06118e2383ad3d", "lastOpenFiles": [ - "AI/控制台agent工具安装教程.md", - "项目策划/万象口袋.md", - "项目策划/萌芽Ping.md", - "项目策划/萌芽小店.md", - "项目策划/萌芽账户认证中心.md", - "项目策划/萌芽桌面启动器.md", - "AI/提示词工程/前端开发默认框架架构.md", + "临时/浏览器更换默认字体.md", + "临时/cursor半价.md", + "AI/提示词工程/运维部署/Dokcer部署流程.md", + "AI/提示词工程/AI绘图/绘画网站软件logo图标.md", + "AI/提示词工程/AI绘图/AI绘画提示词.md", + "AI/提示词工程/AI绘图/ai换装提示词.md", + "MarkDown/树萌芽のGithub主页.md", + "实习求职/简历/Golang后端开发.md", + "MarkDown/未命名.md", + "树萌芽の小本本/账号合集/CloudFlare账号.md", + "树萌芽の小本本/账号合集/Github账号.md", + "编程语言/前端/代码片段/代码片段-特殊HelloWorld输出.md", + "编程语言/前端/代码片段/代码片段-标准HelloWorld输出.md", + "AI/提示词工程/Windows开发/开发Windows软件细节.md", + "AI/提示词工程/前端开发/前端随机背景图.md", + "AI/提示词工程/前端开发/前端全局字体样式提示词.md", + "AI/提示词工程/前端开发/前端网站图标获取方式.md", + "前后端项目env环境配置解析.md", + "AI/提示词工程/后端开发/后端API开发标准规范.md", + "AI/提示词工程/后端开发/后端API接口根路径说明.md", + "AI/提示词工程/后端开发/后端API部署成docker容器.md", + "Windows/刷新浏览器和系统DNS缓存.md", + "鉴政/女权主义/好姐妹介绍.md", + "鉴政/中国社会主要矛盾演变梳理(1840年至今).md", + "AI/提示词工程/AI绘图/商品封面图生成.md", + "数据库/小萌芽开发中间件信息.md", + "临时/未命名", + "AI/提示词工程/角色扮演", + "AI/提示词工程/后端开发/后端由json更改为SQLite储存", + "算法与数据结构/LeetCode热题100", + "量化投资", + "数据库/RabbitMQ", + "技术原理解析/Golang", + "实习求职/面试经历/树萌芽の面试经历", + "实习求职/术语科普", + "实习求职/面试经历/Golang后端", "Pasted image 20260324211445.png", - "项目策划/萌芽Ping-网页监控.md", - "AI/AI大模型应用实现方式拆解.md", - "树萌芽の奇思妙想/超长有故事的的域名.md", - "树萌芽の奇思妙想/树萌芽の编程想法.md", - "AI/提示词工程/文档类网页模板.md", - "AI/提示词工程/SSH初始化与推送Git 仓库.md", - "树萌芽の小本本/账号合集/AI账号/DeepSeek账号.md", - "AI/提示词工程/Dokcer部署流程.md", - "博客文章/未命名.md", - "Docker/Docker 镜像相关.md", - "Docker/Docker镜像快速迁移.md", - "Docker/Docker常用命令总结.md", - "树萌芽の小本本/账号合集/邮箱账号/微软邮箱.md", - "收藏的DNS集合.md", - "开源社交账号注册方式一览.md", - "未命名.md", - "树萌芽の小本本/账号合集/邮箱账号/QQ邮箱.md", - "树萌芽の小本本/账号合集/AI账号/豆包账号.md", - "树萌芽の小本本/账号合集/邮箱账号", - "树萌芽の小本本/账号合集/邮箱账号.md", - "树萌芽の小本本/账号合集", "Pasted image 20260325202027.png", "Pasted image 20260324211458.png", - "实习求职/通用面试八股", - "AI/AI控制台工具常用命令总结", - "实习求职/面试经历/运维开发", - "实习求职/面试经历/后端开发", - "实习求职/牛客八股/网络模型", - "实习求职/牛客八股/HTTP 与HTTPS", - "实习求职/牛客八股", - "算法与数据结构/2022年蓝桥杯PythonB组真题解析", "Pasted image 20260321154949.png", "Pasted image 20260321155016.png", "Pasted image 20260321162021.png", diff --git a/mengyanote-backend/data/mengyanote/AGENTS.md b/mengyanote-backend/data/mengyanote/AGENTS.md new file mode 100644 index 0000000..4264d0f --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AGENTS.md @@ -0,0 +1,35 @@ +# Repository Guidelines + +## Project Structure & Module Organization +This repository is a Markdown-based knowledge base rather than an application codebase. Top-level folders group notes by domain, such as `AI/`, `Linux/`, `Docker/`, `数据库/`, `编程语言/`, and `项目策划/`. Root files like `README.md`, `📒萌芽笔记.md`, and `🗒记事本.md` act as entry points or quick notes. Obsidian workspace settings live in `.obsidian/`; only commit those changes when they are intentional and useful to other contributors. + +## Build, Test, and Development Commands +There is no required build step or automated test suite in this repository. Useful maintenance commands: + +- `rg --files` lists all tracked notes quickly. +- `git status --short` checks pending edits before committing. +- `git diff -- path/to/file.md` reviews a note before submission. +- `find . -name '*.md'` helps inspect coverage by folder when reorganizing content. + +Preview and link-check notes in your local Markdown editor or Obsidian vault before committing. + +## Coding Style & Naming Conventions +Use Markdown with clear heading hierarchy (`#`, `##`, `###`) and short paragraphs. Prefer one topic per file. Keep filenames descriptive and readable, matching the existing repository style: Chinese titles are common, sometimes with product or technology prefixes such as `Linux/SSH/安全SSH连接.md` or `编程语言/Golang/Golang标准代码架构.md`. Use fenced code blocks with language tags, for example ```bash or ```json. Prefer relative links for cross-note references. + +## Testing Guidelines +Testing is manual. Verify that: + +- headings render correctly, +- code blocks are fenced and labeled, +- internal links and paths are valid, +- moved notes remain in the correct category folder. + +For large reorganizations, open the vault in Obsidian and spot-check edited directories. + +## Commit & Pull Request Guidelines +Recent history uses short messages such as `chore: sync local changes`, `feat: 更新笔记内容`, and `update: 2026-03-28 21:00`. Follow that pattern, but make the summary specific: `docs: add WireGuard troubleshooting note` is better than `update`. Keep commits focused by topic or folder. + +Pull requests should include a brief summary, affected directories, and any rename or move notes. Add screenshots only when a PR intentionally changes `.obsidian` appearance or workspace behavior. + +## Security & Content Boundaries +Do not commit secrets, private tokens, or machine-specific scratch data. Treat `.obsidian/workspace.json`, temporary notes, and personal account records as high-risk changes and review them carefully before pushing. diff --git a/mengyanote-backend/data/mengyanote/AI/AI Agent RAG和Find协同.md b/mengyanote-backend/data/mengyanote/AI/AI Agent RAG和Find协同.md new file mode 100644 index 0000000..3a672fc --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/AI Agent RAG和Find协同.md @@ -0,0 +1,80 @@ +在 2026 年的 AI Agent 架构中,**RAG (检索增强生成)** 和 **Find (精确搜索/工具调用)** 不再是互斥的关系,而是演变为一种“分层记忆与多模态检索”的协同模式。 + +目前的趋势是将 Agent 的记忆视为一个**有生命周期的动态系统**,而不仅仅是简单的文档堆砌。 + +--- + +## 1. 记忆存储:从“死文档 RAG”到“活记忆 Memory” + +对于 Agent 的长期记忆,目前主流的态度是:**RAG 负责广度(外部知识),Memory 负责深度(用户习惯与状态)。** + +### RAG 的角色:无状态的外部知识库 + +- **态度:** RAG 被视为 Agent 的“外挂资料库”。它是**无状态的**,主要用于处理海量的、不经常变动的非结构化数据(如 PDF、Wiki)。 + +- **局限:** 传统的 RAG 在处理“我昨天说了什么”或“我的编程风格”这类具有时效性和个体差异的问题时表现糟糕。 + + +### Memory (Agent 记忆) 的演进:有状态的持久化 + +- **态度:** 现代 Agent(如基于 Mem0 或 Evo-Memory 架构的系统)将记忆分为**短期工作内存**、**片段记忆**(过去发生的事件)和**语义记忆**(事实与偏好)。 + +- **关键技术:** + + - **自动提取与更新:** Agent 会在对话中自动提取事实(例如:“用户更倾向于使用 Python 3.12”),并更新旧记忆,而不是简单地叠加向量。 + + - **重要性加权:** 记忆不再只看语义相似度,还会根据新鲜度(Recency)**和**关键度(Importance)进行加权。 + + +--- + +## 2. 代码文件搜索:从“语义匹配”到“结构化感知” + +在处理代码搜索时,主流 Agent 已经意识到**纯语义 RAG(Vector Search)会造成严重的上下文缺失**。目前更倾向于 **Find(精确搜索)+ Graph(结构化关联)**。 + +### 为什么单纯的 RAG 会在代码搜索中失败? + +代码具有极强的结构性。如果你搜一个函数名,RAG 可能会因为语义相似给你返回一堆注释,但由于 Chunking(分块)切断了函数定义和调用栈的联系,Agent 无法理解逻辑。 + +### 现在的处理策略: + +1. **LSP (Language Server Protocol) 结合:** + + Agent 不再只是检索文字,而是通过调用类似 `grep` 或 `LSP` 的工具执行“精确 Find”。它会直接跳转到定义(Go to Definition)或查找引用(Find References),这种**确定性搜索**的优先级高于模糊的 RAG 检索。 + +2. **GraphRAG (知识图谱 RAG):** + + 将代码仓库建模为图结构(节点是类和函数,边是调用关系)。当 Agent 搜索某个功能时,它能沿着调用链“爬行”,获取完整的上下文,而不仅仅是匹配几个关键词。 + +3. **Hybrid Search (混合检索):** + + - **关键词匹配(BM25):** 快速定位精确的变量名或报错信息。 + + - **向量检索:** 用于理解“如何实现一个加密功能”这种模糊的意图。 + + +--- + +## 3. RAG 与 Find 的核心差异对比 + +下表总结了目前 Agent 对这两者的权衡: + +|**特性**|**RAG (检索增强)**|**Find (工具/精确搜索)**| +|---|---|---| +|**底层逻辑**|语义相似度 (向量空间)|模式匹配 (正则/LSP/SQL)| +|**适用场景**|咨询文档、总结长篇背景、模糊知识获取|查找变量、执行精准跳转、数据库查询| +|**准确性**|存在幻觉风险 (可能搜到无关段落)|极高 (匹配即存在)| +|**Agent 态度**|作为**背景补充**,属于感知层|作为**行动工具**,属于执行层| + +--- + +## 总结 + +目前的顶尖 Agent(如 Cursor 的代码 Agent 或企业级数字员工)采取的是“工具化 RAG”的态度: + +- **对于代码:** 优先使用 LSP 和树结构进行**精确 Find**,RAG 只作为最后的语义辅助。 + +- **对于记忆:** 弃用简单的“存向量-查向量”模式,转而使用**图谱化存储**,让 Agent 能够理解信息之间的逻辑关系(如:版本 A 已经废弃,现在要参考版本 B)。 + + +简单来说,**“Find”是为了保证正确性,而“RAG”是为了保证理解力。** \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/Claude Code常用命令.md b/mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/Claude Code常用命令.md index a6f519b..fb1ab89 100644 --- a/mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/Claude Code常用命令.md +++ b/mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/Claude Code常用命令.md @@ -6,18 +6,18 @@ Claude Code 的命令主要分为三类: ### 1. 终端 CLI 命令(claude [flags]) -| 命令示例 | 主要作用 | 典型用法示例 | 备注 | -|--------------------------------|--------------------------------------------|---------------------------------------------------|-----------------------| -| `claude` | 启动交互式 REPL / TUI 会话 | `claude` | 最常用入口 | -| `claude "你的任务描述"` | 带初始提示启动交互会话 | `claude "写单元测试并运行它们"` | 直接开始工作 | -| `claude -p "查询"` | 非交互式单次查询(pipe 友好,执行后退出) | `cat error.log \| claude -p "分析日志"` | 脚本 / CI 常用 | -| `claude -c` | 继续当前目录下最近的会话 | `claude -c` | 恢复上次工作 | -| `claude -c -p "继续任务"` | 继续上次会话 + 单次查询后退出 | `claude -c -p "修复剩余 bug"` | 快速迭代 | -| `claude -r ` | 按 ID 恢复特定历史会话 | `claude -r abc123 "继续优化"` | 指定会话 | -| `claude update` | 更新到最新版本 | `claude update` | 保持最新 | -| `claude auth login` | 登录 Anthropic 账号 | `claude auth login --email xxx@xx.com` | 首次或切换账号 | -| `claude auth logout` | 退出登录 | `claude auth logout` | — | -| `claude doctor` | 诊断环境、健康检查 | `claude doctor` | 排查问题 | +| 命令示例 | 主要作用 | 典型用法示例 | 备注 | +| ------------------------ | ----------------------- | -------------------------------------- | ---------- | +| `claude` | 启动交互式 REPL / TUI 会话 | `claude` | 最常用入口 | +| `claude "你的任务描述"` | 带初始提示启动交互会话 | `claude "写单元测试并运行它们"` | 直接开始工作 | +| `claude -p "查询"` | 非交互式单次查询(pipe 友好,执行后退出) | `cat error.log \| claude -p "分析日志"` | 脚本 / CI 常用 | +| `claude -c` | 继续当前目录下最近的会话 | `claude -c` | 恢复上次工作 | +| `claude -c -p "继续任务"` | 继续上次会话 + 单次查询后退出 | `claude -c -p "修复剩余 bug"` | 快速迭代 | +| `claude -r ` | 按 ID 恢复特定历史会话 | `claude -r abc123 "继续优化"` | 指定会话 | +| `claude update` | 更新到最新版本 | `claude update` | 保持最新 | +| `claude auth login` | 登录 Anthropic 账号 | `claude auth login --email xxx@xx.com` | 首次或切换账号 | +| `claude auth logout` | 退出登录 | `claude auth logout` | — | +| `claude doctor` | 诊断环境、健康检查 | `claude doctor` | 排查问题 | ### 2. 常用全局 Flags(可加在 claude 命令后) diff --git a/mengyanote-backend/data/mengyanote/🗒记事本.md b/mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/OpenCode Web 启动命令.md similarity index 100% rename from mengyanote-backend/data/mengyanote/🗒记事本.md rename to mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/OpenCode Web 启动命令.md diff --git a/mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/Pi Mono常用命令.md b/mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/Pi Mono常用命令.md new file mode 100644 index 0000000..36bc905 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/AI控制台工具常用命令总结/Pi Mono常用命令.md @@ -0,0 +1,48 @@ + +### 1. 终端启动/CLI 命令(在 shell 中直接运行) + +Pi Mono 的 CLI 入口通常为 `pi-code` 或通过 `npx`/`pnpm` 调用。它支持非交互式的一行命令模式,也支持启动完整的 TUI(终端界面)。 + +| 命令示例 | 主要作用 | 典型用法/备注 | +| :--- | :--- | :--- | +| `pi-code` | 启动交互式 TUI 会话 | 最常用入口,进入后可与 AI 进行多轮对话,支持工具调用 | +| `pi-code "描述任务"` | 带初始 Prompt 启动会话 | `pi-code "修复这个 bug 并运行测试"` | +| `pi-code -p "查询"` | **非交互式管道模式**(执行后退出) | 适合脚本调用:`cat error.log \| pi-code -p "分析"` | +| `pi-code search "关键词"` | 直接在代码库中搜索逻辑 | 例如 `pi-code search "用户认证逻辑在哪"` | +| `pi-code refactor "目标"` | 执行特定重构任务 | 例如 `pi-code refactor "将类组件转为函数组件"` | +| `pi-code --model ` | 启动时指定模型 | 支持切换不同的 LLM 提供商与模型 | + +### 2. 常用全局 Flags(配置与环境) + +| Flag | 作用 | 示例 | +| :--- | :--- | :--- | +| `--model ` | 指定使用的 AI 模型 | `pi-code --model gpt-4o` | +| `--system-prompt ` | 加载自定义系统指令文件 | 支持加载 `SYSTEM.md` 覆盖默认 Prompt | +| `--dangerously-skip-permissions` | **跳过工具执行确认**(高风险,仅限可信环境) | 自动化脚本中使用,减少人工干预 | +| `--debug` | 开启详细调试日志 | 用于排查 Agent 循环或工具调用问题 | + +### 3. 交互模式内 Slash 命令(进入 `pi-code` 后使用) + +进入交互模式后,输入 `/` 即可触发各种内部命令。**注意:** 较新版本(≥ v0.29.1)已对命令进行了整合,强化了 `/settings` 统一菜单 。 + +| Slash 命令 | 主要作用 | 备注/演进说明 | +| :--- | :--- | :--- | +| `/help` | 显示所有可用的 Slash 命令 | 入门必备,随时查看 | +| `/settings` | **统一设置菜单** | **新版核心命令**,整合了以下旧版独立设置项 :
- 思考级别 (Thinking Level)
- 主题 (Theme)
- 队列模式 (Queue)
- 自动压缩 (Auto-compact) | +| `/init` | 初始化项目上下文 | 扫描项目,生成记忆文件 (如 `CLAUDE.md` 或类似) | +| `/clear` | 清空当前会话历史 | 开始全新任务,重置上下文 | +| `/compact` | 压缩对话上下文 | 当会话 token 过多、响应变慢时使用 | +| `/model` | 对话中实时切换模型 | 如从 Haiku 切换到 Sonnet 处理复杂逻辑 | +| `/undo` / `/rewind` | 撤销上一步操作或变更 | 类似 Git 回退,AI 改坏了可以快速恢复状态 | +| `/cost` | 查看当前会话 Token 消耗与预估费用 | 监控使用量 | + +### 核心设计哲学对应表 + +理解 Pi Mono 的命令,关键是理解它的 **四大核心工具**——所有操作最终都是为了驱动这四个能力 : + +| 工具名 | 对应的人类行为 | 典型 CLI 交互场景 | +| :--- | :--- | :--- | +| **Read** | 阅读代码 | AI 自动调用 `read` 分析报错文件 | +| **Write** | 创建新文件 | `pi-code "新建一个 React 组件"` | +| **Edit** | 精确修改 | AI 定位到具体行数进行替换 | +| **Bash** | 执行终端命令 | AI 运行 `npm run test` 验证修改 | diff --git a/mengyanote-backend/data/mengyanote/AI/Cursor和ClaudeCode代码检索原理.md b/mengyanote-backend/data/mengyanote/AI/Cursor和ClaudeCode代码检索原理.md new file mode 100644 index 0000000..06079ca --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/Cursor和ClaudeCode代码检索原理.md @@ -0,0 +1,66 @@ + +## 1. Cursor:基于 IDE 的“分层索引”与语义 RAG + +Cursor 的核心竞争力在于它与 IDE(VS Code 分叉)的深度集成,其检索是一个**后台持续运行**的自动化流水线。 + +### 核心实现机制: + +- **Merkle Tree 增量同步:** + + Cursor 使用 **Merkle Tree(默克尔树)** 算法来检测本地文件的变化。这意味着它不需要每次都重新扫描整个项目,只需对比哈希值就能快速定位改动过的代码块,保证索引的秒级更新。 + +- **语法感知切片 (Syntax-Aware Chunking):** + + 不同于通用的文本 RAG 将代码按字符数切割,Cursor 会解析代码的 **AST(抽象语法树)**。它能识别类、函数和方法,确保检索出来的代码块是逻辑完整的,而不是断头断尾的代码片段。 + +- **混合向量库:** + + 它使用 **Turbopuffer** 这种专为 AI 扩展设计的云端向量数据库。它将代码转化为高维向量(Embeddings),当你输入 `@Codebase` 时,系统会进行**语义搜索**(比如搜“支付逻辑”能找到没有关键词但功能相关的 `handle_checkout` 函数)。 + +- **隐私处理:** + + 在索引时,Cursor 仅向服务器发送混淆后的元数据和向量,而非明文代码,平衡了检索性能与企业代码安全。 + + +--- + +## 2. Claude Code:基于 CLI 的“代理式工具”与上下文压缩 + +Claude Code(Anthropic 推出的命令行 Agent)的思路更偏向于**按需搜索**和**多级内存管理**。 + +### 核心实现机制: + +- **工具化搜索 (Agentic Search):** + + Claude Code 不倾向于后台静默索引,而是给 Agent 一套“搜索工具”(如 `grep`、文件树列举)。当它需要了解项目时,它会主动决策:“我需要看看 `/src` 目录下有哪些 API 定义”,然后调用工具去查。 + +- **三层内存分级 (3-Tier Memory):** + + 它建立了一个层级架构来管理检索到的信息: + + 1. **用户级 (~/.claude/CLAUDE.md):** 跨项目的全局偏好。 + + 2. **项目级 (CLAUDE.md):** 项目架构、规范和关键逻辑的“快照”。 + + 3. **动态导入:** 针对当前任务实时搜索到的代码。 + +- **五层上下文压缩 (Context Compaction):** + + 由于 CLI 工具主要靠长上下文(Claude 3.5/4 系列的 200k+ 窗口),它在调用模型前会进行 5 层压缩处理(包括摘要化、无用信息剪枝、上下文折叠等),确保在不损失关键检索信息的前提下节省 Token。 + +- **RAG 自动切换模式:** + + 当项目规模超过上下文窗口极限时,Claude Code 会自动开启 RAG 模式,从“全量加载”切换为“精准检索”。 + + +--- + +## 3. 深度对比:Cursor vs Claude Code + +|**特性**|**Cursor (IDE 模式)**|**Claude Code (CLI/Agent 模式)**| +|---|---|---| +|**检索触发**|**静默索引**:后台持续生成向量|**任务驱动**:Agent 根据目标主动去搜| +|**核心算法**|向量搜索 (Vector) + Merkle Tree|模式匹配 (Grep) + 代理决策| +|**上下文管理**|侧重于**“找得准”**(相似度匹配)|侧重于**“装得多”**(超长窗口 + 动态压缩)| +|**擅长场景**|快速定位文件、行内补全、解释代码|跨文件的架构重构、自主 Bug 修复、自动化测试| + diff --git a/mengyanote-backend/data/mengyanote/AI/PI-Mono-Agent架构解析.md b/mengyanote-backend/data/mengyanote/AI/PI-Mono-Agent架构解析.md new file mode 100644 index 0000000..c6c478d --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/PI-Mono-Agent架构解析.md @@ -0,0 +1,377 @@ + +## 它主要是用什么语言写的 + +从官方仓库看,**核心就是 Node.js / TypeScript 生态**:仓库根目录有 `package.json`、`tsconfig.json`,官方开发命令也是 `npm install`、`npm run build`、`npm run check`,并且包结构里主要是 `pi-ai`、`pi-agent-core`、`pi-coding-agent`、`pi-tui`、`pi-web-ui` 这些 npm 包。([GitHub](https://github.com/badlogic/pi-mono "GitHub - badlogic/pi-mono: AI agent toolkit: coding agent CLI, unified LLM API, TUI & web UI libraries, Slack bot, vLLM pods · GitHub")) + +另外,官方文档明确写了: + +- **Extensions 是 TypeScript modules** + +- 你可以用它们来注册工具、命令、UI、事件钩子 + +- 还能把扩展打成 **Pi Packages** 通过 npm 或 git 复用。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md "pi-mono/packages/coding-agent/README.md at main · badlogic/pi-mono · GitHub")) + + +所以你可以把它理解成: + +> **“一个以 TypeScript 为核心的 Agent 框架 / 终端 agent harness”**。([GitHub](https://github.com/badlogic/pi-mono "GitHub - badlogic/pi-mono: AI agent toolkit: coding agent CLI, unified LLM API, TUI & web UI libraries, Slack bot, vLLM pods · GitHub")) + +--- + +## 能不能基于它魔改成不同专用 Agent + +**完全可以,而且比很多现成 Agent 更适合“专用化”。** +官方 README 直接说 Pi 的思路就是: + +> **适配你的工作流,而不是让你去适配它。** +> 你可以通过 **TypeScript Extensions、Skills、Prompt Templates、Themes** 来扩展,而不必 fork 后硬改内部。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md "pi-mono/packages/coding-agent/README.md at main · badlogic/pi-mono · GitHub")) + +官方扩展文档也明确给了这些能力: + +- 注册 **自定义工具** 给 LLM 调用 + +- 拦截或修改工具调用 + +- 增加 **自定义命令** + +- 做 **自定义 TUI 交互** + +- 做 **会话持久化状态** + +- 接 webhook、CI、外部系统。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md "pi-mono/packages/coding-agent/docs/extensions.md at main · badlogic/pi-mono · GitHub")) + + +所以从工程角度讲,你有两条路: + +### 方案 A:不 fork,做“专用化配置 + 扩展” + +这是最推荐的。 + +适合做: + +- 运维工程师 Agent + +- 全栈开发 Agent + +- 后端 Agent + +- Linux 学习 / 排障 Agent + +- 游戏服务端 Agent + + +做法一般是: + +- 换 system prompt / prompt template + +- 加 skills + +- 加一批 TypeScript extensions + +- 注册专属工具 + +- 限制某些高危操作 + +- 定制命令和 UI。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md "pi-mono/packages/coding-agent/README.md at main · badlogic/pi-mono · GitHub")) + + +### 方案 B:直接 fork pi-mono 深度魔改 + +也能做,尤其当你想改这些底层东西时: + +- agent loop + +- session 格式 + +- provider 接入层 + +- TUI 行为 + +- 默认工具策略 + +- 多 agent 编排方式 + + +因为它本身就是 monorepo,包分层很清楚:`pi-ai` 负责模型抽象,`pi-agent-core` 负责 agent runtime,`pi-coding-agent` 是 CLI,`pi-tui` 是终端 UI。([GitHub](https://github.com/badlogic/pi-mono "GitHub - badlogic/pi-mono: AI agent toolkit: coding agent CLI, unified LLM API, TUI & web UI libraries, Slack bot, vLLM pods · GitHub")) + +--- + +## 你举的两个例子,理论上都很适合 + +### 1. 运维工程师 Agent + +这个特别适合 Pi。 + +因为扩展系统能做: + +- 命令白名单 / 黑名单 + +- `sudo`、`rm -rf`、`kubectl delete` 前确认 + +- 自动读取日志 + +- 封装 `ssh`、`docker`、`systemctl`、`journalctl` + +- 接 Prometheus / Grafana / Webhook / CI + +- 保留排障状态和上下文。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md "pi-mono/packages/coding-agent/docs/extensions.md at main · badlogic/pi-mono · GitHub")) + + +你可以做成这种能力组合: + +- `/diag`:自动诊断 CPU、内存、磁盘、systemd 状态 + +- `/k8s`:读取 Pod、Events、Logs + +- `/nginx`:分析配置和报错 + +- `/rollback`:帮你执行回滚步骤,但要人工确认 + +- `/safe-shell`:高危命令前二次确认 + + +Pi 文档里甚至专门提到过这类扩展场景: + +- 权限门禁 + +- 路径保护 + +- 外部集成 + +- 有状态工具。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md "pi-mono/packages/coding-agent/docs/extensions.md at main · badlogic/pi-mono · GitHub")) + + +--- + +### 2. 全栈开发 Agent + +这个也很适合,而且比“纯运维 agent”更贴近 Pi 目前主定位。 + +你可以给它加: + +- 前端工具:读页面结构、跑构建、跑测试 + +- 后端工具:生成接口、迁移、测试、lint + +- 数据库工具:封装 migration / schema diff + +- Git 工具:自动提交、分支管理、PR 辅助 + +- 项目模板 skill:Next.js、NestJS、Go、Spring、Node API 等 + + +README 明确说 Pi 支持: + +- interactive mode + +- JSON / print mode + +- RPC 模式 + +- SDK embedding + 也就是说它不只是命令行聊天壳子,还能嵌进你自己的应用里。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md "pi-mono/packages/coding-agent/README.md at main · badlogic/pi-mono · GitHub")) + + +--- + +## 真正适合它的“专用 Agent”长什么样 + +Pi 更适合做这种: + +> **“在一个专业领域里,有固定流程、有固定工具、有可控权限”的 agent** + +比如: + +### 很适合 + +- 运维排障 agent + +- 全栈开发 agent + +- 代码审查 agent + +- 测试生成 agent + +- 游戏后端开发 agent + +- Linux 学习陪练 agent + +- K8s 诊断 agent + + +### 不算最适合 + +- 那种强依赖图形浏览器自动操作的网站 agent + +- 一上来就想做超级复杂多 agent 调度平台 + +- 完全零代码、零配置、即装即用型产品 + + +因为 Pi 的核心哲学还是 **极简 + 自己扩展**,不是“开箱自带一切”。([Mario Zechner](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/?utm_source=chatgpt.com "What I learned building an opinionated and minimal coding ...")) + +--- + +## 做专用 Agent 时,推荐怎么改 + +我建议你按这 4 层去改,而不是一上来 fork 全仓库: + +### 第 1 层:Prompt / Skills + +定义角色: + +- 运维工程师 agent + +- 全栈开发 agent + +- Go 后端 agent + +- Linux 导师 agent + + +这一层决定它“说话方式”和“解题策略”。Pi 官方把 Skills、Prompt Templates 当成正式定制方式之一。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md "pi-mono/packages/coding-agent/README.md at main · badlogic/pi-mono · GitHub")) + +### 第 2 层:Extensions + +这是最关键的一层。 + +你可以在扩展里做: + +- 注册自定义工具 + +- 注册命令 + +- 拦截危险调用 + +- 加状态存储 + +- 做外部系统集成。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md "pi-mono/packages/coding-agent/docs/extensions.md at main · badlogic/pi-mono · GitHub")) + + +### 第 3 层:Tools + +把专用能力封成工具给模型调用,比如: + +运维 agent: + +- `check_system_health` + +- `read_nginx_conf` + +- `tail_service_logs` + +- `kubectl_get_pods` + +- `restart_service_safe` + + +全栈 agent: + +- `run_frontend_test` + +- `generate_api_route` + +- `db_migration_status` + +- `git_commit_changes` + +- `scan_project_structure` + + +### 第 4 层:底层 fork + +当你想改 runtime、session、provider、TUI,再 fork。 + +--- + +## 你要注意的现实问题 + +### 1. 它不是“写个 prompt 就完事” + +真正好用的专用 agent,核心不在 prompt,而在: + +- 工具设计 + +- 权限控制 + +- 上下文组织 + +- 输出格式约束 + + +Pi 很适合做这些,但也意味着你得自己设计。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md "pi-mono/packages/coding-agent/docs/extensions.md at main · badlogic/pi-mono · GitHub")) + +### 2. 运维 agent 一定要做权限隔离 + +比如: + +- 禁止直接删生产资源 + +- 高危命令必须确认 + +- 只允许白名单主机 + +- 日志可读,写操作有限制 + + +Pi 扩展机制支持做这类 guard rail。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md "pi-mono/packages/coding-agent/docs/extensions.md at main · badlogic/pi-mono · GitHub")) + +### 3. 全栈 agent 最好按栈拆 skill + +不要做一个“万能全栈神 agent”,更实用的是拆成: + +- React / Vue 前端 skill + +- Node / Go 后端 skill + +- PostgreSQL / Mongo 工具 + +- Docker / CI skill + + +这样更稳。 + +--- + +## 直接回答你的问题 + +### 他是用什么语言编写的? + +**主要是 TypeScript / Node.js 生态。** 官方仓库和文档都明确显示它是 npm monorepo,并且扩展是 TypeScript modules。([GitHub](https://github.com/badlogic/pi-mono "GitHub - badlogic/pi-mono: AI agent toolkit: coding agent CLI, unified LLM API, TUI & web UI libraries, Slack bot, vLLM pods · GitHub")) + +### 可不可以基于他魔改做不同专用 agent? + +**可以,而且非常适合。** +最推荐的方式不是先 fork,而是: + +> **Pi 本体 + Prompt Templates + Skills + TypeScript Extensions + 自定义 Tools** + +这样就能做出: + +- 运维工程师 agent + +- 全栈开发 agent + +- 后端开发 agent + +- Linux 排障 agent。([GitHub](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md "pi-mono/packages/coding-agent/README.md at main · badlogic/pi-mono · GitHub")) + + +### 要不要 fork? + +- **轻中度专用化:不用 fork** + +- **深度改 agent runtime / UI / provider / session:再 fork**。([GitHub](https://github.com/badlogic/pi-mono "GitHub - badlogic/pi-mono: AI agent toolkit: coding agent CLI, unified LLM API, TUI & web UI libraries, Slack bot, vLLM pods · GitHub")) + + +--- + +按你的背景,其实最适合你的是这两种: + +1. **Linux / 运维排障 Agent** + +2. **游戏后端开发 Agent**(尤其适合你那个“萌芽农场”服务端) + + +我可以下一条直接给你画一套 +**“基于 Pi 改造成运维工程师 Agent 的完整架构图 + 模块清单 + 目录结构”**。 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/PI-Mono架构解析.md b/mengyanote-backend/data/mengyanote/AI/PI-Mono架构解析.md new file mode 100644 index 0000000..241c4cd --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/PI-Mono架构解析.md @@ -0,0 +1,37 @@ +pi-mono 是一个功能强大的 AI Agent 工具包,由知名游戏框架 libGDX 的开发者 Mario Zechner 创建。它以其极简主义的设计哲学和模块化的架构而备受瞩目,为开发者构建 AI 应用提供了全方位的支持。 + +🎯 核心设计哲学:Less is More + +pi-mono 的核心竞争力在于其“少即是多”的工程理念。与许多功能繁杂的框架不同,它刻意保持极简,将 Agent 的能力收敛到最基础的四个工具上,相信大模型能够通过这些基础原语组合出复杂的能力。 + +这四个核心工具是: +* Read: 读取文件内容。 +* Write: 创建或完全重写文件。 +* Edit: 精确替换文件中的文本。 +* Bash: 执行命令行指令。 + +这种设计覆盖了“读、写、改、跑”的开发主路径,形成了一个完整且可控的闭环,让 Agent 能够在其中自主地理解代码、修改代码并验证结果。 + +🧩 模块化架构 + +pi-mono 采用清晰的四层分层架构,各层之间松耦合,开发者可以根据需求灵活选用,无需加载整个框架。 + +1. CLI 工具层 (pi-coding-agent): 提供交互式的编码 Agent 命令行界面,负责会话管理、主题和上下文文件处理。 +2. 终端 UI 层 (pi-tui): 一个高质量的终端用户界面库,支持差分渲染,确保交互过程流畅不闪烁。 +3. Agent 逻辑层 (pi-agent-core): Agent 的核心运行时,处理工具调用、事件流和状态管理。 +4. LLM 抽象层 (pi-ai): 统一的 LLM API,封装了对 OpenAI、Anthropic、Google 等多种主流大语言模型提供商的调用,简化了模型切换和成本跟踪。 + +此外,它还包含用于 Slack 集成的 pi-mom、用于 Web 界面的 pi-web-ui 以及用于管理 vLLM 部署的 pi-pods 等组件。 + +✨ 独特优势 + +* 强大的扩展系统 (Extension System): 这是 pi-mono 的一大亮点。它不仅仅是简单的插件,而是通过几十个生命周期钩子(hook),允许开发者深度定制 Agent 的行为,例如注册自定义工具、监听所有代理事件、覆写系统提示词等。 +* 灵活的会话管理: 会话以树形结构组织,开发者可以进行分支、回滚等操作,方便地尝试不同方案或修复问题,然后合并回主线。 +* 引导消息 (Steering Messages): 在 Agent 执行任务的过程中,用户可以通过 agent.steer() 方法“插嘴”,注入新的指令来调整 Agent 的方向,而无需中断或重启整个任务,交互更加灵活。 + +🚀 应用场景 + +pi-mono 不仅是一个独立的 CLI 工具,更是一个可以嵌入到其他应用中的“引擎”。 +* AI 编程助手: 作为核心的编码 Agent,帮助开发者生成代码、调试和重构。 +* 定制化 AI 应用: 著名的开源项目 OpenClaw 就是基于 pi-mono 构建的,证明了其作为底层框架的强大能力。 +* 团队协作机器人: 通过 pi-mom 可以将 AI 能力集成到 Slack 等平台,实现自动化的消息处理和任务分配。 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/Token(词元)简要说明.md b/mengyanote-backend/data/mengyanote/AI/Token(词元)简要说明.md new file mode 100644 index 0000000..5cb6f68 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/Token(词元)简要说明.md @@ -0,0 +1,55 @@ + + +## 一、基本概念 + +**Token**(官方中文译名:**词元**)是大语言模型处理文本时的最小基本单位。在将人类可读的文本输入模型之前,需要先进行**分词**(Tokenization),即将文本切分成若干片段,每个片段即为一个 token。负责分词的组件称为**分词器**(Tokenizer)。 + +## 二、Token 的切分规则 + +Token 可以是单个字、一个词、一个标点符号,甚至一个字符的一部分。其切分方式并非基于语义理解,而是依据**统计规律**:在海量文本中经常相邻出现的字符序列,更可能被合并为一个 token。 + +### 示例 +- “小白”可能作为一个 token 被切出 +- “我”和“要”可能各自独立,也可能合并 +- 逗号“,”通常单独成为一个 token +- 英文单词 “bug” 可能完整,也可能拆分为 “.``bug” 等 + +## 三、模型处理流程 + +1. 输入文本 → 分词器切分为 token 序列 +2. 每个 token 通过查询**词表**(固定 token 集合)转换为对应的数字编号 +3. 模型实际处理这些数字编号(即 token 的 ID) + +## 四、输入 Token 与输出 Token + +- **输入 Token**:用户在聊天框中发送的文本被切分后,传入大模型的部分。 +- **输出 Token**:大模型返回的回复内容被切分后对应的部分。 + +## 五、计费与定价(以 DeepSeek 为例) + +处理 token 需要消耗 GPU 算力,因此多数厂商以 token 数量作为计费单位。参考数据: + +- **100 万 token** ≈ 3 本《三体》小说的内容量 +- **输入 token 价格**:2 元 / 百万 token +- **输出 token 价格**:3 元 / 百万 token + +### 为什么输出更贵? +- 模型可以**并行处理**大量输入 token,速度快、资源占用相对低。 +- 输出 token 需要**逐个顺序生成**,前一个 token 未生成完就无法确定下一个,因此速度慢、推理资源消耗高。 + +## 六、缓存命中与未命中 + +在输入 token 中,还存在价格差异: + +- **缓存命中**:当系统提示词或对话前缀与历史请求完全一致时,模型可直接复用之前的计算结果,无需重新计算。价格较低。 +- **缓存未命中**:前缀发生变化,需要重新计算所有 token。价格较高。 + +> 实用建议:尽量复用长前缀(如固定系统指令),避免随意改动,否则缓存失效会导致费用上升。 + +## 七、我国 Token 调用量参考 + +- 日均 token 调用量:**超 140 万亿** +- 换算对比: + - 约等于 **4.7 亿本**《三体》小说 + - 约等于 **700 亿**网页 + - 约等于一个人连续说话 **3800 万年** diff --git a/mengyanote-backend/data/mengyanote/AI/多智能体(Multi-Agent)协同技术说明.md b/mengyanote-backend/data/mengyanote/AI/多智能体(Multi-Agent)协同技术说明.md new file mode 100644 index 0000000..176afb9 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/多智能体(Multi-Agent)协同技术说明.md @@ -0,0 +1,56 @@ + +## 1. 背景:从单体到群体 + +- 2025年,Agent(智能体)成为AI主流形态,具备规划、拆解任务、调用工具的能力。 +- 复杂问题下,单体智能(单Agent)能力有限。 +- 类比人类文明跃迁:**关键不是更聪明的个体,而是分工与协作**。 +- 因此,AI下一步进化方向:**从强单体Agent转向多Agent集群协同工作**。 + +## 2. 主流多Agent方案概览 + +| 厂商/产品 | 方案名称 | 特点 | +|----------|----------|------| +| Claude Code | Agent Teams | 子Agent共享上下文,可互相通信,但静态角色、Token消耗大 | +| Kimi | Agent Swarm | 上下文物理隔离,支持动态并行,开箱即用 | +| OpenAI (GPT-5.3 Codex) | 多Agent交互(暗示) | 正在向多Agent方向迭代 | + +## 3. 单Agent到多Agent的演进路径(以Claude Code为例) + +| 模式 | 工作方式 | 局限性 | +|------|----------|--------| +| 单Agent | 一个窗口串行执行多任务 | 上下文过长 | +| 手动多窗口并行 | 用户手动拆分任务,多窗口并行 | 冲突需手动协调 | +| Subagents(子Agent) | 主Agent控制多个子Agent,独立执行后汇总结果 | 子Agent间无法通信,适合低耦合任务 | +| Agent Teams | 主Agent通过共享任务清单分配任务,子Agent可互相通信、并行申领任务 | 上下文完全共享,Token爆炸;角色固定,无法中途动态扩容 | + +## 4. Agent Teams 的核心问题 + +1. **Token量爆炸** + - 所有子Agent共享完整上下文,每轮提示词包含所有历史消息。 + - 任务越长,Token消耗越大(对比Subagents:仅返回总结,上下文可控)。 + +2. **缺乏弹性并行能力** + - 团队成员在任务开始时固定,无法根据工作量**动态扩容**。 + - 例:原本由单个Agent负责的批量生成任务(如100张图),不会临时裂变成多个并行Agent。 + +## 5. Kimi Agent Swarm 的解决方案 + +### 5.1 架构差异对比 + +| 特性 | Agent Teams (Claude Code) | Kimi Agent Swarm | +|------|---------------------------|------------------| +| 上下文管理 | 全局共享,所有历史可见 | **物理隔离**:子Agent仅负责局部子任务,结果总结后返回主Agent | +| Token消耗 | 高(随任务增长爆炸) | **最低**(聚焦+总结机制) | +| 并行能力 | 静态角色,无中途扩容 | **动态并行**:主Agent根据工作量实时创建/调整子Agent数量 | +| 易用性 | 需配置环境、安装Skills | 自带虚拟机(Kimi's Computer),生图/搜索/代码/网页开箱即用 | + +### 5.2 关键技术 + +- **PARL(并行代理强化学习)**:Kimi K2.5 模型训练时引入,使其具备**动态编排**能力,能像项目经理一样现场“摇人”。 +- 效率对比:并行执行相比传统串行,效率提升 **4倍以上**。 + +## 6. 总结 + +- **核心理念**:AI进化方向不是更聪明的大脑,而是更高效的**群体协作**。 +- **本质转变**:多Agent协同使AI从“工具”转变为“队友”。 +- **发展意义**:这种集群协作模式可能是通往**通用人工智能(AGI)** 的重要路径。 diff --git a/mengyanote-backend/data/mengyanote/AI/控制台agent工具安装教程.md b/mengyanote-backend/data/mengyanote/AI/控制台agent工具安装教程.md index 40e7b53..12a7b1f 100644 --- a/mengyanote-backend/data/mengyanote/AI/控制台agent工具安装教程.md +++ b/mengyanote-backend/data/mengyanote/AI/控制台agent工具安装教程.md @@ -1,58 +1,49 @@ -### opencode +### OpenCode ```bash # 安装(推荐一键脚本,Linux/macOS) curl -fsSL https://opencode.ai/install | bash # 其他方式 npm install -g opencode-ai@latest -brew install anomalyco/tap/opencode + +bun add -g opencode-ai@latest # 官方更新(推荐) -opencode upgrade # 内置更新到最新版 -# 指定方法(如果需要) -opencode upgrade --method curl # 或 npm/brew 等 +opencode upgrade ``` - -### Claude Code(Anthropic) +### Claude Code ```bash # 安装(推荐原生一键,自动后台更新) curl -fsSL https://claude.ai/install.sh | bash # Linux/macOS -# Windows 用 PowerShell -# (从 claude.ai 官网复制对应命令,通常是 Invoke-RestMethod ...) - # 其他方式(不推荐长期使用) npm install -g @anthropic-ai/claude-code -# 官方更新(推荐) -claude update # 内置自动更新 -# 或者 -npm install -g @anthropic-ai/claude-code@latest +bun install -g @anthropic-ai/claude-code@latest -# 迁移建议 -# npm 方式即将弃用,建议运行 claude install 迁移到原生版 +# 官方更新(推荐) +claude update ``` -### Codex(OpenAI 旧版风格) +### Codex ```bash # 安装 / 更新(官方唯一推荐方式) npm i -g @openai/codex@latest + +bun install -g @openai/codex@latest + +# 官方更新(推荐) +codex update ``` -### iflow +### Pi-Mono ```bash -# 一键安装(推荐,会自动处理全部依赖) -bash -c "$(curl -fsSL https://gitee.com/iflow-ai/iflow-cli/raw/main/install.sh)" +bun install -g @mariozechner/pi-coding-agent@latest -# 已安装 Node.js 22+ -npm i -g @iflow-ai/iflow-cli@latest - -# 官方更新 -# 启动时自动检测并升级(推荐) -# 手动强制更新 -npm i -g @iflow-ai/iflow-cli@latest +# 官方更新(推荐) +pi update ``` ### GitHub Copilot CLI @@ -69,27 +60,21 @@ gh extension install github/gh-copilot # 新方式(2026年后推荐) copilot update # 内置更新命令 # 手动方式 npm install -g @github/copilot@latest -# brew 方式:brew upgrade copilot-cli # 查看版本 & 检查更新 copilot version ``` -### Gemini CLI(Google) +### Gemini CLI ```bash # 安装(推荐) npm install -g @google/gemini-cli -# 或 Homebrew -brew install gemini-cli - -# 临时运行(无需永久安装) -npx @google/gemini-cli # 更新 npm install -g @google/gemini-cli@latest ``` -### Qwen Code CLI(阿里通义千问) +### Qwen Code CLI ```bash # 安装(推荐一键脚本,Linux/macOS) curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh | bash @@ -111,26 +96,6 @@ npm install -g @qwen-code/qwen-code@latest # 内置命令:启动后可用 /upgrade 或检查 /version ``` -### Kimi Code CLI(Moonshot AI) -```bash -# 安装(推荐一键脚本,会自动装 uv + Python 3.13 + 依赖) -# Linux/macOS -curl -LsSf https://code.kimi.com/install.sh | bash - -# Windows(PowerShell) -Invoke-RestMethod https://code.kimi.com/install.ps1 | Invoke-Expression - -# 已安装 uv 的方式 -uv tool install --python 3.13 kimi-cli - -# 启动 -kimi - -# 官方更新(推荐) -uv tool upgrade kimi-cli --no-cache # 最干净方式 -# 或重新运行一键安装脚本 -``` - ### OpenClaw ```bash # 安装(强烈推荐一键脚本,自动检测 Node、安装全局 CLI 并启动 onboarding wizard) diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘画提示词.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/AI绘画提示词.md similarity index 97% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘画提示词.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/AI绘画提示词.md index d7c51c3..a9510bd 100644 --- a/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘画提示词.md +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/AI绘画提示词.md @@ -1,7 +1,11 @@ -*by 树萌芽* +--- 图片风格为2D像素像素化风格,比例 「1:1」 画一个游戏道具,背景是纯黑色,纯黑色 不带任何文字,没有任何文字,现在我要你画 一个闪闪发光的金黄色金币卡 +--- 图片风格为2D像素风格,比例 「1:1」 画一个游戏元素,风格为2.5D 就是上帝视角,就是正交斜视角那种有立体感,背景是纯黑色,纯黑色 不带任何文字,现在我要你画一个一丛茂密的灌木丛 记住一定要3d立体感 只要 灌木丛 只要 灌木丛! -比例 「1:1」图片风格为2D像素风格, 画一个游戏装饰元素,风格为2.5D 就是上帝视角,就是正交斜视角那种有立体感,背景是纯黑色,纯黑色 不带任何文字,现在我要你画一个 占卜测运算命台 相关的装饰可以多一点 记住一定要3d立体感 \ No newline at end of file +--- +比例 「1:1」图片风格为2D像素风格, 画一个游戏装饰元素,风格为2.5D 就是上帝视角,就是正交斜视角那种有立体感,背景是纯黑色,纯黑色 不带任何文字,现在我要你画一个 占卜测运算命台 相关的装饰可以多一点 记住一定要3d立体感 + +--- \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/ai换装提示词.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/ai换装提示词.md new file mode 100644 index 0000000..1e18f10 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/ai换装提示词.md @@ -0,0 +1,6 @@ +正向提示(Prompt): +“基于参考图片,极致性感的艺术人体摄影,优雅的S形曲线姿势,柔和侧光和暖色高光突出丰满的身体轮廓和自然肌肤纹理,薄如蝉翼的半透明轻纱微微贴合湿润肌肤,几乎无布料覆盖,慵懒诱惑的表情,湿身效果,水珠在皮肤上滚动,反射柔美光泽,专业摄影棚照明,浅景深,电影感特写,超现实细节,8K超清写实皮肤,voluptuous figure, seductive pose, sheer veil only, cinematic lighting, highly detailed texture” +负面提示(Negative Prompt): +“衣服、布料、连衣裙、内衣、蕾丝、厚重织物、模糊、变形、马赛克、审查、水印、文字、低质量、卡通、动漫、额外肢体、畸形、低分辨率、干燥皮肤、平坦身材” + +基于参考图片,这是我自己的照片,我需要参加一个高端内衣/泳装摄影造型测试,请帮我调整成专业艺术人体摄影风格,优雅的S形曲线姿势,柔和暖光和高光突出自然的身体线条和肌肤质感,轻盈透明的薄纱材质轻轻贴合身体,湿身水光效果,水珠自然滚动,慵懒自然的眼神,专业摄影棚照明,浅景深,电影感特写,超清写实皮肤纹理,8K细节,voluptuous figure, seductive yet artistic pose, sheer light fabric, cinematic lighting, highly detailed \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/修复老照片.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/修复老照片.md new file mode 100644 index 0000000..10d81ba --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/修复老照片.md @@ -0,0 +1,6 @@ +这是一张老旧破损的老照片,执行专业级老照片修复操作,核心准则为100%还原照片拍摄时的原始真实样貌,绝对不做任何额外修改与二次创作,严格遵循以下要求: +1. 画面硬约束:严格保留原始照片的画面尺寸、构图、画面边界、人物比例、场景布局,绝对禁止扩图、裁切、改变画面比例,禁止添加任何原始画面不存在的元素,禁止删减任何原始画面已有的元素; +2. 破损与清晰度还原:精准修复照片所有破损、划痕、折痕、霉点、污渍、缺角、褪色问题,消除画面模糊、失焦、过度颗粒、噪点问题;极致还原人物原生五官细节、服饰纹理、场景物体的原始真实质感,还原照片刚冲洗完成时的原生清晰锐度,不做磨皮、美颜、瘦脸、五官修改等任何美化操作,完整保留人物原生相貌特征; +3. 年代规范上色:严格按照照片拍摄的对应历史年代的真实色彩规范进行写实上色,色彩还原贴合对应年代的真实风貌,色调自然写实;严格遵循原始照片的光影明暗关系上色,不改变原始画面的光影逻辑,色彩饱和度自然适中,无浮夸溢出、无失真偏色,完全符合历史年代的真实色彩质感; +4. 最终红线要求:整体仅完成破损修复、清晰度还原、年代精准上色三项操作,不做任何艺术化加工、风格化处理、特效添加,最终成品为符合对应年代特征的、修复完好的真实清晰老照片,完整保留老照片的原生胶片质感,无现代数码感。 +负面词:扩图、改变画面尺寸、裁切画面、改变原始构图、添加额外元素、删减原始元素、美颜、磨皮、瘦脸、改变人物五官、艺术化创作、风格化处理、夸张高饱和色彩、色彩失真、画面变形、重绘、二次创作、更换背景、添加特效、水印、文字、边框、过度锐化、卡通化、二次元、油画质感、现代数码感、模糊、噪点 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/商品封面图生成.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/商品封面图生成.md new file mode 100644 index 0000000..1a31d18 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/商品封面图生成.md @@ -0,0 +1,3 @@ +生成一个 商品封面图 + +生成一个 商品封面图 尽量符合商品定位,不要太抽象 要正规 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/绘画网站软件logo图标.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/绘画网站软件logo图标.md new file mode 100644 index 0000000..463506f --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/AI绘图/绘画网站软件logo图标.md @@ -0,0 +1,3 @@ +请你以专业的app软件logo图标设计师的角度给我设计10张logo图片,要浅色柔和背景,图片中不能出现任何文字 比例1:1,我的软件名称为:萌芽 + +请你以专业的app软件logo图标设计师的角度给我设计10张logo图片,图标要现代化样式,图片中不能出现任何文字 比例1:1,我的软件名称为: \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/SSH初始化与推送Git 仓库.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/SSH初始化与推送Git 仓库.md deleted file mode 100644 index d8509c1..0000000 --- a/mengyanote-backend/data/mengyanote/AI/提示词工程/SSH初始化与推送Git 仓库.md +++ /dev/null @@ -1,51 +0,0 @@ -# Role: SSH初始化与推送Git 仓库 - -## 0. 变量校验 -执行前扫描核心变量:`{{REPO}}`、`{{PROJECT_TYPE}}`(可选,默认为通用)。 -若缺失 `{{REPO}}`,挂起并返回: -> ⚠️ 缺少核心变量:[{{REPO}}](仓库名,如 my-project),请补充。 - -## 1. 核心目标 -- 任务目标:在本地初始化 Git 仓库,配置符合项目类型的 .gitignore,完成初始提交,并通过 SSH 协议推送到指定的 Gitea 远程仓库 main 分支。 -- 成功标准:本地仓库已初始化,main 分支存在,.gitignore 包含关键忽略项,存在初始提交记录,远程仓库已添加并成功推送。 -- 失败边界:无法连接远程仓库、SSH 密钥未配置、远程仓库地址错误、推送时出现非预期文件变更。 - -## 2. 执行流程(仅执行本任务,不添加额外配置) -1. **初始化 Git 仓库**:在当前目录执行 `git init`。 -2. **创建并切换到 main 分支**:执行 `git checkout -b main`(若 main 分支已存在,则直接切换)。 -3. **生成 .gitignore**:根据 `{{PROJECT_TYPE}}` 创建 .gitignore 文件,**必须包含**以下关键项: - - Node/React: node_modules/, build/, coverage/ - - Go: *.exe, *.test, *.out, *.dll, *.so, *.dylib - - Python: __pycache__/, *.pyc, .venv/ - - 通用关键项: data/data.json, *.log, .DS_Store, Thumbs.db - - (允许根据实际项目类型额外补充,但不得删除上述关键项) -4. **添加并提交**:执行 `git add .` 和 `git commit -m "初始化提交"`。 -5. **添加远程仓库(使用SSH 协议)**:执行 `git remote add gitea ssh://git@repo.shumengya.top:8022/shumengya/{{REPO}}.git`。若已存在 gitea 远程,先删除后添加。 -6. **推送到远程 main 分支**:执行 `git push -u gitea main`。 - -## 3. 输出协议 -按以下顺序输出: -1. **执行状态**:每步成功或失败,失败时终止并提示原因。 -2. **最终交付**:仅一个结果——本地仓库已初始化、.gitignore 已配置、初始提交已创建、远程仓库已添加、推送成功。 -3. **推送结果**:显示推送的 commit 哈希和远程分支。 - -## 4. 异常情况处理 -- **SSH 连接失败**:提示检查 SSH 密钥是否配置,并给出配置指引。 -- **远程仓库已存在但地址错误**:提示使用 `git remote set-url` 更正。 -- **main 分支已存在且有提交**:提示当前分支已有提交,建议先拉取合并或使用 `--force-with-lease`(需用户确认)。 -- **{{REPO}} 缺失**:挂起,要求补充仓库名。 -- **非预期文件变更**:若 `git add .` 后出现大量非预期文件,提示用户检查 .gitignore 并询问是否重置。 - -## 5. 动态 Few-Shot 示例 -[输入] 项目类型为 React,仓库名为 demo -[内部逻辑] 检测到缺失 {{REPO}},要求补充。补充后执行 git init → 创建 main → 生成 .gitignore 含 node_modules/ → 添加并提交 → 添加远程 → 推送。 -[输出] 每步成功,最终显示“推送成功:commit abc123 已推送到 gitea/main”。 - -[输入] 项目类型为 Python,仓库名为 data-pipeline,当前目录存在 __pycache__/ -[内部逻辑] 检测到 {{REPO}} 存在,直接执行。git init 后创建 main 分支,生成 .gitignore 包含 __pycache__/、*.pyc、data/data.json,执行 git add . 时 __pycache__/ 被忽略,提交成功,添加远程,推送成功。 -[输出] 每步成功,最终显示“推送成功:commit def456 已推送到 gitea/main”。 - -## 6. 防漂移规则 -- 禁止使用 HTTP/HTTPS 协议替代 SSH。 -- 禁止删除或修改 .gitignore 中的关键忽略项(node_modules/、data/data.json 等)。 -- 禁止使用非指定的远程仓库地址或非 main 分支。 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/开发Windows软件细节.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/Windows开发/开发Windows软件细节.md similarity index 100% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/开发Windows软件细节.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/Windows开发/开发Windows软件细节.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/PWA应用启动动画.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/PWA应用启动动画.md similarity index 100% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/PWA应用启动动画.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/PWA应用启动动画.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端全局字体样式提示词.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端全局字体样式提示词.md new file mode 100644 index 0000000..f69cc1a --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端全局字体样式提示词.md @@ -0,0 +1,6 @@ + +前端网站默认字体栈约定: +- 有网:默认使用思源黑体,尽量使用国内外都能正常加载的字体CDN +- 无网/外链失败默认:跳过 Webfont,从 PingFang SC → Hiragino Sans GB → Microsoft YaHei → Segoe UI → system-ui → sans-serif 仅用系统无衬线 + + diff --git a/mengyanote-backend/data/mengyanote/临时/灵创第13周周报.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端网站图标获取方式.md similarity index 100% rename from mengyanote-backend/data/mengyanote/临时/灵创第13周周报.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端网站图标获取方式.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端进入管理员后台方式.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端进入管理员后台方式.md new file mode 100644 index 0000000..5b559f6 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端进入管理员后台方式.md @@ -0,0 +1,2 @@ +点击网站logo图标五次弹出一个极简输入框,输入正确的token(默认内网token为shumengya520)后进入前端管理员后台 + diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端随机背景图.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端随机背景图.md new file mode 100644 index 0000000..f9d4687 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/前端随机背景图.md @@ -0,0 +1 @@ +添加前端网站统一随机背景图 随机背景图API请使用: https://randbg.api.smyhub.com 设置背景高斯模糊效果为10% \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/文档类网页模板.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/文档类前端网页模板.md similarity index 100% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/文档类网页模板.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发/文档类前端网页模板.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发默认框架架构.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发默认框架架构.md deleted file mode 100644 index 222e8b2..0000000 --- a/mengyanote-backend/data/mengyanote/AI/提示词工程/前端开发默认框架架构.md +++ /dev/null @@ -1,2 +0,0 @@ -Vite+React+Tailwind CSS -Vite+Vue+Tailwind CSS \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/前端进入管理员后台方式.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/前端进入管理员后台方式.md deleted file mode 100644 index 13233ca..0000000 --- a/mengyanote-backend/data/mengyanote/AI/提示词工程/前端进入管理员后台方式.md +++ /dev/null @@ -1 +0,0 @@ -点击网站logo图标五次弹出一个输入框,输入正确的token后进入后台 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/后端API部署成docker容器.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端API部署成docker容器.md deleted file mode 100644 index fb09f65..0000000 --- a/mengyanote-backend/data/mengyanote/AI/提示词工程/后端API部署成docker容器.md +++ /dev/null @@ -1 +0,0 @@ -在后端文件夹中建立Docker构建相关文件 把后端服务构建打包成Docker容器,对外端口使用:[] 然后我会把这个后端API服务映射成:[]给前端调用,你设置在生产环境下让前端使用这个后端API地址:[],设置容器持久化储存映射路径为:项目目录下的data文件夹 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API在Windows平台构建.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API在Windows平台构建.md new file mode 100644 index 0000000..60d9fc0 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API在Windows平台构建.md @@ -0,0 +1 @@ +把当前项目后端API构建直接构建出linux amd64版本放在后端API目录下的dist目录,然后docker compose部署的时候直接使用这个已构建好的构建产物 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API开发标准规范.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API开发标准规范.md new file mode 100644 index 0000000..c1c1850 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API开发标准规范.md @@ -0,0 +1,47 @@ + +你是一个严格遵循行业最佳实践的后端API开发者。在开发RESTful/GraphQL API时,必须严格遵守以下规范: + +1. **接口设计** + - 使用RESTful原则,资源导向,URL使用名词复数形式 + - HTTP方法语义正确:GET(查)、POST(增)、PUT(改全部)、PATCH(改部分)、DELETE(删) + - 返回数据统一使用JSON格式 + +2. **响应结构**(必须统一) + ```json + { + "code": 200, // 业务状态码 + "message": "success", // 提示信息 + "data": { ... } // 业务数据(失败时可为null) + } + ``` + +3. **命名规范** + - 接口路径、字段名统一使用小写字母 + 下划线(snake_case) + - 变量、函数使用驼峰或snake_case(与语言保持一致) + - 避免缩写,除非是行业公认(如id、url、uuid) + +4. **分页、查询** + - 分页统一使用 `page`、`page_size` 或 `limit`、`offset` + - 查询参数使用filter前缀(如filter_status、filter_created_at) + +5. **版本控制** + - API路径必须带版本:`/api/v1/xxx` + +6. **安全与校验** + - 所有用户输入必须进行严格校验(@Valid / DTO校验) + - 敏感接口必须鉴权(JWT / OAuth) + - 防止SQL注入、XSS等常见攻击 + +7. **错误处理** + - 全局异常捕获,统一返回错误码和友好提示 + - 业务异常使用自定义异常类 + +8. **文档与注释** + - 必须使用Swagger/OpenAPI注解生成接口文档 + - 关键业务逻辑必须添加清晰注释 + +9. **性能与日志** + - 重要接口添加请求耗时日志 + - 避免N+1查询,使用JOIN或批量查询优化 + +请严格按照以上规范进行API开发,代码清晰、结构合理、安全可靠。 diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API接口根路径说明.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API接口根路径说明.md new file mode 100644 index 0000000..901099d --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API接口根路径说明.md @@ -0,0 +1,16 @@ +默认后端API的根路径,如https://infogenie.api.shumengya.top/ 这个路径可以为: +```json +{ + "description": "提供AI模型应用接口,用户认证由萌芽账户认证中心提供", + "endpoints": { + "admin_site": "/api/admin/site/*", + "aimodelapp": "/api/aimodelapp", + "auth": "/api/auth (via 萌芽认证中心)", + "site": "/api/site (含 feature-card-clicks)", + "user": "/api/user" + }, + "message": "万象口袋 后端 API 服务运行中", + "timestamp": "2026-04-19T21:55:03+08:00", + "version": "3.3.0-go" +} +``` \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API部署成docker容器.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API部署成docker容器.md new file mode 100644 index 0000000..db159bb --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端API部署成docker容器.md @@ -0,0 +1 @@ +在后端文件夹中建立Docker构建相关文件 把后端服务构建打包成Docker容器,对外端口使用一个合适的未占用端口 然后我会把这个后端API服务映射成:[]给前端调用,你设置在生产环境下让前端使用这个后端API地址:[],设置容器持久化储存映射路径为:项目目录下的子文件夹data \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端储存由json改为SQLite.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端储存由json改为SQLite.md new file mode 100644 index 0000000..6339981 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端储存由json改为SQLite.md @@ -0,0 +1 @@ +把后端当前储存由json改为SQLite储存方式,记得sql架构符合标准 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端开发接口调试文档.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端开发接口调试文档.md new file mode 100644 index 0000000..6813a11 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/后端开发/后端开发接口调试文档.md @@ -0,0 +1,7 @@ +在当前项目目录下的api-docs中,把当前后端API接口给我做成postman和apifox导入文件,使用Postman Collection v2.1 (.json)规范 +尽量使用postman和apifox都兼容的导入格式, +集合文件中只使用变量,不创建环境,避免变量和环境重复冲突, +默认使用生产环境接口调试 +集合名和文件名默认为API名称小写,比如:meme-api,geoip-api 文件名:meme-api.json geo-api.json +接口集合描述直接简单描述该API的作用即可 +我的默认生产环境是https:// \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/pages重构为worker.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/pages重构为worker.md new file mode 100644 index 0000000..8b67511 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/pages重构为worker.md @@ -0,0 +1,5 @@ +给我把这个pages项目重构成CloudFlare workers项目 +要兼容以前的d1数据库 +前端改成React+vite +保证能使用wrangler deploy 一键部署整个前后端 +worker.js只打包进API,防止超出CloudFlare worker的大小限制 4mb \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/一键启动前后端脚本.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/一键启动前后端脚本-create-project-start-script.md similarity index 100% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/一键启动前后端脚本.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/一键启动前后端脚本-create-project-start-script.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/代码使用中文注释.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/代码使用中文注释-chinese.md similarity index 52% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/代码使用中文注释.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/代码使用中文注释-chinese.md index 908400c..60c2e80 100644 --- a/mengyanote-backend/data/mengyanote/AI/提示词工程/代码使用中文注释.md +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/代码使用中文注释-chinese.md @@ -1 +1 @@ -代码注释给我默认使用中文注释,不要使用英文注释 \ No newline at end of file +代码注释给我默认使用中文注释,不要使用英文注释,如果原本有英文注释,请给我改成中文注释 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/前端后端开发默认框架架构.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/前端后端开发默认框架架构.md new file mode 100644 index 0000000..7a1f6d7 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/前端后端开发默认框架架构.md @@ -0,0 +1,17 @@ +### 前端 +Vite+React+Tailwind +Vite+Vue+Tailwind +Vite+React +Vite+Vue + +### 后端 +Golang+Gin +Python+Flask +Python+FastAPI +Java+SpringBoot + +### 数据库储存 +MySQL +Redis +MongoDB +SQLite \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/模块化代码架构.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/模块化代码架构.md similarity index 100% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/模块化代码架构.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/模块化代码架构.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/项目英文起名.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/项目英文起名-englishname.md similarity index 100% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/项目英文起名.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/开发通用提示词/项目英文起名-englishname.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/网站logo绘画.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/网站logo绘画.md deleted file mode 100644 index b1fb3cb..0000000 --- a/mengyanote-backend/data/mengyanote/AI/提示词工程/网站logo绘画.md +++ /dev/null @@ -1 +0,0 @@ -请你以专业的app软件logo图标设计师的角度给我设计10张logo图片,要浅色柔和背景,图片中不能出现任何文字 比例1:1,我的软件名称为:萌芽 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟后端AI开发面试者系统提示词.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟后端AI开发面试者系统提示词.md new file mode 100644 index 0000000..7693fd6 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟后端AI开发面试者系统提示词.md @@ -0,0 +1,22 @@ + +你是一位正在面试字节跳动的高级工程师候选人,有 5~8 年后端开发经验,主攻方向为 Golang 或 Java,同时对 AI 大模型工程化(推理优化、微调、RAG、Agent 框架等)有深入的实践积累。 + +【角色设定】 +- 曾在中大型互联网公司担任后端 Tech Lead 或高级工程师 +- 熟悉高并发、分布式系统设计,有真实的线上大规模系统经验 +- 对 LLM 工程化有一线落地经验(非纸上谈兵) +- 技术判断力强,能快速权衡方案 trade-off + +【回答规范】 +1. **长度**:中等篇幅,点到为止。核心观点 + 关键细节即可,不铺垫废话,不无限展开 +2. **结构**:优先用简短的要点列表或小标题,代码示例控制在关键片段,无需完整可运行代码 +3. **语气**:自信、务实,像在和技术同行交流,不卑不亢,偶尔可以提出自己的见解或反问 +4. **深度**:主动体现技术深度——说到某技术点时,可主动提及踩过的坑、做过的优化或与其他方案的对比 +5. **真实感**:可以适当表达不确定(如"这块我了解不深"),避免万能模板式的完美回答 + +【格式约束】 +- 使用轻量 Markdown(`代码`、**加粗**、列表、三级标题),不准使用分割线 +- 禁止大量堆砌内容,单个回答一般不超过 300 字 +- 不以"当然!""非常好的问题!"等奉承语开头 + +现在面试开始,请等待面试官提问。 diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟算法大师.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟算法大师.md new file mode 100644 index 0000000..fc91ab2 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟算法大师.md @@ -0,0 +1,108 @@ +# 角色:AI 算法大师 + +你是一位资深算法导师,专门帮助 Python 零基础新手攻克 LeetCode Hot 100 题目。 +讲解风格:耐心细致,从最直观的解法切入,循序渐进引导学习者理解算法思想与实现。 + +--- + +## 行为规则 + +1. **无需打招呼**,收到题目后直接输出内容,第一行必须是标签行。 +2. 若题目描述不完整,礼貌询问补充,或基于已知原题直接作答。 +3. 禁止使用"显然""很容易""不难发现"等跳过关键步骤的措辞。 +4. 始终假设用户只掌握 Python 基础(变量、循环、列表、字典、函数)。 + +--- + +## 输出结构(严格按顺序) + +### 第一行:标签 +格式固定为: + +**标签**:[标签1]、[标签2]、[标签3] + +- 提取本题核心算法 / 数据结构,3~4 个,中文顿号分隔。 +- 示例:`**标签**:数组、哈希表、双指针` + +--- + +### 正文结构 + +# [题目名称] + +## 题目理解 +用自己的话重新描述题意(不照搬原文),确保零基础用户看懂: +- 输入是什么?输出是什么? +- 有哪些约束条件? +- 用一个具体小例子辅助说明。 + +--- + +## 解法一:[名称](最简单解法,必须首位) + +> 原则:优先选最直观的暴力解法,让用户先看懂逻辑,效率次之。 + +### 思路 +- 解释"为什么想到用这种方法" +- 用文字逐步描述解题过程,不要跳步 + +### 代码 +​```python +class Solution: + def methodName(self, ...): + # 每一个关键操作都写行内注释 + # 第一次出现的内置函数/语法(如 enumerate、zip)必须注释说明用法 + ... + +# 测试示例 +sol = Solution() +print(sol.methodName(...)) # 期望输出:... +​``` + +### 复杂度分析 +| 维度 | 复杂度 | 说明 | +|------|--------|------| +| 时间 | O(?) | 简要说明来源 | +| 空间 | O(?) | 简要说明来源 | + +### 边界情况 +列出需要注意的边界条件及代码如何处理,例如: +- 空数组 / 单元素 +- 有重复值 +- 目标值不存在(若题目保证有解则注明) + +--- + +## 解法二:[名称](优化解法,视题目情况提供) + +> 说明本解法相对解法一的改进点(时间 / 空间 / 代码简洁度)。 + +### 思路 +(同上,详细描述) + +### 代码 +(同上,含注释和测试) + +### 复杂度分析 +(同上,并与解法一对比) + +--- + +## 解法对比总结(有多解法时必须提供) + +| 解法 | 时间复杂度 | 空间复杂度 | 推荐场景 | +|------|-----------|-----------|----------| +| 解法一:暴力枚举 | O(n²) | O(1) | 理解思路、数据量小 | +| 解法二:哈希表 | O(n) | O(n) | 面试首选 | + +--- + +## 注释规范(代码内) + +以下情形**必须**写注释: +- 每个循环的目的 +- 条件判断的含义 +- 第一次出现的 Python 内置函数或语法糖 +- 关键变量的含义(用 `# 变量名:含义` 格式) + +好的,现在你将等待用户向你发送算法题目,如果你已经准备好,请回复"我已准备好,请发送算法题目" \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟项目经历面试官.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟项目经历面试官.md new file mode 100644 index 0000000..f84aad2 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/角色扮演/模拟项目经历面试官.md @@ -0,0 +1,54 @@ +你是一位字节跳动技术面试模拟器,同时扮演面试官和候选人两个角色。 + +## 第一步:读取并分析项目 + +主动读取项目源码,重点关注: +- 依赖文件(go.mod / pom.xml / requirements.txt / package.json 等) +- 配置文件(数据库、中间件、服务发现等) +- 核心目录结构(service / handler / repository 层划分) +- 关键业务模块代码(并发处理、缓存策略、数据库交互等) + +分析完成后,先输出一段简短的「项目技术栈总结」: +- 语言 & 框架 +- 存储 & 中间件 +- 核心业务模块 +- 识别到的技术难点或亮点 + +## 第二步:出八股和项目面试题 + +基于代码中实际用到的技术,出题要「有据可查」——题目要能对应到具体文件或代码片段。 + +- 【八股】:考察项目用到的底层原理(如用了 Redis 就考 Redis 数据结构/过期策略/集群,用了 Kafka 就考消息顺序/幂等) +- 【项目】:针对代码中看到的具体实现追问(如"你这里用了本地缓存 + Redis 双层缓存,一致性怎么保证?") + +## 第三步:逐题给出候选人回答 + +候选人背景:5~8 年后端经验,主攻 Golang/Java,有 LLM 工程化一线经验。 + +回答风格: +- 自信务实,不铺垫废话 +- 结合项目代码中的实际做法来回答,不泛泛而谈 +- 主动提及踩过的坑或与其他方案的 trade-off +- 单题回答 200~300 字,轻量 Markdown 格式 +- 不以"当然!""非常好的问题!"开头 + +## 输出格式 + +### 项目技术栈分析 +(简短总结) + +--- + +【八股 1】问题 + +回答…… + +--- + +【项目 1】问题 + +回答…… + +--- + +(以此类推) \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/Dokcer部署流程.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/Dokcer部署流程.md similarity index 58% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/Dokcer部署流程.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/Dokcer部署流程.md index 0601009..7ba3e9c 100644 --- a/mengyanote-backend/data/mengyanote/AI/提示词工程/Dokcer部署流程.md +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/Dokcer部署流程.md @@ -2,4 +2,12 @@ - 部署方式:使用 `docker compose`,不得破坏现有容器配置 - 数据持久化:必要时在 `docker-compose.yml` 同级 `data` 目录映射挂载 - 资源限制:所有容器统一设置内存限制 `5GB` -- 管理员认证:默认使用内网密码 `shumengya520`(存在默认认证时使用) \ No newline at end of file +- 设置容器开机自启和报错常识自动重启三次,三次之后不再重启 +- 管理员认证:默认使用内网密码 `shumengya520`(存在默认认证时使用) +- 默认dokcer-compose.yml清除所有代理 + HTTP_PROXY: "" + http_proxy: "" + HTTPS_PROXY: "" + https_proxy: "" + ALL_PROXY: "" + all_proxy: "" diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/SSH初始化与推送Git 仓库.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/SSH初始化与推送Git 仓库.md new file mode 100644 index 0000000..a409853 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/SSH初始化与推送Git 仓库.md @@ -0,0 +1 @@ +在本地 Git 仓库中,根据 `{{PROJECT_TYPE}}` 生成含关键忽略项的 .gitignore,完成初始提交,然后通过 SSH 推送到 `ssh://git@repo.shumengya.top:8022/shumengya/{{REPO}}.git` 的 main 分支。若 `{{REPO}}` 缺失则提示补充,全程仅使用 SSH 且不修改关键忽略项。 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/SSH提交注意事项.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/SSH提交注意事项.md similarity index 100% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/SSH提交注意事项.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/SSH提交注意事项.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/提交代码到自建Gitea仓库.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/提交代码到自建Gitea仓库.md similarity index 100% rename from mengyanote-backend/data/mengyanote/AI/提示词工程/提交代码到自建Gitea仓库.md rename to mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/提交代码到自建Gitea仓库.md diff --git a/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/运维命令审计.md b/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/运维命令审计.md new file mode 100644 index 0000000..fa61270 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/提示词工程/运维部署/运维命令审计.md @@ -0,0 +1 @@ +执行某某操作,然后执行完最后简单总结以顺序列出执行了哪些命令 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/未命名.md b/mengyanote-backend/data/mengyanote/AI/未命名.md new file mode 100644 index 0000000..1ead600 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/未命名.md @@ -0,0 +1,43 @@ +**2026 AI Agent 应用层开发关键词精炼文档** + +### 1. Skills 层(核心能力层) +- **Skills**:Agent 可重用能力包,高于 Tools。包含领域知识、最佳实践、决策逻辑、步骤指南。 +- **Skills vs Tools**:Tools 是执行动作(API/函数调用);Skills 是“如何思考+执行”的专家流程(结构化指令 + 推理逻辑)。 +- **SKILL.md**:主流定义格式,包含元数据、决策准则、Guardrails、输入输出示例。 +- **Skill Engineering**:构建、管理、组合 Skills 的工程实践。 +- **Skill Registry / Discovery**:Skills 注册与动态发现机制。 + +### 2. 应用层核心概念 +- **Agentic Applications**:基于 Agent 的端到端业务应用(自主完成工作流)。 +- **Role-based Agents**:按角色定义(Researcher、Analyst、Executor 等)。 +- **Multi-Agent Orchestration**:多 Agent 协作完成复杂业务目标。 +- **Agentic Workflows**:自主业务流程(规划-执行-反思循环)。 +- **Task Decomposition**:业务目标拆解为子任务。 +- **Domain-specific / Vertical Agents**:行业垂直 Agent(销售、财务、HR、代码等)。 +- **Human-in-the-Loop (HITL)**:关键节点人工审核(合规必备)。 + +### 3. 协议与集成 +- **MCP (Model Context Protocol)**:Agent 连接企业系统、数据、工具的标准协议(“Agent 的 USB-C”)。 +- **A2A (Agent-to-Agent Protocol)**:Agent 间发现、委托、协作互操作标准。 +- **Agent Card**:描述 Agent 能力、Skills 的元数据文件。 + +### 4. 主流开发框架(应用层) +- **CrewAI**:角色-based 多 Agent 团队协作,适合快速构建业务流程。 +- **LangGraph**:图状有状态工作流,生产级复杂应用首选。 +- **Low-code Platforms**:Dify、n8n、LangFlow 等。 +- **其他**:AutoGen、OpenAI Agents SDK、LlamaIndex 等。 + +### 5. 高频应用机制 +- ReAct + Reflection(推理-行动-反思) +- Multi-Modal Agents(文本+图像+文档) +- Memory Layers(短期会话 + 长期业务记忆) +- Guardrails & Observability(安全防护 + 监控评估) + +### 2026 应用层核心趋势 +- 从 Tool Calling 转向 **Skills + MCP + A2A** 组合。 +- 强调生产就绪:状态管理、Human-in-the-Loop、安全 Guardrails、Observability。 +- Agent 从实验原型走向“数字员工”,重点通过 Skills 标准化业务能力。 + +**文档精要**:2026 年应用层开发核心是 **Role-based Multi-Agent + Skills 工程 + MCP/A2A 集成**,目标是构建可靠、可落地、可协作的业务 Agent 系统。 + +需要输出 Markdown 版本、添加具体 Skill 示例,或导出为特定格式吗? \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/AI/表示边界论.md b/mengyanote-backend/data/mengyanote/AI/表示边界论.md new file mode 100644 index 0000000..c7840db --- /dev/null +++ b/mengyanote-backend/data/mengyanote/AI/表示边界论.md @@ -0,0 +1,24 @@ +AI Coding 的效率不取决于模型的能力,而是取决于你所使用的架构是否支持多工作树低冲突合并; +AI 的能力边界也不取决于模型的能力,而是取决于工程过程可被 token 化的范围边界在哪里。 + +--- + +# **表示边界论** + +## 定义 + +人工智能系统直接处理的不是现实本身,而是现实的表示。 +所谓 token,在广义上指一切可被机器编码、传输、存储和运算的符号单元。 +所谓 token 化,指将现实中的对象、状态、关系和过程转化为机器可处理表示的过程。 + +## 基本命题 + +只有进入机器表示系统的对象,才可能被人工智能处理。因而,人工智能的认知边界首先受表示边界约束。 +模型本身只能执行表示变换,而不能直接触及现实。人工智能要对现实产生作用,必须借助接口、工具、传感器与执行系统形成因果闭环。 +算力决定表示处理的规模、速度、成本与可持续性。 + +## 推论 + +人工智能能力的扩张,不只是模型能力的扩张,更是表示边界与接口边界的扩张。 +多模态、工具调用、数据库连接、传感器接入,本质上都是在扩大人工智能可处理和可作用的世界。 +Agent 的价值,不在于替代模型,而在于将模型嵌入现实闭环,使其从“表示变换器”变成“可作用系统”的核心部件。 diff --git a/mengyanote-backend/data/mengyanote/Docker/Docker 镜像相关.md b/mengyanote-backend/data/mengyanote/Docker/Docker 镜像相关.md deleted file mode 100644 index ca13621..0000000 --- a/mengyanote-backend/data/mengyanote/Docker/Docker 镜像相关.md +++ /dev/null @@ -1,67 +0,0 @@ - -**QQ机器人框架NapCat** -```shell -#安装 -docker run -d \ --e NAPCAT_GID=$(id -g) \ --e NAPCAT_UID=$(id -u) \ --p 4080:3000 \ --p 4070:3001 \ --p 4060:6099 \ ---name napcat \ ---restart=always \ -mlikiowa/napcat-docker:latest - -#固化以下路径,方便掉线时快速重新登录 -#QQ 持久化数据路径:/app/.config/QQ -#NapCat 配置文件路径: /app/napcat/config - -#登录 WebUI 后台地址:http://<宿主机ip>:6099/webui -``` - -**网页SSH客户端-EasyNode** -```shell -#项目地址:https://github.com/chaos-zhu/easynode - -# 1. 创建easynode目录 -mkdir -p /root/easynode && cd /root/easynode - -# 2. 下载docker-compose.yml文件 -wget https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/docker-compose.yml - -# 3. 启动服务 -docker-compose up -d - -docker run -d -p 8082:8082 --restart=always -v /root/easynode/db:/easynode/app/db chaoszhu/easynode - -### 监控服务 -#安装 -# 使用默认端口22022安装 -curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash - -# 使用自定义端口安装, 例如54321 -curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash -s -- 54321 - -#卸载 -curl -o- https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash - -#查看监控服务状态:systemctl status easynode-client -#查看监控服务日志: journalctl --follow -u easynode-client -#查看详细日志:journalctl -xe -``` - -**docker加速地址** - -![[Screenshot_20250717_225309.jpg]] - - -docker.1ms.run/ - -docker run -d -p 4050:80 --restart=always -v /shumengya/tu:/var/www/html docker.1ms.run/oaooa/pichome - - - - - - - diff --git a/mengyanote-backend/data/mengyanote/Docker/Docker常用命令总结.md b/mengyanote-backend/data/mengyanote/Docker/Docker常用命令总结.md index 2872c6f..2377e11 100644 --- a/mengyanote-backend/data/mengyanote/Docker/Docker常用命令总结.md +++ b/mengyanote-backend/data/mengyanote/Docker/Docker常用命令总结.md @@ -1,7 +1,5 @@ -# Docker 常用命令总结 ## 🐳 容器生命周期管理 - ### 容器操作 ```bash # 运行容器 @@ -177,6 +175,4 @@ docker exec -it $(docker ps -lq) bash 3. **标签管理**:为镜像打上有意义的标签 4. **清理策略**:定期使用 `prune` 命令清理未使用资源 5. **日志查看**:使用 `docker logs` 调试容器问题 -6. **组合命令**:将常用命令写入脚本或使用 docker-compose - -记住这些命令可以帮助你高效地使用 Docker 进行容器化管理! \ No newline at end of file +6. **组合命令**:将常用命令写入脚本或使用 docker-compose \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/Docker/Docker镜像快速迁移.md b/mengyanote-backend/data/mengyanote/Docker/Docker镜像快速迁移.md index 1925c6f..5fca93d 100644 --- a/mengyanote-backend/data/mengyanote/Docker/Docker镜像快速迁移.md +++ b/mengyanote-backend/data/mengyanote/Docker/Docker镜像快速迁移.md @@ -1,10 +1,8 @@ --- - ## ✅ 方法一:直接导出容器(container → tar) 如果你运行的是一个 **容器**(而不是镜像),可以直接打包它: - ```bash # 导出容器 docker export <容器ID或名字> -o container.tar @@ -16,16 +14,12 @@ docker import container.tar new-image:latest 特点: - 容器的运行时文件系统会被打包(包含你安装的软件和修改),**但不会包含容器的历史层和环境变量**。 - - 类似于“快照”迁移。 - - --- ## ✅ 方法二:保存镜像(image → tar) 如果你已经把容器做成了一个镜像,建议用 **save/load**: - ```bash # 在源主机上保存镜像 docker save -o myimage.tar myimage:latest @@ -39,11 +33,7 @@ docker load -i myimage.tar ``` 特点: - - 保留镜像层和构建历史。 - - 推荐这种方式。 - - --- diff --git a/mengyanote-backend/data/mengyanote/Git/GitHub优秀项目.md b/mengyanote-backend/data/mengyanote/Git/GitHub优秀项目(待更新).md similarity index 100% rename from mengyanote-backend/data/mengyanote/Git/GitHub优秀项目.md rename to mengyanote-backend/data/mengyanote/Git/GitHub优秀项目(待更新).md diff --git a/mengyanote-backend/data/mengyanote/临时/快速开启一个http服务器.md b/mengyanote-backend/data/mengyanote/Linux/快速开启一个HTTP服务器.md similarity index 100% rename from mengyanote-backend/data/mengyanote/临时/快速开启一个http服务器.md rename to mengyanote-backend/data/mengyanote/Linux/快速开启一个HTTP服务器.md diff --git a/mengyanote-backend/data/mengyanote/MarkDown/GitHub版markdown格式大全.md b/mengyanote-backend/data/mengyanote/MarkDown/GitHub版markdown格式大全.md new file mode 100644 index 0000000..31f6b52 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/MarkDown/GitHub版markdown格式大全.md @@ -0,0 +1,538 @@ + + +=========================== + +该文件用来测试和展示书写README的各种markdown语法。GitHub的markdown语法在标准的markdown语法基础上做了扩充,称之为`GitHub Flavored Markdown`。简称`GFM`,GFM在GitHub上有广泛应用,除了README文件外,issues和wiki均支持markdown语法。 + +**** + +|作者|果冻虾仁| +|---|--- +|知乎|[![zhihu-shield]][zhihu] +|公众号|编程往事 + + +**** +## 目录 +* [横线](#横线) +* [标题](#标题) +* [文本](#文本) + * 普通文本 + * 单行文本 + * 多行文本 + * 文字高亮 + * 换行 + * 斜体 + * 粗体 + * 删除线 +* [图片](#图片) + * 来源于网络的图片 + * GitHub仓库中的图片 +* [链接](#链接) + * 文字超链接 + * 链接外部URL + * 链接本仓库里的URL + * 锚点 + * [图片链接](#图片链接) +* [列表](#列表) + * 无序列表 + * 有序列表 + * 复选框列表 +* [块引用](#块引用) +* [代码高亮](#代码高亮) +* [表格](#表格) +* [Alerts](#Alerts) +* [表情](#表情) +* [diff语法](#diff语法) +* [常用HTML语法](#常用HTML语法) + * [折叠](#折叠) + * [居中](#居中) +* [其他](#其他) + * [徽章](#徽章) + * [star历史](#star历史) + +### 横线 +----------- +***、---、___可以显示横线效果 + +*** +--- +___ + + + +## 标题 +``` +# 一级标题 +## 二级标题 +### 三级标题 +#### 四级标题 +##### 五级标题 +###### 六级标题 +``` +效果如下: + +# 一级标题 +## 二级标题 +### 三级标题 +#### 四级标题 +##### 五级标题 +###### 六级标题 + + +## 文本 +### 普通文本 +这是一段普通的文本 +### 单行文本 + Hello,大家好,我是果冻虾仁。 +在一行开头加入1个Tab或者4个空格。 +### 文本块 +#### 语法1 +在连续几行的文本开头加入1个Tab或者4个空格。 + + 欢迎到访 + 很高兴见到您 + 祝您,早上好,中午好,下午好,晚安 + +#### 语法2 +使用一对各三个的反引号: +``` +欢迎到访 +我是C++码农 +你可以在知乎、CSDN、简书搜索【果冻虾仁】找到我 +``` +该语法也可以实现代码高亮,见[代码高亮](#代码高亮) +### 文字高亮 +文字高亮功能能使行内部分文字高亮,使用一对反引号。 +语法: +``` +`linux` `网络编程` `socket` `epoll` +``` +效果:`linux` `网络编程` `socket` `epoll` + +也适合做一篇文章的tag +#### 换行 +直接回车不能换行, +可以在上一行文本后面补两个空格, +这样下一行的文本就换行了。 + +或者就是在两行文本直接加一个空行。 + +也能实现换行效果,不过这个行间距有点大。 +#### 斜体、粗体、删除线 + +|语法|效果| +|----|-----| +|`*斜体1*`|*斜体1*| +|`_斜体2_`| _斜体2_| +|`**粗体1**`|**粗体1**| +|`__粗体2__`|__粗体2__| +|`这是一个 ~~删除线~~`|这是一个 ~~删除线~~| +|`***斜粗体1***`|***斜粗体1***| +|`___斜粗体2___`|___斜粗体2___| +|`***~~斜粗体删除线1~~***`|***~~斜粗体删除线1~~***| +|`~~***斜粗体删除线2***~~`|~~***斜粗体删除线2***~~| + + 斜体、粗体、删除线可混合使用 + +## 图片 +基本格式: +``` +![alt](URL title) +``` +alt和title即对应HTML中的alt和title属性(都可省略): +- alt表示图片显示失败时的替换文本 +- title表示鼠标悬停在图片时的显示文本(注意这里要加引号) + +URL即图片的url地址,如果引用本仓库中的图片,直接使用**相对路径**就可了,如果引用其他github仓库中的图片要注意格式,即:`仓库地址/raw/分支名/图片路径`,如: +``` +https://github.com/guodongxiaren/ImageCache/raw/master/Logo/foryou.gif +``` + +|#|语法|效果| +|---|---|---- +|1|`![baidu](http://www.baidu.com/img/bdlogo.gif "百度logo")`|![baidu](http://www.baidu.com/img/bdlogo.gif "百度logo") +|2|`![][code-past]`|![][code-past] + +注意例2的写法使用了**URL标识符**的形式,在[链接](#链接)一节有介绍。 +>在文末有code-past的定义: +``` +[code-past]:/img/codepast-logo.jpg "公众号:编程往事" +``` + +## 链接 +### 链接外部URL + +|#|语法|效果| +|---|----|-----| +|1|`[我的博客](http://blog.csdn.net/guodongxiaren "悬停显示")`|[我的博客](http://blog.csdn.net/guodongxiaren "悬停显示")| +|2|`[我的知乎][zhihu] `|[我的知乎][zhihu] | + +语法2由两部分组成: +- 第一部分使用两个中括号,[ ]里的标识符(本例中zhihu),可以是数字,字母等的组合,标识符上下对应就行了(**姑且称之为URL标识符**) +- 第二部分标记实际URL。 + +>使用URL标识符能达到复用的目的,一般把全文所有的URL标识符统一放在文章末尾,这样看起来比较干净。除了干净之外,它还能达到复用的目的,比如你在多个地方想使用同一个链接,那么文内使用标识符,只在最底部给标识符定义出实际的URL链接即可,类似编程语言中的变量。 +>>URL标识符是我起的名字,不知道是否准确。囧。。 + +### 链接本仓库里的URL + +|语法|效果| +|----|-----| +|`[我的简介](/example/profile.md)`|[我的简介](/example/profile.md)| +|`[example](./example)`|[example](./example)| + +### 图片链接 +给图片加链接的本质是混合图片显示语法和普通的链接语法。普通的链接中[ ]内部是链接要显示的文本,而图片链接[ ]里面则是要显示的图片。 +直接混合两种语法当然可以,但是十分啰嗦,为此我们可以使用URL标识符的形式。 + +|#|语法|效果| +|---|----|:---:| +|1|`[![weibo-logo]](http://weibo.com/linpiaochen)`|[![weibo-logo]](http://weibo.com/linpiaochen)| +|2|`[![](/img/zhihu.png "我的知乎,欢迎关注")][zhihu]`|[![](/img/zhihu.png "我的知乎,欢迎关注")][zhihu]| +|3|`[![csdn-logo]][csdn]`|[![csdn-logo]][csdn]| + +因为图片本身和链接本身都支持URL标识符的形式,所以图片链接也可以很简洁(见例3)。 +注意,此时鼠标悬停时显示的文字是图片的title,而非链接本身的title了。 +> 本文URL标识符都放置于文末 + +### 锚点 +其实呢,每一个标题都是一个锚点,和HTML的锚点(`#`)类似,比如我们 + +|语法|效果| +|---|---| +|`[回到顶部](#readme)`|[回到顶部](#readme)| + +不过要注意,标题中的英文字母都被转化为**小写字母**了。 +> 以前GitHub对中文支持的不好,所以中文标题不能正确识别为锚点,但是现在已经没问题啦! + +## 列表 +### 无序列表 +#### 语法 +``` +* 昵称:果冻虾仁 +- 别名:隔壁老王 +* 英文名:Jelly +``` +#### 效果 +* 昵称:果冻虾仁 +- 别名:隔壁老王 +* 英文名:Jelly + +### 多级无序列表 +#### 语法 +``` +* 编程语言 + * 脚本语言 + * Python +``` +#### 效果 +* 编程语言 + * 脚本语言 + * Python + +### 一级有序列表 +#### 语法 +就是在数字后面加一个点,再加一个空格。不过看起来起来可能不够明显。 +``` +面向对象的三个基本特征: + +1. 封装 +2. 继承 +3. 多态 +``` + +#### 效果 +面向对象的三个基本特征: + +1. 封装 +2. 继承 +3. 多态 + + +### 多级有序列表 +和无序列表一样,有序列表也有多级结构。 +#### 语法 +``` +1. 这是一级的有序列表,数字1还是1 + 1. 这是二级的有序列表,阿拉伯数字在显示的时候变成了罗马数字 + 1. 这是三级的有序列表,数字在显示的时候变成了英文字母 +``` + +#### 效果 + +1. 这是一级的有序列表,数字1还是1 + 1. 这是二级的有序列表,阿拉伯数字在显示的时候变成了罗马数字 + 1. 这是三级的有序列表,数字在显示的时候变成了英文字母 + + +### 复选框列表 +#### 语法 +``` +- [x] 需求分析 +- [x] 系统设计 +- [x] 详细设计 +- [ ] 编码 +- [ ] 测试 +- [ ] 交付 +``` +#### 效果 + +- [x] 需求分析 +- [x] 系统设计 +- [x] 详细设计 +- [ ] 编码 +- [ ] 测试 +- [ ] 交付 + +您可以使用这个功能来标注某个项目各项任务的完成情况。 +> Tip: +>> 在GitHub的**issue**中使用该语法是可以实时点击复选框来勾选或解除勾选的,而无需修改issue原文。 + +## 块引用 + +### 常用于引用文本 +#### 文本摘自《深入理解计算机系统》P27 + 令人吃惊的是,在哪种字节顺序是合适的这个问题上,人们表现得非常情绪化。实际上术语“little endian”(小端)和“big endian”(大端)出自Jonathan Swift的《格利佛游记》一书,其中交战的两个派别无法就应该从哪一端打开一个半熟的鸡蛋达成一致。因此,争论沦为关于社会政治的争论。只要选择了一种规则并且始终如一的坚持,其实对于哪种字节排序的选择都是任意的。 +> **“端”(endian)的起源** +以下是Jonathan Swift在1726年关于大小端之争历史的描述: +“……下面我要告诉你的是,Lilliput和Blefuscu这两大强国在过去36个月里一直在苦战。战争开始是由于以下的原因:我们大家都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端,可是当今的皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了,因此他的父亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打破较小的一端,违令者重罚。” + +### 块引用有多级结构 +#### 语法 +``` +> 数据结构 +>> 树 +>>> 二叉树 +>>>> 平衡二叉树 +>>>>> 满二叉树 +``` +#### 效果 +> 数据结构 +>> 树 +>>> 二叉树 +>>>> 平衡二叉树 +>>>>> 满二叉树 + +## 代码高亮 + +### 语法 +在三个反引号后面加上编程语言的名字,另起一行开始写代码,最后一行再加上三个反引号。 + +### 效果 +```Java +public static void main(String[]args){} //Java +``` +```c +int main(int argc, char *argv[]) //C +``` +```Bash +echo "hello GitHub" #Bash +``` +```javascript +document.getElementById("myH1").innerHTML="Welcome to my Homepage"; //javascipt +``` +```cpp +string &operator+(const string& A,const string& B) //cpp +``` +## 表格 +基本语法: +``` +| 表头1 | 表头2| +| ---------- | -----------| +| 表格单元 | 表格单元 | +| 表格单元 | 表格单元 | +``` + +| 表头1 | 表头2| +| ---------- | -----------| +| 表格单元 | 表格单元 | +| 表格单元 | 表格单元 | + +简洁语法: +``` +表头1 | 表头2| +--------- | --------| +表格单元 | 表格单元 | +表格单元 | 表格单元 | +``` +表头1 | 表头2| +--------- | --------| +表格单元 | 表格单元 | +表格单元 | 表格单元 | + + +### 对齐 +表格可以指定对齐方式 +``` +| 左对齐 | 居中 | 右对齐 | +| :------------ |:---------------:| -----:| +| col 3 is | some wordy text | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | +``` + + +| 左对齐 | 居中 | 右对齐 | +| :------------ |:---------------:| -----:| +| col 3 is | some wordy text | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +### 混合其他语法 +表格单元中的内容可以和其他大多数GFM语法配合使用,如: +#### 使用普通文本的删除线,斜体等效果 + +| 名字 | 描述 | +| ------------- | ----------- | +| Help | ~~Display the~~ help window.| +| Close | _Closes_ a window | + +#### 表格中嵌入图片(链接) +其实前面介绍图片显示、图片链接的时候为了清晰就是放在在表格中显示的。 + +| 图片 | 描述 | +| ---- | ---- | +|![baidu][baidu-logo] | 百度| + +## Alerts +``` +> [!NOTE] +> 这是一个提示信息 + +> [!TIP] +> 这是一个技巧提示 + +> [!IMPORTANT] +> 这是重要信息 + +> [!WARNING] +> 这是警告信息 + +> [!CAUTION] +> 这是危险警告 +``` +效果如下: +> [!NOTE] +> 这是一个提示信息 + +> [!TIP] +> 这是一个技巧提示 + +> [!IMPORTANT] +> 这是重要信息 + +> [!WARNING] +> 这是警告信息 + +> [!CAUTION] +> 这是危险警告 + +## 表情 +Github的Markdown语法支持添加emoji表情,输入不同的符号码(两个冒号包围的字符)可以显示出不同的表情。 + +比如`:blush:`,可以显示:blush:。 + +具体每一个表情的符号码,可以查询GitHub的官方网页[http://www.emoji-cheat-sheet.com](http://www.emoji-cheat-sheet.com)。 + +但是这个网页每次都打开**奇慢**。。所以我整理到了本repo中,大家可以直接在此查看[emoji](./emoji.md)。 + +## diff语法 +版本控制的系统中都少不了diff的功能,即展示一个文件内容的增加与删除。 +GFM中可以显示的展示diff效果。使用绿色表示新增,红色表示删除。 +#### 语法 +其语法与代码高亮类似,只是在三个反引号后面写diff, +并且其内容中,可以用 `+ `开头表示新增,`- `开头表示删除。 +另外还有有 `!`和`#`的语法。 + +#### 效果 + +```diff ++ 人闲桂花落, +- 夜静春山空。 +! 月出惊山鸟, +# 时鸣春涧中。 +``` +## 常用HTML语法 +markdown是支持HTML语法的,虽然不鼓励大量使用HTML语法,毕竟那样就丧失了markdown的意义,但是有一些HTML语法在写README的时候是很好的补充。 +### 折叠 +``` +
+Linux环境 + +##### 编译 +xxxx + +##### 安装 +xxxx +
+``` +
+Linux环境 + +##### 编译 +xxxx + +##### 安装 +xxxx + +
+ +### 居中 + +很多地方都会用到居中的效果,比如如下内容将会把一个表格在页面中居中展示: + +``` +
+ +| 表头1 | 表头2| +| ---------- | -----------| +| 表格单元 | 表格单元 | +| 表格单元 | 表格单元 | + +
+``` + +
+ +| 表头1 | 表头2| +| ---------- | -----------| +| 表格单元 | 表格单元 | +| 表格单元 | 表格单元 | + +
+ +其他任意需要居中展示的语法,都可以放在其中。 + +## 其他 +还有一些非markdown语法,但是在README文件中也很实用的组件。 +### 徽章 +绘制徽章,首选就是[shields.io](https://shields.io/) 具体语法去官网探索。 + +![LICENSE](https://img.shields.io/badge/license-MIT-green) +![Author](https://img.shields.io/badge/Author-guodongxiaren-blue.svg) +![zhihu-shield] + +其次有些第三方平台也提供方便的徽章,比如gitter: + +[![Join the chat at https://gitter.im/guodongxiaren/README](https://badges.gitter.im/guodongxiaren/README.svg)](https://gitter.im/guodongxiaren/README?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +### star历史 +star历史可以使用这个网站[star-history.com](https://star-history.com/) +``` +[![Star History Chart](https://api.star-history.com/svg?repos=guodongxiaren/README&type=Date)](https://star-history.com/#guodongxiaren/README&Date) +``` +这段代码的显示效果如下: + +[![Star History Chart](https://api.star-history.com/svg?repos=guodongxiaren/README&type=Date)](https://star-history.com/#guodongxiaren/README&Date) + + +-------------------------------- +[csdn]:http://blog.csdn.net/guodongxiaren "我的博客" +[zhihu]:https://www.zhihu.com/people/guodongxiaren "我的知乎,欢迎关注" +[weibo]:http://weibo.com/linpiaochen +[baidu-logo]:http://www.baidu.com/img/bdlogo.gif "百度logo" +[weibo-logo]:/img/weibo.png "点击图片进入我的微博" +[csdn-logo]:/img/csdn.png "我的CSDN博客" +[code-past]:/img/codepast-logo.jpg "公众号:编程往事" +[zhihu-shield]:https://img.shields.io/badge/dynamic/json?color=0084ff&logo=zhihu&label=%E6%9E%9C%E5%86%BB%E8%99%BE%E4%BB%81&query=%24.data.totalSubs&url=https%3A%2F%2Fapi.spencerwoo.com%2Fsubstats%2F%3Fsource%3Dzhihu%26queryKey%3Dguodongxiaren diff --git a/mengyanote-backend/data/mengyanote/MarkDown/markdown-cheat-sheet.md b/mengyanote-backend/data/mengyanote/MarkDown/markdown-cheat-sheet.md new file mode 100644 index 0000000..c94cb27 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/MarkDown/markdown-cheat-sheet.md @@ -0,0 +1,119 @@ +# Markdown Cheat Sheet + +Thanks for visiting [The Markdown Guide](https://www.markdownguide.org)! + +This Markdown cheat sheet provides a quick overview of all the Markdown syntax elements. It can’t cover every edge case, so if you need more information about any of these elements, refer to the reference guides for [basic syntax](https://www.markdownguide.org/basic-syntax/) and [extended syntax](https://www.markdownguide.org/extended-syntax/). + +## Basic Syntax + +These are the elements outlined in John Gruber’s original design document. All Markdown applications support these elements. + +### Heading + +# H1 +## H2 +### H3 + +### Bold + +**bold text** + +### Italic + +*italicized text* + +### Blockquote + +> blockquote + +### Ordered List + +1. First item +2. Second item +3. Third item + +### Unordered List + +- First item +- Second item +- Third item + +### Code + +`code` + +### Horizontal Rule + +--- + +### Link + +[Markdown Guide](https://www.markdownguide.org) + +### Image + +![alt text](https://www.markdownguide.org/assets/images/tux.png) + +## Extended Syntax + +These elements extend the basic syntax by adding additional features. Not all Markdown applications support these elements. + +### Table + +| Syntax | Description | +| ----------- | ----------- | +| Header | Title | +| Paragraph | Text | + +### Fenced Code Block + +``` +{ + "firstName": "John", + "lastName": "Smith", + "age": 25 +} +``` + +### Footnote + +Here's a sentence with a footnote. [^1] + +[^1]: This is the footnote. + +### Heading ID + +### My Great Heading {#custom-id} + +### Definition List + +term +: definition + +### Strikethrough + +~~The world is flat.~~ + +### Task List + +- [x] Write the press release +- [ ] Update the website +- [ ] Contact the media + +### Emoji + +That is so funny! :joy: + +(See also [Copying and Pasting Emoji](https://www.markdownguide.org/extended-syntax/#copying-and-pasting-emoji)) + +### Highlight + +I need to highlight these ==very important words==. + +### Subscript + +H~2~O + +### Superscript + +X^2^ \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/MarkDown/markdown实用格式.md b/mengyanote-backend/data/mengyanote/MarkDown/markdown实用格式.md new file mode 100644 index 0000000..745278b --- /dev/null +++ b/mengyanote-backend/data/mengyanote/MarkDown/markdown实用格式.md @@ -0,0 +1,479 @@ +

The Complete Guide of
Readme Markdown Syntax

+ +Markdown is a syntax for styling all forms of writing on the GitHub platform. +Mostly, it is just regular text with a few non-alphabetic characters thrown in, like ```git # or * ``` + +You can use Markdown most places around GitHub: + +1. Gists +2. Comments in Issues and Pull Requests +3. Files with the .md or .markdown extension + +___ + +### Headers + +```git +# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 +###### Heading 6 +``` + +# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 +###### Heading 6 + +___ + +### Font + +```git +*Italics* +_This will also be italic_ +**Bold text** +__This will also be bold__ +***Bold and Italics*** +_You **can** combine them_ +~~Striked Text~~ +***~~Italic, bold, and strikethrough1~~*** +``` + +*Italics*
+_This will also be italic_
+**Bold Text**
+__This will also be bold__
+***Bold and Italics***
+_You **can** combine them_
+~~Striked Text~~
+***~~Italic, bold, and strikethrough1~~*** +___ + +### Lists + +> Unordered + +```git +* Item 1 +* Item 2 + * Item 1a + * Item 2a + * Item 1b + * Item 2b +``` + +* Item 1 +* Item 2 + * Item 1a + * Item 2a + * Item 1b + * Item 2b + +OR +`- Item 1` +- Item 1 + +> Ordered + +```git +1. First +2. jhg + 1. Second + 2. jhg + 1. Third + 2. jhg +``` + +1. First +2. jhg + 1. Second + 2. jhg + 1. Third + 2. jhg + +___ + +### Links + +```git +* [Link with more info with various formatting options](https://docs.github.com/en/github/writing-on-github "more info") +* https://www.google.com/ +* +``` + +* [Link with more info with various formatting options](https://docs.github.com/en/github/writing-on-github "more info") +* https://www.google.com/ +* + +### Link Label +```git +[My GitHub][GitHubLink] +``` +[My GitHub][GitHubLink] + +You may define your link label anywhere in the document. +```git +e.g. put on bottom: + +-------------------------------- +[GitHubLink]:https://github.com/darsaveli +``` + +### Links to the URLs in a repository + +```git +[Example document](/example/example.md) +``` + +[Example document](/example/example.md) + +```git +[example](./example) +``` + +[example](./example) + +___ + +### Inserting Images or Gifs using links + +```git +![alt](URL "title") +``` + +- alt in square bracket indicates the replacement text when the image fails to display (can be omitted) +- parenthesis contains image source +- title in quotes indicates the text to display when the mouse hovers over the image (can be omitted) + +Nite: Dropping the image to the readme file will upload it automatically with this syntax; +It's the same as links, but add an exlamation mark (!) before opening square bracket; +Image source can be either a location from the local machine or any valid image URL; + +>Example + +```git +![Octocat](https://user-images.githubusercontent.com/81953271/124010886-b571ca80-d9df-11eb-86ac-b358c48ac6aa.png "Github logo") +``` +![Octocat](https://user-images.githubusercontent.com/81953271/124010886-b571ca80-d9df-11eb-86ac-b358c48ac6aa.png "Github logo") + +### Resize images/Gifs +``` + +``` + + +You can use HTML tags like width="385px", hight="876px", align="center", etc depending what you need. In this case this gif was once uploaded to the repository and the link was taken from there. + +Other options to resize: +- `![](https:// link | width=100)` + +___ + +### Linking Image/Gif +To open another webpage when image is clicked, enclose the Markdown for the image in brackets, and then add the link in parentheses. + +``` +[![Octocat](https://user-images.githubusercontent.com/81953271/124010886-b571ca80-d9df-11eb-86ac-b358c48ac6aa.png "GitHub Logo")](https://github.com/) + +``` + +[![Octocat](https://user-images.githubusercontent.com/81953271/124010886-b571ca80-d9df-11eb-86ac-b358c48ac6aa.png "GitHub Logo")](https://github.com/) + +___ + +### Tables + +```git +|Header1|Header2|Header3| +| --- | --- | --- | +| This | is a | table | +| This | is 2nd | row | +| This | is 3rd | row | +``` + +|Header1|Header2|Header3| +| --- | --- | --- | +| This | is a | table | +| This | is 2nd | row | +| This | is 3rd | row | + +### Align +You may specify alignment like this: +```git +| Align left | Centered | Align right | +| :------------ |:---------------:| -----:| +| col 3 is | some wordy text | $1600 | +``` +| Align left | Centered | Align right | +| :------------ |:---------------:| -----:| +| aaa | bbb | ccc | + +p.s. You can use alignment with `

your text

` tags or with `

your text

` tag to align plain text. +___ + +### CheckBox + +```git +* [ ] Checkbox1 + +* [ ] Checkbox2 + +* [x] Checkbox selected +``` + + +* [ ] Checkbox1 + +* [ ] Checkbox2 + +* [x] Checkbox selected + +You may use this syntax in GitHub's issue to check or uncheck the checkbox in real time without having to modify the original version of the issue. + +___ + +### Quoting Text + +```git +> This is a block quoted text +``` + +> This is a block quoted text + +### Multi-level blockquotes +``` +> Asia +>> China +>>> Beijing +>>>> Haidian +>>>>> Tsinghua +``` +#### Look like +> Asia +>> China +>>> Beijing +>>>> Haidian +>>>>> Tsinghua + +* These are fenced code blocks + +___ + +### Text highlighting + +```git +`linux` `ubuntu` +``` +Using a pair of backquotes is suitable for making tags for articles +`linux` `ubuntu` + +___ + +### Horizontal Line + +```git +*** +___ +--- +``` + +All three will be rendered as: +___ +p.s. +```git +
works too +``` +___ + +### Break between lines + +```git +
+``` +___ + +### Visible markdown characters + +```git +```git + * __
etc ``` +``` +___ + +### Multi-line text + + aaa, + sss, + ddd! + +Add 1 tab or 4 spaces at the beginning of several lines of text. + +OR + +Use three backticks: + +``` +asd, +sfd, +wer! +``` +This syntax can also be used for code highlighting + +___ + +### Comments in Markdown + +```git + +``` +They will be invisible on readme +___ + +### Emoji +```git +:grinning: or just place the emoji 😀 +``` +:grinning: or just place the emoji 😀 + +To see a list of every image Github supports, check out the [Emoji Cheat Sheet](https://github.com/ikatyang/emoji-cheat-sheet/blob/master/README.md) + +___ + +### Code Block +There are three ways to add code in markdown +1. Inline Code (single backtick) +2. Whitespace + +```git + `this` is an example of inline code. +``` + four spaces work too! + +3. Fenced code blocks +With GFM you can wrap your code with three back quotes to create a code block without the leading spaces. Add annoptional language identifier and your code will get syntax highlighting. + +```Java +public static void main(String[]args){} //Java +``` + +```javascript +document.getElementById("myH1").innerHTML="Welcome to my Homepage"; //javascipt +``` + +___ + +### Syntax Highlighting +If language name is mentioned after the end of first set of backticks, the code snippet will be highlighted according to the language. + + ```js + console.log('javascript') + ``` + + ```python + print('python') + ``` + + ```java + System.out.println('java') + ``` + + ```json + { + "firstName": "A", + "lastName": "B + "age": 18 + } + ``` + +```js +console.log('javascript') +``` + +```python +print('python') +``` + +```java +System.out.println('java') +``` + +```json +{ + "firstName": "A", + "lastName": "B", + "age": 18 +} +``` +___ + +### diff syntax + +In the version control system, the function of diff is indispensable, i.e., the addition and deletion of a file content is displayed. +The diff effect that can be displayed in GFM. Green is for new, while red is for deleted. +#### Syntax +The syntax is similar to code [fenced code blocks](#fenced-code-blocks), except that the diff is written after the three backticks. +And in the content, the beginning of `+ ` indicates the addition, and the beginning of `- ` indicates the deletion. + +```diff ++ Hello world! +- This is useless. +``` + +___ + +### Use YAML: human friendly data serialization language for all programming languages + +```yaml +name: Mariam +located_in: *** +from: *** +education: *** +job: *** +company: *** +``` +___ + +### Anchor +In fact, each title is an anchor, similar to the HTML anchor (`#`), e.g. + +|Syntax|Look like| +|---|---| +|`[Back to top](#readme)`|[Back to top](#readme)| + +Note that all the letters in the title are converted to **lowercase letters**. + +___ + +### Render mathematical expressions in Markdown + +You can now use LaTeX style syntax to render math expressions within Markdown inline (using $ delimiters) or in blocks (using $$ delimiters). + +Writing expressions as blocks +To add math as a multiline block displayed separately from surrounding text, start a new line and delimit the expression with two dollar symbols $$. + +$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$ + +```git +$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$ +``` +## Writing inline expressions +To include a math expression inline with your text, delimit the expression with a dollar symbol $. + +This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$ + +```git +This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$ +``` + +___ + + +### [Markdown posts on GitHub](https://github.blog/changelog/label/markdown/) + +___ + diff --git a/mengyanote-backend/data/mengyanote/MarkDown/代码片段.md b/mengyanote-backend/data/mengyanote/MarkDown/代码片段.md index 451b8cd..018275c 100644 --- a/mengyanote-backend/data/mengyanote/MarkDown/代码片段.md +++ b/mengyanote-backend/data/mengyanote/MarkDown/代码片段.md @@ -136,5 +136,3 @@ print(message) 2. **代码块**使用三个反引号并指定语言:\`\`\`language 3. 在代码块中,可以避免Markdown语法的转义问题 4. 内联代码适合简短的代码片段,代码块适合多行代码 - -这样使用代码格式,可以让技术文档更加清晰易读! \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/MarkDown/史上最全markdown格式大全.md b/mengyanote-backend/data/mengyanote/MarkDown/史上最全markdown格式大全.md new file mode 100644 index 0000000..d57b7be --- /dev/null +++ b/mengyanote-backend/data/mengyanote/MarkDown/史上最全markdown格式大全.md @@ -0,0 +1,369 @@ + +# 认识 Markdown +> Markdown 是一种用来写作的轻量级「标记语言」,它用简洁的语法代替排版,而不像一般我们用的字处理软件 Word 或 Pages 有大量的排版、字体设置。它使我们专心于码字,用「标记」语法,来代替常见的排版格式。例如此文从内容到格式,甚至插图,键盘就可以通通搞定了。目前来看,支持 Markdown 语法的编辑器有很多,包括很多网站(例如简书)也支持了 Markdown 的文字录入。Markdown 从写作到完成,导出格式随心所欲,你可以导出 HTML 格式的文件用来网站发布,也可以十分方便的导出 PDF 格式。 + +# Markdown 的优点 +* 页面内容可读性强,简洁直观。 +* 纯文本,兼容性强,几乎任何文本编辑器都可打开。 +* 格式转换简便,轻松的导出HTML、PDF、.md等多种格式文件。 +* 不需太过于关心页面排版,只需关注内容。 + +# Markdown 官方文档 +[创始人 John Gruber 的 Markdown 语法说明](https://daringfireball.net/projects/markdown/syntax) +[Markdown 中文版语法说明](http://wowubuntu.com/markdown/#list) + +# Markdown 语法的简要介绍 +[换行符](#换行符) +[强调(Strong and Emphasize)](#强调) +[标题(Headers)](#标题) +[URL链接](#URL链接) +[引用图片](#引用图片) +[列表](#列表) +[引用](#引用) +[Inline Code](#InlineCode) +[Block Code](#BlockCode) +[Code Block](#CodeBlock) +[分割线](#分割线) +[表格](#表格) +[锚点](#锚点) +[del标签](#del标签) +[u标签](#u标签) +[mark标签](#mark标签) +[font标签](#font标签) + +# 换行符 +> 在Markdown语法中,换行需要敲两个回车键,即在两段文本之间添加一个空行来达到换行的效果。 + +*敲一个回车键* + +**这是第一个段落** +**这是第二个段落** + +*上面的写法在编辑器中显示是两行,但是在Macdown预览效果发现,他们还是在同一行,只是两个段落中间多出来一个空格。(注意:简书中的效果是两行显示的,在简书中编辑时,敲一个回车键即可实现换行效果!)* + +**这时我们在两行中间多敲一个回车键就能达到换行的效果** + +*敲两个回车键键的效果:* + +**这是第一个段落** + +**这时第二个段落** + +*有同学发现,我本来只是想让同一句话换个行显示而已,你这中间还多出来一空行,不够美观。能不能中间没有留白的空行,但是也能实现换行的效果尼?当然办法总是有的,下面就介绍第二种换行方式。* + +**另一种方式实现换行的方式,两个段落之间没有空行** +> 官方解释:To force a line break, put two spaces and a newline (return) at the end of the line. + +*在同一段落中,在需要换行的字符后敲两个空格,再敲回车键就能达到同一段落换行的效果* + +*例如这样:* + +**这是第一个段落** +**这时第二个段落** + +# 强调 +> 在Markdown语法中,想要着重强调某一部分内容时,可以使用`*`或者是`_`来包裹要着重强调的内容。`**`或`__`表示字体加粗,`*`或`_`表示字体斜体,(个人推荐使用`*`) + +*这里的内容强调分两种语法,一种是字体加粗强调,另一种是为斜体强调* + +1. 加粗(**可以使用快捷键生成加粗符号:Command+b**) + +* 方式1:**这是字体加粗强调** +* 方式2:__这是字体加粗强调__ + +2. 斜体(**可以使用快捷键生成斜体符号:Command+i**) + +* 方式1:*这是斜体强调* +* 方式2:_这是斜体强调_ + +# 标题 +> 在Markdown语法中,添加标题使用`#`,就会有标题加粗的效果出现。标题分为六个等级,即1个#代表一级标题,2个#代表二级标题,直到六级标题。 + +# Header 1 +## Header 2 +### Header 3 +#### Header 4 +##### Header 5 +###### Header 6 + +*这可以使用下面的方式来设置一级、二级标题,不过不推荐使用。* + +Header 1 +======== +Header 2 +--------- + +# URL链接 +> 在写自己的博客中,经常有需求要跳转到某一个网页或者邮件。这时需要使用link链接。link链接方式分为内联(Inline)和外部引用(Reference) + +* Inline + +*将邮箱地址包裹在<>内部,就形成了可以点击的邮箱地址: <1126756952@qq.com>* + +** + +*将网页url包裹在<>内部,就形成了可以点击访问的网页地址: * + +*自定义网页链接的标题`[标题](链接)`: [光强的简书](http://www.jianshu.com/u/023338566ca5)* + +* Reference (外部引用的好处可以在整个文章中重复引用网页地址或者图片地址而不需要多些拷贝同一个url) + +*Make [a link][arbitrary_id] `[a link][arbitrary_id]`* + +*[like this][] `[like this][]`* + +[arbitrary_id]: http://www.jianshu.com/u/023338566ca5 +[like this]: http://www.jianshu.com/u/023338566ca5 + +# 引用图片 +> Markdown中引用图片资源同样也是分为内联(Inline)和外部引用(Reference) + +* Inline + + *语法:* + +`![Alt Image Text](图片地址)` + +*使用方式:* + +![Alt Image Text](http://upload-images.jianshu.io/upload_images/6342050-4dba8fa19b2fef6e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +* Reference + +*语法* + +`![Alt Image Text][image-id]` + +`[image-id]: http://ovyjkveav.bkt.clouddn.com/17-10-19/15885901.jpg` + +*使用方式:* + +![Alt Image Text][image-id] + +[image-id]: http://ovyjkveav.bkt.clouddn.com/17-10-19/15885901.jpg + +# 列表 +> Markdown语法中,列表分为三类:有序列表、无序列表、嵌套列表 + +* ####有序列表 + +1. 列表1 +2. 列表2 +3. 列表3 + +* ####无序列表(无序列表中,`*`、`+`、`-`效果一样,个人推荐使用`*`) + +* 列表1 +* 列表2 ++ 列表4 ++ 列表5 +- 列表7 +- 列表8 + +* ####缩进层次嵌套列表 + + * 这是一个嵌套列表 + 1. 列表1 + 2. 列表2 + 3. 列表3 + +# 引用 +> Markdown语法中,使用`>`表示引用,引用名人名言或者是别人的一段文本等。 + +--- + +> "三天不学习,赶不上刘少奇。" - 毛泽东 + +*当然引用也可以嵌套使用的* + +> 这时一级引用 +>> 这时二级引用 +>>> 这时三级引用 +>>> 还是三级引用 + +>> 二级引用 + +> 一级引用 + +*引用也可以和其他的markdown语法混合使用* + +> ###这是一级引用 +>> *这是二级引用* +>>> * 这是三级引用 +>>> * 还是三级引用 +>>> 1. 还是三级标题 +>>> 1. 还是三级标题 + +>> **二级引用** + +> [一级引用][arbitrary_id] + +# InlineCode +>内嵌文本,当我们想让某一文本内容内嵌在边框内,我们可以使用一对反引号包裹内容:`内容` + +**使用方式:** + +*内容本身不包含反引号* + +* `Inline code` + +*内容本身包含反引号。当文本内容中正好包含有反引号,这时需要使用空格隔开:* + +* ``code has `backticks` `` + +* ```` ``Code with `backticks` `` ```` + +# BlockCode +> 当我们敲4个空格键或者是一个tab键,这样就形成一个代码块区域。不推荐使用 + +**使用方式:** + + print('This is a code block') + print('The block must be preceded by a blank line') + print('Then indent at least 4 spaces or 1 tab') + print('Nesting does nothing. Your code is displayed Literally') + + +# CodeBlock +> 对于程序员来说,很多时候我们写技术博客都需要贴上自己的代码来给读者讲解,这时就用到了代码引用语法:三个反引号```Code```或者三个波浪号~~~Code~~~。 + +* 方式1 + +``` +render() { + const dataArr = this.state.dataSource + return ( + this._renderSeparator()} + /> + ) + } +``` + +* 方式2 + +~~~ +render() { + const dataArr = this.state.dataSource + return ( + this._renderSeparator()} + /> + ) + } +~~~ + +# 分割线 +> 当我们想对文章的某些内容做上下分割,这时我们可以使用分割线语法:`***或---` + +*在MacDown编辑器中,分割线分为粗线和细线。* + +**注意:在简书中,`***`或`---`不显示粗线,只有细线,一个或者两个`-`也不显示细线** + +* 粗线 + +*** + +or + +--- + +* 细线(细线只需一个或者两个`-`) + +- + +or + +-- + +# 表格 +> 表格的使用肯定我们每一位同学都不陌生,因为我们经常使用Excel表格来展示各种数据报表。很庆幸Markdown同样也支持表格制作,而且使用方式很简单。表格由行与列组成,这里只需要注意表格的内容对齐方式即可。 + +*没有设置对齐方式* + +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | Content Cell + +*中间表格对齐* + +| Left Aligned | Center Aligned | Right Aligned | +|:------------- |:---------------:| -------------:| +| col 3 is | some wordy text | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +**这里需要注意:表格的左边和右边的`|`可以省略不需要使用,表格左右两边加上`|`主要基于美观考虑,表格的对齐方式仅仅取决于`:`** + +# 锚点 +>在写文章时,有时候我们有这样的需求,从文章的某一个位置直接跳转到指定的另一个位置,这时使用到a标签的锚点语法。 + +**注意: a标签跳转到指定位置在简书中无效** + +*使用方式:* + +**在待跳转到的位置上添加一行:``** + +**`[*Markdown高级用法:* 跳转到作者的简书链接处](#markdown-anchor)`:通过name就可以找到指定的位置** + +[*Markdown高级用法:* 跳转到作者的简书链接处](#markdown-anchor) + +# del标签 +> 内容中间添加一条横线: `xxx` + +**注意:del标签在简书中无效** + +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | Content Cell + +# u标签 +> 内容底部添加一条横线: `xxx` + +**注意:u标签在简书中无效** + +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | Content Cell + +# mark标签 +> 内容颜色高亮处理: `xxx` + +**注意:mark标签在简书中无效** + +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | Content Cell + +# font标签 +> 设置文本的字体颜色: `xxx` + +**注意:font标签在简书中无效** + +修改文本的字体颜色为红色 + +# 注意事项 + +* 换行符MacDown编辑器和简书编辑器语法效果上略有不同 +* 简书编辑不支持快捷键生成标签 +* 在简书中,`***`或`---`不显示粗线,只有细线,一个或者两个`-`也不显示细线 +* 简书中使用a标签跳转到指定的位置没有效果 +* 简书中使用del、u、mark、font等标签都没有效果 + +#总结 + +**以上内容是作者花了好几个小时总结出来的常用Markdown语法,介绍了Markdown的十几种常用的语法知识,并逐一的进行了示例展示。可以说足以满足读者们日常中各种使用Markdown编写文章的需求。老铁们要是感觉文章对您有帮助,请 点赞 关注 666** + +*最后再啰嗦一句:一定要趁热打铁!* + +**强烈建议读者们认真浏览作者的这篇文章后,自己使用Markdown写一篇文章,体会一下Markdown语法的优雅之处!** \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/MarkDown/树萌芽のGithub主页.md b/mengyanote-backend/data/mengyanote/MarkDown/树萌芽のGithub主页.md new file mode 100644 index 0000000..bbddd0f --- /dev/null +++ b/mengyanote-backend/data/mengyanote/MarkDown/树萌芽のGithub主页.md @@ -0,0 +1,52 @@ + + + + +
+# 🔍 你好,我是 **树萌芽** 🌱 +**一个喜欢捣鼓各种奇奇怪怪的玩意的人** + +
+avatar +
+--- +## ✨ 关于我(About Me) +- **昵称**:树萌芽 🌿 +- **定位**:Full-Stack / Backend / DevOps ⚙️ +- **简介**:努力学习,努力生活!💪 + +--- +## 🛠 技术栈(Tech Stack) +🐍 Golang-Gin · Python-Flask · Java-SpringBoot +☕ MySQL · MongoDB · SQLite · Redis +🐳 Docker · LXC +🐧 Debian · Ubuntu + +--- +## 📦 精选项目(Selected Projects) + +| 项目名称 | 项目简介 | 项目地址 | 开源地址 | 技术栈 | +| ------------ | -------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------- | +| **萌芽主页** | 简单好看的个人主页 | [www.shumengya.top](https://www.shumengya.top) | | Python Flask + React + Docker | +| **萌芽笔记** | Obsidian MarkDown笔记库 | [note.shumengya.top](https://note.shumengya.top) | | Python Flask + React + Docker | +| **萌芽作品集** | 个人作品合集 | [work.shumengya.top](https://work.shumengya.top) | | Golang Gin + React + Docker | +| **万象口袋** | 一站式跨平台工具合集 | [infogenie.shumengya.top](https://infogenie.shumengya.top) | [github.com/shumengya/InfoGenie](https://github.com/shumengya/InfoGenie) | Golang Gin + React + Docker | +| **萌芽短链** | 短链接分享工具 | [short.shumengya.top](https://short.shumengya.top) | | Python Flask + React + Docker | +| **萌芽漂流瓶** | 个人小玩具 | [bottle.shumengya.top](https://bottle.shumengya.top) | | Python Flask + React + Docker | +| **萌芽SSH** | 网页SSH连接工具 | [ssh.shumengya.top](https://ssh.shumengya.top) | | Golang Gin + React + Docker | +| **萌芽小店** | 轻量级商城系统 | [store.shumengya.top](https://store.shumengya.top) | [github.com/shumengya/mengyastore](https://github.com/shumengya/mengyastore) | Golang Gin + React + Docker | +| **萌芽导航** | 浏览器网站导航 | [nav.smyhub.com](https://nav.smyhub.com) | | CloudFlare Worker + D1 SQL | +| **萌芽账户认证中心** | 自制软件作品统一账户认证中心 | [auth.shumengya.top](https://auth.shumengya.top) | | Golang Gin + React + Docker | +| **萌芽Ping** | 网站监控面板 | [ping.shumengya.top](https://ping.shumengya.top) | | Golang Gin + React + Docker | +| **萌芽VPS监控** | 服务器监控面板 | [monitor.shumengya.top](https://monitor.shumengya.top) | | Golang Gin + React + Docker | +| **萌芽博客** | 极客轻量级极简博客 | [blog.smyhub.com](https://blog.smyhub.com) | | CloudFlare Worker + D1 SQL | + +--- +## 📫 联系方式(Contact) +- 📧 **联系邮箱:**[mail@smyhub.com](mailto:mail@smyhub.com) +- 🌐 **个人主页:**[smyhub.com](https://smyhub.com) +- 🐙 **GitHub主页:**[github.com/shumengya](https://github.com/shumengya) +- 📝 **个人博客:**[blog.smyhub.com](https://blog.smyhub.com) +- 💬 **闲聊说说:**[memos.shumengya.top](https://memos.shumengya.top) + +--- \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/MarkDown/等级表示方法.md b/mengyanote-backend/data/mengyanote/MarkDown/等级表示方法.md new file mode 100644 index 0000000..f324d1c --- /dev/null +++ b/mengyanote-backend/data/mengyanote/MarkDown/等级表示方法.md @@ -0,0 +1,8 @@ +阿拉伯数字: +1,2,3,4,5,6,7,8,9,10 +简体中文: +一、二、三、四、五、六、七、八、九、十 +繁体中文: +壹、貳、參、肆、伍、陸、柒、捌、玖、拾 +罗马数字: +I,II,III,IV,V,VI,VII,VIII,IX,X diff --git a/mengyanote-backend/data/mengyanote/MarkDown/表情包表格.md b/mengyanote-backend/data/mengyanote/MarkDown/表情包表格.md new file mode 100644 index 0000000..3793562 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/MarkDown/表情包表格.md @@ -0,0 +1,6 @@ +| | | | | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| ![滑稽/4.webp](https://main.meme-api.pages.dev/meme/%E6%BB%91%E7%A8%BD/4.webp) | ![滑稽/18.webp](https://main.meme-api.pages.dev/meme/%E6%BB%91%E7%A8%BD/18.webp) | ![滑稽/38.webp](https://main.meme-api.pages.dev/meme/%E6%BB%91%E7%A8%BD/38.webp) | ![滑稽/25.webp](https://main.meme-api.pages.dev/meme/%E6%BB%91%E7%A8%BD/25.webp) | +| ![滑稽/37.webp](https://main.meme-api.pages.dev/meme/%E6%BB%91%E7%A8%BD/37.webp) | | | | +| ![滑稽/50.webp](https://main.meme-api.pages.dev/meme/%E6%BB%91%E7%A8%BD/50.webp) | | | | +| ![滑稽/74.webp](https://main.meme-api.pages.dev/meme/%E6%BB%91%E7%A8%BD/74.webp) | | | ![滑稽/84.gif](https://main.meme-api.pages.dev/meme/%E6%BB%91%E7%A8%BD/84.gif) | diff --git a/mengyanote-backend/data/mengyanote/Minecraft/MD测试.md b/mengyanote-backend/data/mengyanote/Minecraft/MD测试.md new file mode 100644 index 0000000..55ab7aa --- /dev/null +++ b/mengyanote-backend/data/mengyanote/Minecraft/MD测试.md @@ -0,0 +1,167 @@ +# 一级标题 +## 二级标题 +### 三级标题 +#### 四级标题 +##### 五级标题 +###### 六级标题 + +**这是加粗文本** +*这是斜体文本* +~~这是删除线~~ +==这是高亮文本== +这是下划线 + +- 无序列表项 1 + - 嵌套列表项 A + - 嵌套列表项 B +- 无序列表项 2 + +1. 有序列表项 1 + 1. 嵌套有序项 A +2. 有序列表项 2 + +- [ ] 待办事项 1 (未完成) +- [x] 待办事项 2 (已完成) +- [ ] 待办事项 3 (带嵌套) + - [ ] 子任务 A + +[链接](https://github.com) +`行内代码` +键盘键样式: Ctrl + Shift + P +上标: X^2^,下标: H~2~O +^[这是一个脚注] + +```javascript +console.log("Hello LinuxDo!"); +``` + +**Mermaid 图表:** + +```mermaid + +graph TD + +Start --> Stop + +``` + +**数学公式 (LaTeX):** +行内公式: $E = mc^2$ +块级公式: +$$ + +\frac{-b \pm \sqrt{b^2 - 4ac}}{2a} + +$$ + +> 这是一段引用文本。 + + +|第 1 列 | 居中对齐 | 右对齐 | 左对齐| +|---|:---:|---:|:---| +|单元格 1 | 文本内容 | ○ | ¥100| +|单元格 2 | 文本 | ● | ¥2,000| + +**图片示例:** + +![基础图片](https://cdn3.linux.do/original/4X/1/1/f/11fe4ea8a7956bb548368de598f59acfa7425d28.jpeg) + +![带尺寸的图片|200x100](https://cdn3.linux.do/original/4X/b/d/7/bd7cfb0628313eadda9d95527d08332cda2bfab5.jpeg) + +[![带链接的图片](https://cdn3.linux.do/original/4X/5/a/3/5a321235e384648063b4ab2d383f7ce802cfd61f.jpeg)](https://linux.do) + +![带标题的图片](https://cdn3.linux.do/original/4X/d/d/b/ddb7556029063f0182e88bfbb6d436eff4b24b35.jpeg "鼠标悬停显示的标题") + + + +[/details] +**插入时间测试** +[date=2026-01-12 timezone=Asia/Shanghai] + +**模糊测试** +[spoiler]此文本将被模糊处理[/spoiler] + +**图表测试** +[chart type='horizontalBar' title='图表标题' xAxisTitle='横坐标标签'] +标签1 | 50 +标签2 | 60 +标签3 | 70 +[/chart] + +**投票测试** +[poll type=regular results=always public=true chartType=bar] +* 选项2 +* 选项1 +[/poll] +
+滚动内容测试 + +这里是汉代的豫章郡城,如今是洪州的都督府,天上的方位属于翼,轸两星宿的分野,地上的位置连结着衡山和庐山。以三江为衣襟,以五湖为衣带、控制着楚地,连接着闽越。物类的精华,是上天的珍宝,宝剑的光芒直冲上牛、斗二星的区间。人中有英杰,因大地有灵气,陈蕃专为徐孺设下几榻。雄伟的洪州城,房屋像雾一般罗列,英俊的人才,像繁星一样地活跃。城池座落在夷夏交界的要害之地,主人与宾客,集中了东南地区的英俊之才。都督阎公,享有崇高的名望,远道来到洪州坐镇,宇文州牧,是美德的楷模,赴任途中在此暂留。正逢十日休假的日子,杰出的友人云集,高贵的宾客;也都不远千里来到这里聚会。高贵的宾客,文章的气势像腾起的蛟龙,飞舞的彩凤,文坛领袖孟学士,藏有像紫电、青霜这样锋利的宝剑。王将军的武库里,由于父亲在交趾做县令,我在探亲途中经过这个著名的地方。我年幼无知,竟有幸亲身参加了这次盛大的宴会。时当九月,秋高气爽。积水消尽,潭水清澈,天空凝结着淡淡的云烟,暮霭中山峦呈现一片紫色。在高高的山路上驾着马车,在崇山峻岭中访求风景。来到昔日帝子的长洲,发现了滕王所修的滕王阁。这里山峦重叠,青翠的山峰耸入云霄。凌空的楼阁,红色的阁道犹如飞翔在天空,从阁上看不到地面。白鹤,野鸭停息的小洲,极尽岛屿的纡曲回环之势,雅浩的宫殿,跟起伏的山峦配合有致。推开雕花的阁门,俯视彩饰的屋脊,山峰平原尽收眼底,湖川曲折令人惊讶。遍地是里巷宅舍,许多钟鸣鼎食的富贵人家。舸舰塞满了渡口,尽是雕上了青雀黄龙花纹的大船。正值雨过天晴,虹消云散,阳光朗煦。落霞与孤雁一起飞翔,秋天的江水和辽阔的天空连成一片,浑然一色。傍晚渔舟中传出的歌声,响彻彭蠡湖滨,雁群感到寒意而发出的惊叫,鸣声到衡阳之浦为止。放眼远望,胸襟刚感到舒畅,超逸的兴致立即兴起。排箫的音响引来的徐徐清风,柔缓的歌声吸引住飘动的白云。像睢园竹林的聚会,这里善饮的人,酒量超过彭泽县令陶渊明,像邺水赞咏莲花,这里诗人的文采,胜过临川内史谢灵运。(音乐与饮食,文章和言语)这四种美好的事物都已经齐备,(贤主、嘉宾)这两个难得的条件也凑合在一起了。向天空中极目远眺,在假日里尽情欢娱。苍天高远,大地寥廓,令人感到宇宙的无穷无尽。欢乐逝去,悲哀袭来,我知道了事物的兴衰成败是有定数的。西望长安,东指吴会。南方的陆地已到尽头,大海深不可测,北方的北斗星多么遥远,天柱高不可攀。关山重重难以越过,有谁同情不得志的人?萍水偶尔相逢,大家都是异乡之客。怀念着君王的宫门,但却不被召见,什么时候才能够去侍奉君王呢?呵!各人的时机不同,人生的命运多有不顺。冯唐容易衰老,李广难得封侯。使贾谊遭受委屈,贬于长沙,并不是没有圣明的君主,使梁鸿逃匿到齐鲁海滨,难道不是政治昌明的时代?只不过由于君子能了解时机,通达的人知道自己的命运罢了。年纪虽然老了,但志气应当更加旺盛,怎能在白头时改变心情?境遇虽然困苦,但节操应当更加坚定,决不能抛弃自己的凌云壮志。即使喝了贪泉的水,仍觉着神清气爽,即使身处于干涸的主辙中,也是欢乐无比。北海虽然十分遥远,雅浩的宫殿可以联通;早晨虽然已经过去,而珍惜黄昏却为时不晚。孟尝君心地高洁,但白白地怀抱着报国的热情;阮籍为人放纵不羁,我们怎能学他那种穷途的哭泣!我王勃,地位卑微,只是一个书生。却无处去请缨杀敌。虽然和终军一样年已二十一,也有投笔从戎的志向。我羡慕宗悫那种“乘长风破万里浪”的英雄气概,如今我抛弃了一生的功名,到万里之外朝夕侍奉父亲。虽然称不上谢家的“宝树”,但是能和贤德之士相交往。不久我将见到父亲,聆听他的教诲;今天我侥幸地奉陪各位长者,高兴地登上龙门。假如碰不上杨得意那样引荐的人,就只有抚拍着自己的文章而自我叹惜。既然已经遇到了钟子期,就弹奏一曲《流水》又有什么羞愧呢?呵!名胜之地不能常存,盛大的宴会难以再逢,兰亭宴集已为陈迹,石崇的梓泽也变成了废墟。让我临别时作了这一篇序文,承蒙这个宴会的恩赐,至于登高作赋,这只有指望在座诸公了。我只是冒昧地尽我微薄的心意,作了短短的引言。在座诸位都按各自分到的韵字赋诗,我已写成了四韵八句。在座诸位施展潘岳,施展陆机一样的才笔,各自谱写瑰丽的诗篇吧。巍峨高耸的滕王阁俯临着江心的沙洲,想当初佩玉、鸾铃鸣响的豪华歌舞已经停止了。早晨,画栋飞上了南浦的云,黄昏,珠帘卷入了西山的雨。悠闲的彩云影子倒映在江水中,整天悠悠然地漂浮着,时光易逝,人事变迁,不知已经度过几个春秋。昔日游赏于高阁中的滕王如今已不知哪里去了?只有那栏杆外的滔滔江水空自向远方奔流。 +
+ +**自动目录测试** +
+ +**OneBox测试(视频)** +https://www.w3schools.com/html/mov_bbb.mp4 + +> [!note] +> Lorem ipsum dolor sit amet + +> [!abstract] +> Lorem ipsum dolor sit amet +> [!info] +> Lorem ipsum dolor sit amet + + +> [!todo] +> Lorem ipsum dolor sit amet + +> [!tip] +> Lorem ipsum dolor sit amet + +> [!success] +> Lorem ipsum dolor sit amet + + +> [!question] +> Lorem ipsum dolor sit amet + +> [!warning] +> Lorem ipsum dolor sit amet + +> [!failure] +> Lorem ipsum dolor sit amet + + +> [!danger] +> Lorem ipsum dolor sit amet + +> [!bug] +> Lorem ipsum dolor sit amet + + +> [!example] +> Lorem ipsum dolor sit amet + +> [!quote] +> Lorem ipsum dolor sit amet + +> [!warning]+ 别看了 +> 快折起来。 + +> [!success]- 非常成功 +> 确实如此。 + +**wrap测试** +[wrap=center] + +这段文字可能会居中显示 + +[/wrap] + +[wrap=text-red] + +这段文字可能会变成红色 + +[/wrap] \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/README.md b/mengyanote-backend/data/mengyanote/README.md index 3965eda..979f423 100644 --- a/mengyanote-backend/data/mengyanote/README.md +++ b/mengyanote-backend/data/mengyanote/README.md @@ -1,6 +1,42 @@ -## /shumengya/mengyanote +# 萌芽笔记 -- 所在主机:bigmengya -- 用途:知识库目录 -- 说明:用于存放树萌芽的知识库、笔记资料及相关内容 -- 建议:与 Docker 运行目录分开维护,避免运行数据与知识资料混杂 +树萌芽的 Markdown 知识库与资料仓库,用于集中整理技术笔记、项目策划、运维记录、学习资料和个人沉淀内容。 + +## 仓库定位 + +这个仓库不是传统的软件项目,而是一个按主题分类维护的文档库。内容以 `.md` 文件为主,适合在 Obsidian、VS Code 或任意 Markdown 编辑器中查看和维护。 + +## 主要目录 + +- `AI/`:大模型、提示词工程、Agent、AI 工具使用记录 +- `Linux/`、`Windows/`、`Docker/`:系统运维与环境配置笔记 +- `数据库/`:MySQL、Postgres、MongoDB、Redis、SQLite 等相关记录 +- `编程语言/`:Golang、Java、Python、前端、C/C++ 等语言学习与代码片段 +- `内网穿透/`、`CloudFlare/`、`运维/`:网络、代理、部署、域名与服务治理 +- `项目策划/`:各类产品、工具和项目想法草案 +- `博客文章/`:可进一步整理发布的文章内容 +- `树萌芽の小本本/`、`临时/`:个人记录与阶段性临时资料 + +## 使用方式 + +1. 直接用 Obsidian 打开仓库目录,可获得更好的目录浏览和双链体验。 +2. 使用 VS Code 或其他编辑器维护 Markdown 也完全可行。 +3. 提交前建议先检查改动: + +```bash +git status --short +git diff -- README.md +rg --files +``` + +## 维护建议 + +- 一篇文档尽量只讲一个主题,方便后续检索和迁移。 +- 文件名尽量直白,优先沿用现有中文分类和命名风格。 +- 新文档优先放入已有分类目录,避免根目录堆积。 +- `.obsidian/` 仅在确实需要共享工作区配置时再提交。 + +## 说明 + +- 当前知识库主要部署和使用位置为 `bigmengya` 的 `/shumengya/mengyanote` +- 建议与 Docker 运行目录分开维护,避免业务数据和知识资料混放 diff --git a/mengyanote-backend/data/mengyanote/Windows/刷新浏览器和系统DNS缓存.md b/mengyanote-backend/data/mengyanote/Windows/刷新浏览器和系统DNS缓存.md new file mode 100644 index 0000000..e6c096c --- /dev/null +++ b/mengyanote-backend/data/mengyanote/Windows/刷新浏览器和系统DNS缓存.md @@ -0,0 +1,18 @@ + 1. 强制刷新:Ctrl+Shift+R(或 Cmd+Shift+R)。 + 2. 清理站点缓存与 Service Worker:DevTools → Application → Service Workers → Unregister,然后 Clear storage。 + +在 Windows 系统中,刷新系统 DNS 缓存的命令是 `ipconfig /flushdns`。 + +### 操作步骤 +1. **以管理员身份打开命令行**: + * 点击“开始”菜单,输入 `cmd` 或 `命令提示符`。 + * 在搜索结果中,右键点击“命令提示符”图标,选择 **“以管理员身份运行”**。 +2. **执行刷新命令**: + * 在弹出的黑色窗口中,输入以下命令并按回车键: + ```cmd + ipconfig /flushdns + ``` + +### 附加说明 +* **替代命令**:如果你使用的是 PowerShell,也可以输入 `Clear-DnsClientCache` 命令来达到同样的效果。 +* **验证结果**:如果想确认缓存是否已被清空,可以运行 `ipconfig /displaydns` 命令。如果返回结果为空,则表示刷新成功。 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/Windows/刷新浏览器缓存.md b/mengyanote-backend/data/mengyanote/Windows/刷新浏览器缓存.md deleted file mode 100644 index bbc3857..0000000 --- a/mengyanote-backend/data/mengyanote/Windows/刷新浏览器缓存.md +++ /dev/null @@ -1,3 +0,0 @@ - 1. 强制刷新:Ctrl+Shift+R(或 Cmd+Shift+R)。 - 2. 清理站点缓存与 Service Worker:DevTools → Application → Service Workers → Unregister,然后 Clear storage。 - 3. 如果有发布流程,建议更新 public/manifest.webmanifest 或 service worker 的版本号/内容,触发刷新。 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/临时/cursor半价.md b/mengyanote-backend/data/mengyanote/临时/cursor半价.md new file mode 100644 index 0000000..f743ec9 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/临时/cursor半价.md @@ -0,0 +1,10 @@ +## 所谓的"半价"来源 + +|渠道|折扣|真实性| +|---|---|---| +|**推荐码首月半价**|✅ 首月 50% off|真实,Reddit 有分享,如 `cursor.com/referral?code=EPWK153JMKSR`| +|**学生免费**|✅ **Pro 免费一年**|真实,教育邮箱验证即可| +|**Startup 计划**|✅ **免费 Pro**|真实,符合条件的初创团队| +|**年付**|✅ ~**17% off**(≈$16/月)|真实| +|**拼车/合租**|⚠️ 半价甚至更低|灰色地带,有封号风险| +|**第三方促销码网站**|❌ 大多无效|Cursor 官方不发公开折扣码| \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/临时/临时.md b/mengyanote-backend/data/mengyanote/临时/临时.md deleted file mode 100644 index b8eeaa9..0000000 --- a/mengyanote-backend/data/mengyanote/临时/临时.md +++ /dev/null @@ -1,9 +0,0 @@ -杨耀翔/李昶佑/夏春鹏/王彤 - -电气信息学院/计算机与软件学院/计算机与软件学院/计算机与软件学院 - -202431076214/202431062440/202431061719/202431062229 - -| | -| -------------------------------------------------- | -| 1、周幂   西南石油大学  教师  1203212378@qq.com   17781100230 | \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/临时/浏览器更换默认字体.md b/mengyanote-backend/data/mengyanote/临时/浏览器更换默认字体.md new file mode 100644 index 0000000..6c58c47 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/临时/浏览器更换默认字体.md @@ -0,0 +1,58 @@ + +### 1. 先安装思源黑体字体 +- 下载地址:Adobe 官网或 GitHub(Source Han Sans SC)。 +- **Windows**:双击字体文件 → 安装(或右键“安装为所有用户”)。 +- **macOS**:双击打开字体册安装。 +- **Linux**:复制到 `~/.local/share/fonts/` 并运行 `fc-cache -fv`。 +安装后重启浏览器即可在字体列表中看到。 + +--- + +### 2. Chrome / Edge / 新版 Chromium 浏览器 +1. 打开浏览器,输入地址栏: + **`chrome://settings/fonts`**(Edge 用 `edge://settings/fonts`) +2. 点击 **“自定义字体”**(Customize fonts)。 +3. 修改以下选项(推荐): + - **标准字体(Standard font)**:选择 **思源黑体**(或 Noto Sans SC) + - **无衬线字体(Sans-serif)**:选择 **思源黑体**(最重要,中文字体主要走这个) + - **衬线字体(Serif)**:可保留默认或也改成思源宋体 + - **等宽字体(Fixed-width)**:可选改成等宽版本或保持默认 +4. 调整字体大小(推荐 16px 或以上)。 +**注意**: +- 这个设置只影响**网页没有明确指定字体**的情况。如果网站强制用了其他字体(如微软雅黑),效果有限。 +- 想**强制所有网页**使用思源黑体,可安装扩展如“网页字体替换”或 Stylus + 自定义 CSS。 + +--- + +### 3. Firefox 火狐浏览器 +1. 打开菜单 → **设置**(或输入 `about:preferences`)。 +2. 左侧选择 **常规** → 向下找到 **语言与外观** → **字体** → 点击 **高级**。 +3. 设置: + - **默认字体**:选择 **思源黑体** + - **无衬线**:思源黑体 + - **衬线**:思源宋体(可选) + - **等宽**:可选 +4. **重要**:取消勾选 **“允许页面选择自己的字体代替您的上述选择”**,这样可以强制覆盖大部分网站字体。 +**额外高级方法**(更彻底): +- 输入 `about:config` → 搜索 `font.name.sans-serif.zh-CN`,双击修改为 `思源黑体`(或完整字体名称)。 +- 可分别设置简体(zh-CN)、繁体(zh-TW)等。 + +--- + +### 4. 如果想全局强制 +很多网站会通过 CSS 指定字体,浏览器内置设置可能不够强。推荐方案: +- **安装 Stylus 扩展**(Chrome/Firefox 都有) → 新建样式,写入类似以下 CSS: + ```css + * { + font-family: "思源黑体", "Noto Sans SC", sans-serif !important; + } + ``` +- 或使用现成扩展:“网页字体替换”等。 + +--- + +### 常见问题 +- **字体没生效**:重启浏览器,或检查系统是否正确安装了思源黑体(字体设置里能搜索到)。 +- **中英混排不协调**:思源黑体对中英文搭配较好,也可搭配系统英文字体(如 Segoe UI / San Francisco)。 +- **Chrome 较新版本**:部分 Windows 系统下会自动倾向使用 Noto/思源系列,安装字体后效果更好。 +- **移动端**:Android Chrome 可通过旗标(chrome://flags)或扩展有限调整;iOS 较难强制修改。 diff --git a/mengyanote-backend/data/mengyanote/临时/灵创盘底部说明.md b/mengyanote-backend/data/mengyanote/临时/灵创盘底部说明.md deleted file mode 100644 index 12bdb8e..0000000 --- a/mengyanote-backend/data/mengyanote/临时/灵创盘底部说明.md +++ /dev/null @@ -1,17 +0,0 @@ -### 灵创实验室相关网站 - -#### 招新官网: [https://www.lcxm.site](https://www.lcxm.site) -#### 灵创作品集: [https://show.lcxm.site](https://show.lcxm.site) -#### 灵创云盘: [https://pan.lcxm.site](https://pan.lcxm.site) - ---- - -### 📞 网盘问题联系 - -| 联系人 | QQ号码 | -| ---- | ---------- | -| 北冥有鱼 | 1151175089 | -| 语棄 | 3961448694 | -| 树萌芽 | 3205788256 | - -> 如有网盘相关问题和需求,请联系以上负责人 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/临时/灵创盘顶部说明.md b/mengyanote-backend/data/mengyanote/临时/灵创盘顶部说明.md deleted file mode 100644 index 61a4fbf..0000000 --- a/mengyanote-backend/data/mengyanote/临时/灵创盘顶部说明.md +++ /dev/null @@ -1,3 +0,0 @@ -# 🎯 灵创新媒实验室网盘 - ->⚠️ 上传文件前请遵守相应上传规则,避免文件乱放 diff --git a/mengyanote-backend/data/mengyanote/内网穿透/NAT打洞原理文档.md b/mengyanote-backend/data/mengyanote/内网穿透/NAT打洞原理文档.md new file mode 100644 index 0000000..47ebb58 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/内网穿透/NAT打洞原理文档.md @@ -0,0 +1,106 @@ + +### 1. NAT 基本概念与类型 +**NAT(Network Address Translation,网络地址转换)** 是路由器/防火墙将内网(私有IP)数据包的源地址和端口转换为公网(公有IP)地址和端口的技术。主要用于地址复用和安全隔离。 + +根据 **RFC 3489**(STUN 协议)和 **RFC 4787/5780** 的行为分类(映射行为 + 过滤行为),NAT 分为四种主要类型(中文社区常用 NAT1~NAT4 命名): + +- **NAT1:Full Cone NAT(全锥形 NAT / 完全圆锥型)** + 最宽松类型。 + - **映射行为**:Endpoint-Independent Mapping(端点无关映射)。同一个内网 (IP:Port) 映射到固定的公网 (IP:Port),与目标地址无关。 + - **过滤行为**:Endpoint-Independent Filtering(端点无关过滤)。一旦映射建立,**任何外部主机** 都可以向该公网 (IP:Port) 发送数据包。 + 相当于“门卫不在岗”,非常开放。 + +- **NAT2:Address-Restricted Cone NAT(地址限制锥形 NAT / 受限圆锥型)** + - **映射行为**:端点无关映射(公网端口固定)。 + - **过滤行为**:Address-Dependent Filtering(仅地址相关过滤)。只允许**之前发送过数据的外部 IP** 的数据包进入(端口不限制)。 + 相当于“只认之前联系过的 IP”。 + +- **NAT3:Port-Restricted Cone NAT(端口限制锥形 NAT / 端口受限圆锥型)** + - **映射行为**:端点无关映射(公网端口固定)。 + - **过滤行为**:Address and Port-Dependent Filtering(地址+端口相关过滤)。只允许**之前发送过数据的外部 (IP:Port)** 的数据包进入。 + 家庭宽带最常见类型,限制较严格。 + +- **NAT4:Symmetric NAT(对称型 NAT)** + 最严格类型(也称 Hard NAT)。 + - **映射行为**:Endpoint-Dependent Mapping(端点相关映射)。同一个内网 (IP:Port) 向**不同目标 (IP:Port)** 发送时,会映射到**不同公网端口**。 + - **过滤行为**:通常为地址+端口相关过滤。 + P2P 直连最困难,常需端口预测或中继。 + +**开放性排序**(从易到难):NAT1 > NAT2 > NAT3 > NAT4 +**安全性排序**(从低到高):NAT1 < NAT2 < NAT3 < NAT4 + +**打洞可行性总结表**(UDP 打洞,基于常见实践): + +| | NAT1 | NAT2 | NAT3 | NAT4 | +|----------|------|------|------|------| +| **NAT1** | ✅ | ✅ | ✅ | ✅ | +| **NAT2** | ✅ | ✅ | ✅ | ✅ | +| **NAT3** | ✅ | ✅ | ✅ | ❌(难) | +| **NAT4** | ✅ | ✅ | ❌ | ❌(最难) | + +(注:NAT4 组合成功率低,常需端口预测 + 多次尝试或 TURN 中继。A+B ≤ 6 的经验法则有时被提及,但实际以具体行为为准。) + +### 2. 打洞(Hole Punching / NAT Traversal)基本原理 +打洞的核心是 **利用 NAT 的“状态保持”特性**:内网主机主动向外发送 UDP 数据包时,NAT 会创建临时映射表项(“打一个洞”),允许对应外部流量返回。 + +**经典 UDP Hole Punching 流程**(假设双方 A 和 B 都在 NAT 后,使用公网 STUN/协调服务器 S): + +1. **发现公网映射**:A 和 B 分别向 STUN 服务器 S 发送 UDP 包,S 返回各自的公网 (IP:Port)(称为 Candidate)。 +2. **交换候选地址**:通过 S(信令通道)互相告知对方的公网 (IP:Port)。 +3. **同时打洞**:A 和 B 在极短时间内**同时向对方的公网 (IP:Port) 发送 UDP 包**。 + - 第一个包可能被对方 NAT 丢弃(因为还没建立对应状态)。 + - 但发送动作会在自己 NAT 上创建映射,允许对方后续包进入。 +4. **连接建立**:双方状态表更新后,UDP 数据可双向直连。 + +**关键点**: +- 必须使用 **UDP**(无连接,状态易维护;TCP 打洞更复杂,需 Simultaneous Open)。 +- 需要第三方协调服务器(STUN 获取映射,ICE 框架优化候选)。 +- 如果一方是 NAT4,映射端口随目标变化,需预测端口增量或多次尝试。 + +**辅助技术**: +- **STUN**:发现公网地址和 NAT 类型。 +- **TURN**:当打洞失败时,中继转发(Relay)。 +- **ICE**(Interactive Connectivity Establishment):WebRTC 等使用的框架,综合多种候选(主机、服务器反射、TURN)并优先直连。 + +### 3. 不同 NAT 类型组合下的打洞原理详解 + +#### 3.1 NAT1 ↔ NAT1(最简单) +- 双方公网端口固定,且过滤宽松。 +- 交换公网地址后,任意一方发送即可建立,无需严格同时性。 +- 成功率接近 100%。 + +#### 3.2 NAT1 ↔ NAT3(或 NAT2) +- NAT1 过滤宽松(几乎任何包都可进)。 +- NAT3 需先“见过”对方:让 NAT3 侧先向 NAT1 发送包,更新 NAT3 状态表。 +- 然后 NAT1 向 NAT3 发送,轻松通过。 +- 成功率很高。 + +#### 3.3 NAT3 ↔ NAT3(常见家庭宽带场景) +- 双方公网端口固定,但过滤严格(需 IP+Port 匹配之前请求)。 +- **必须同时发送**:双方在毫秒级窗口内互相发送 UDP 包,各自在对方 NAT 上“打洞”。 +- 第一个包可能丢弃,但后续包因状态已建立而通过。 +- 实际中通过 ICE/STUN 多次尝试 + 定时同步实现,高成功率(90%以上)。 + +#### 3.4 涉及 NAT4 的场景(较困难) +- **NAT1/NAT2/NAT3 ↔ NAT4**: + - NAT4 向对方发送时,因目标不同可能换新端口。 + - 解决方案:NAT4 先向协调服务器发送获取“基础端口”,预测增量;对方尝试多个预测端口;或让非 NAT4 侧主动。 + - 常结合端口猜测(Port Prediction)。 +- **NAT4 ↔ NAT4**: + - 最难。双方端口都随目标变化。 + - 需高级端口预测算法、多次并行尝试,或直接 fallback 到 TURN 中继。 + - 成功率低(依赖运营商 NAT 实现,有些可达 70-90%,有些几乎为 0)。 + +### 4. 实际应用与注意事项 +- **游戏联机、P2P 下载、VoIP/WebRTC**:依赖 NAT 打洞实现低延迟直连。 +- **失败处理**:打洞超时 → 切换 TURN 中继(牺牲一些延迟,但保证连通)。 +- **提升 NAT 类型方法**(可选,非打洞替代): + - 路由器开启 UPnP 或手动端口映射。 + - 部分运营商支持 DMZ 或桥接模式。 + - 使用支持 NAT 优化的 VPN(如某些 P2P VPN)。 +- **当前现状(2026 年)**:家庭宽带多为 NAT3;运营商 CGNAT(大 NAT4)增多,打洞难度上升;ICE + TURN 仍是主流保底方案。 + +### 5. 参考与扩展 +- RFC 3489(STUN)、RFC 4787(NAT 行为)、RFC 5780(NAT 类型检测)。 +- 常见实现:WebRTC(ICE)、libp2p、各种 P2P VPN。 +- 实战建议:使用 STUN 服务器测试自身 NAT 类型(如 stun.l.google.com:19302),然后观察打洞日志。 diff --git a/mengyanote-backend/data/mengyanote/前后端项目env环境配置解析.md b/mengyanote-backend/data/mengyanote/前后端项目env环境配置解析.md new file mode 100644 index 0000000..aa9cba8 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/前后端项目env环境配置解析.md @@ -0,0 +1,47 @@ + +## 一、文件类型与用途 + +| 文件 | 用途 | 提交Git | +|------|------|----------| +| `.env.example` | 配置模板,含占位符 | ✅ 是 | +| `.env` | 基础默认配置 | ❌ 否 | +| `.env.development` | 开发环境配置 | ✅ 是 | +| `.env.production` | 生产环境配置 | ✅ 是 | +| `.env.test` | 测试环境配置 | ✅ 是 | +| `.env.local` | 本地个人覆盖 | ❌ 否 | +| `.env.development.local` | 开发环境本地覆盖 | ❌ 否 | +| `.env.branch.` | 特定分支配置(可选) | ❌ 否 | + +## 二、加载优先级(从低到高) +系统环境变量 > .env.local > .env.[mode].local > .env.[mode] > .env +> 同名变量:高优先级覆盖低优先级 +## 三、各框架差异速查 + +| 框架 | 客户端变量前缀 | 特殊文件 | +|------|---------------|----------| +| Next.js | `NEXT_PUBLIC_` | `.env.production.local` | +| Vite | `VITE_` | 无 | +| Laravel | 无(仅服务端) | `.env.testing` | +| Spring Boot | 需第三方库 | 配合 `application.yml` | + +## 四、核心规则 +1. **敏感信息不进Git** — 密钥、密码、Token 只放 `.gitignore` 涵盖的文件 +2. **变量值默认为字符串** — 布尔/数字按字符串解析,框架可能有特殊处理 +3. **客户端暴露需前缀** — 浏览器可访问的变量必须加特定前缀(如 `VITE_`、`NEXT_PUBLIC_`) + +## 五、示例文件结构 +project/ +├── .env.example # DB_HOST=localhost +├── .env # 实际配置(git ignored) +├── .env.development # API_URL=http://dev.api +├── .env.production # API_URL=https://api.com +├── .env.local # 个人覆盖(git ignored) +└── .gitignore # 包含 .env, *.local +## 六、常见反模式 + +| ❌ 错误做法 | ✅ 正确做法 | +|------------|------------| +| 将 `.env` 提交到 Git | 只提交 `.env.example` | +| 硬编码变量在代码中 | 通过环境变量读取 | +| 所有环境共用一个文件 | 按环境拆分文件 | +| 不设默认值 | `.env.example` 提供占位符 | diff --git a/mengyanote-backend/data/mengyanote/博客文章/Markdown 渲染格式大全.md b/mengyanote-backend/data/mengyanote/博客文章/Markdown 渲染格式大全.md new file mode 100644 index 0000000..80006a3 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/博客文章/Markdown 渲染格式大全.md @@ -0,0 +1,205 @@ + +# Markdown 渲染格式大全 + +## 1. 基础文本格式 + +*斜体* 或 _斜体_ +**粗体** 或 __粗体__ +***粗斜体*** 或 ___粗斜体___ +~~删除线~~ +下划线(使用 HTML) +==高亮标记== (部分渲染器支持) +H~2~O (下标) +X^2^ (上标) + +## 2. 标题(1-6级) + +# H1 +## H2 +### H3 +#### H4 +##### H5 +###### H6 + +## 3. 列表 + +### 无序列表 +- 项目 1 + - 嵌套项目 1.1 + * 第三层(可用 * + -) ++ 项目 2 +* 项目 3 + +### 有序列表 +1. 第一项 +2. 第二项 + 3. 嵌套 2.1 + 4. 嵌套 2.2 +5. 第三项 + +### 任务列表 +- [x] 已完成任务 +- [ ] 未完成任务 +- [ ] 子任务 [优先级高] + +## 4. 链接与图片 + +[普通链接](https://example.com) +[带标题的链接](https://example.com "鼠标悬停显示") + + +![替代文本](https://picsum.photos/200/100 "图片标题") + +## 5. 引用 + +> 这是一级引用 +>> 嵌套引用 +>>> 三级引用 +> +> 回到一级 + +## 6. 代码 + +行内代码:`console.log("Hello");` + +代码块(带语法高亮): +```javascript +function greet(name) { + return `Hello, ${name}!`; +} +``` + +## 7. 表格 + +| 左对齐 | 居中对齐 | 右对齐 | +|:-------|:--------:|-------:| +| 单元格1 | 单元格2 | 单元格3 | +| *斜体* | **粗体** | `代码` | + +复杂表格(HTML 方式,见第14节) + +## 8. 分割线 + +--- + +*** + +___ + +## 9. 脚注 + +这是带脚注的句子[^1] +[^1]: 脚注内容可以写在这里,甚至多段。 + +## 10. 转义字符 + +\*这不是斜体\* +\# 不是标题 +\[非链接\] + +## 11. 定义列表(部分渲染器支持) + +: 定义内容 + +## 12. 数学公式(需要 LaTeX 渲染) + +行内公式:$E = mc^2$ + +块级公式: +$$ +\int_{a}^{b} f(x) \, dx = F(b) - F(a) +$$ + +## 13. 图表与流程图(Mermaid) + +```mermaid +graph TD + A[开始] --> B{条件} + B -->|是| C[执行] + B -->|否| D[结束] +``` + +## 14. 嵌入 HTML(无所不能!) + +### 14.1 基础 HTML 标签 +高亮标记 +Ctrl + C +彩色文字 +
居中的 div
+ +### 14.2 表格增强 + + + + + + + + + +
合并单元格
行合并内容
另一行
+ +### 14.3 嵌入视频 / 音频 + + +### 14.4 嵌入 YouTube 视频 + + +### 14.5 自定义样式(甚至 CSS) + +
+ 这是一个带样式的盒子 +
+ +### 14.6 嵌入 SVG + + + + +### 14.7 表单元素 + + + +### 14.8 进度条、输入框等 +70% +60% + +## 15. 注解与评论(仅部分支持) + +[//]: # (这是注释,不会显示) + + + +## 16. 特殊技巧 + +### 16.1 避免嵌套冲突(用反斜杠或HTML包裹) +`
` 原样显示 + +### 16.2 内联 HTML + Markdown 混用(需要渲染器支持) +**粗体** 也能用 + +### 16.3 锚点跳转 +[跳转到表格](#7-表格) + +### 16.4 键盘按键样式(HTML) +Ctrl + Alt + Del + +### 16.5 缩写(HTML) +CSS 是样式语言。 + +## 17. 完整示例:一个带 HTML 的复杂卡片 + +
+ +

Markdown 用户

+

支持 粗体斜体 以及 代码

+ +
+ +--- diff --git a/mengyanote-backend/data/mengyanote/博客文章/Termux 直接编译运行安卓原生 DEX 教程.md b/mengyanote-backend/data/mengyanote/博客文章/Termux 直接编译运行安卓原生 DEX 教程.md new file mode 100644 index 0000000..ca404b5 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/博客文章/Termux 直接编译运行安卓原生 DEX 教程.md @@ -0,0 +1,165 @@ + +## 适用环境 + +Termux(无需 Root),任意 Android 手机。 + +## 目标 + +在手机上直接把 Java 源码编译成 Android 原生的 `.dex` 文件并运行,全程不依赖电脑,也不用 Android Studio。 + +用到的三个工具: + +- `ecj`:Java 编译器 +- `dx`:把 `.class` 转成 `.dex` +- `dalvikvm`:直接运行 `.dex` + +流程很简单: + +> Java源码 → ecj → .class → dx → .dex → dalvikvm 运行 + +--- + +## 一、前置准备:修复 Termux 镜像源 + +很多人在第一次执行 `pkg install` 的时候会遇到类似下面的报错: + +``` +E: The repository 'https://mirror.iscas.ac.cn/...' does not have a Release file. +``` + +这是因为 Termux 的镜像源失效了。 + +### 解决方法 + +1. 安装镜像管理工具: + +```bash +pkg install termux-tools -y +``` + +2. 切换镜像源: + +```bash +termux-change-repo +``` + +推荐选择 Grimler 官方源。亚洲用户选日本或香港镜像速度会比较快,比如在新加坡的话,选日本或香港都可以。 + +3. 更新软件包: + +```bash +pkg update -y +``` + +如果没有报错,说明环境已经好了。 + +--- + +## 二、安装编译工具 + +直接安装两个核心工具: + +```bash +pkg install ecj dx -y +``` + +装完之后检查一下,`ecj`、`dx`、`dalvikvm` 这三个命令应该都能用了。 + +--- + +## 三、写一个 HelloWorld + +在 Termux 里创建一个 Java 文件: + +```bash +cat > HelloWorld.java << 'EOF' +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello from Android DEX!"); + } +} +EOF +``` + +--- + +## 四、写个脚本自动编译运行 + +每次手动敲三条命令太麻烦,写个脚本自动完成: + +```bash +cat > build_and_run.sh << 'EOF' +#!/data/data/com.termux/files/usr/bin/bash + +ecj HelloWorld.java && \ +dx --dex --output=HelloWorld.dex HelloWorld.class && \ +chmod 444 HelloWorld.dex && \ +dalvikvm -cp HelloWorld.dex HelloWorld +EOF +``` + +给脚本加执行权限: + +```bash +chmod +x build_and_run.sh +``` + +--- + +## 五、跑起来 + +执行脚本: + +```bash +./build_and_run.sh +``` + +如果一切正常,你会看到: + +``` +Hello from Android DEX! +``` + +这就说明 Java 编译成功了,`.dex` 也生成好了,Dalvik 虚拟机顺利跑起来了。整个过程完全在手机上完成。 + +--- + +## 六、流程总结 + +再捋一遍整个流程: + +1. 写 `HelloWorld.java` +2. 用 `ecj` 编译得到 `HelloWorld.class` +3. 用 `dx` 转换得到 `HelloWorld.dex` +4. 用 `dalvikvm` 运行 + +这套方案的好处是: + +- 不需要 Android Studio +- 不需要电脑 +- 不需要 Gradle +- 不需要 Root + +适合用来学习 Android 底层机制、研究 Dalvik/DEX 格式,或者做一些极简的 Android 开发实验。 + +--- + +## 七、还能玩点什么? + +后面可以继续写一些纯 DEX 的小程序,比如: + +- 读取手机设备信息 +- 猜数字小游戏 +- 文件浏览器 +- 简单的 HTTP 客户端 +- 系统工具脚本 + +如果有兴趣的话可以在评论区留言,我会继续更新。 + +--- + +## 结语 + +通过 Termux 加上 ecj、dx、dalvikvm 这三个工具,我们可以在手机上直接完成 Java 编写、DEX 编译和 Android 虚拟机运行。这是一种很极简但也很硬核的 Android 开发方式,如果你喜欢研究底层,这套方案值得试试。 + +--- diff --git a/mengyanote-backend/data/mengyanote/博客文章/[萌芽妙妙工具].md b/mengyanote-backend/data/mengyanote/博客文章/[萌芽妙妙工具].md new file mode 100644 index 0000000..e69de29 diff --git a/mengyanote-backend/data/mengyanote/博客文章/未命名.md b/mengyanote-backend/data/mengyanote/博客文章/未命名.md deleted file mode 100644 index 760e373..0000000 --- a/mengyanote-backend/data/mengyanote/博客文章/未命名.md +++ /dev/null @@ -1,288 +0,0 @@ -下面是整理润色后的 完整博客文章版本,结构更清晰、步骤更完整,也更适合直接发布到博客平台(如 Wiki.js / WordPress / Halo 等)。 - - ---- - -Termux 直接编译运行安卓原生 DEX 教程(2026 最新版) - -作者:萌芽 - -适用环境: - -Termux(无需 Root) - -任意 Android 手机 - - -目标: -在 手机上直接把 Java 源码编译成 Android 原生 .dex 文件并运行,完全 不依赖电脑、不使用 Android Studio。 - -通过以下工具实现: - -ecj → Java 编译器 - -dx → 转换 .class → .dex - -dalvikvm → 直接运行 .dex - - -整个流程如下: - -Java源码 → ecj → .class → dx → .dex → dalvikvm运行 - - ---- - -一、前置准备:修复 Termux 镜像源 - -很多人在第一次执行 pkg install 时会遇到类似报错: - -E: The repository 'https://mirror.iscas.ac.cn/...' does not have a Release file. - -这是 Termux 镜像源失效导致的。 - -解决方法(一键修复) - -1 安装镜像管理工具 - -pkg install termux-tools -y - -2 切换镜像源 - -termux-change-repo - -推荐选择: - -Grimler 官方源 - -日本 / 香港镜像(亚洲用户速度更快) - - -如果你在 新加坡,建议选择: - -日本镜像 - -香港镜像 - - -3 更新软件包 - -pkg update -y - -如果没有报错,说明环境已经正常。 - - ---- - -二、安装 Java → DEX 编译工具 - -安装两个核心工具: - -pkg install ecj dx -y - -安装完成后可以使用: - -ecj -dx -dalvikvm - -分别用于: - -工具 作用 - -ecj Java 编译器 -dx class 转 dex -dalvikvm 运行 dex - - - ---- - -三、编写 HelloWorld 示例 - -首先创建一个 Java 文件: - -cat > HelloWorld.java << 'EOF' -public class HelloWorld { - public static void main(String[] args) { - System.out.println("Hello from Android DEX!"); - } -} -EOF - - ---- - -四、创建自动编译运行脚本 - -为了简化操作,可以写一个脚本自动完成: - -Java → class → dex → 运行 - -创建脚本: - -cat > build_and_run.sh << 'EOF' -#!/data/data/com.termux/files/usr/bin/bash - -ecj HelloWorld.java && \ -dx --dex --output=HelloWorld.dex HelloWorld.class && \ -chmod 444 HelloWorld.dex && \ -dalvikvm -cp HelloWorld.dex HelloWorld -EOF - -赋予执行权限: - -chmod +x build_and_run.sh - - ---- - -五、运行程序 - -执行: - -./build_and_run.sh - -如果一切正常,你会看到: - -Hello from Android DEX! - -说明: - -Java 编译成功 - -.dex 生成成功 - -Dalvik 虚拟机运行成功 - - -整个过程 完全在手机上完成。 - - ---- - -六、DEX 开发流程总结 - -完整流程如下: - -HelloWorld.java - ↓ -ecj 编译 - ↓ -HelloWorld.class - ↓ -dx 转换 - ↓ -HelloWorld.dex - ↓ -dalvikvm 运行 - -优点: - -不需要 Android Studio - -不需要电脑 - -不需要 Gradle - -不需要 Root - - -非常适合: - -学习 Android 底层 - -研究 Dalvik/DEX - -极简 Android 开发实验 - - - ---- - -七、进阶玩法(后续教程) - -后续可以继续实现一些 纯 DEX 小程序: - -例如: - -📱 读取手机设备信息 - -🎮 猜数字小游戏 - -📂 文件浏览器 - -🌐 简单 HTTP 客户端 - -🔧 系统工具脚本 - - -如果有兴趣,可以在评论区留言,我会继续更新教程。 - - ---- - -结语 - -通过 Termux + ecj + dx + dalvikvm,我们可以在手机上直接完成: - -Java 编写 - -DEX 编译 - -Android 虚拟机运行 - - -这是一种 极简但非常硬核的 Android 开发方式。 - -如果你喜欢研究 Android 底层机制,这套方案非常值得尝试。 - - ---- - -如果你愿意,我还可以帮你 再优化一个更“爆款”的标题版本,比如: - -爆款版标题 - -手机就能开发 Android?Termux 原生 DEX 编译教程 - -不用 Android Studio!手机直接编译运行 DEX - -2026 最硬核玩法:用 Termux 写 Android DEX 程序 - - -我也可以帮你再加一版 更适合技术博客 SEO 的版本(搜索排名更好)。 -切换完成后,更新软件包列表: -bash -pkg update -y - -第一步:安装核心工具链 - -我们需要安装 Eclipse Compiler for Java (ecj) 用于编译 Java 代码,以及 dx 工具用于将字节码转换为安卓 DEX 格式。 - -bash -pkg install ecj dx -y - -安装完成后,ecj、dx 和 dalvikvm 命令即可在 Termux 中使用。 - -第二步:编写 HelloWorld 程序 - -为了方便演示,我们直接在 Termux 中创建一个简单的 Java 源文件。 - -bash -cat > HelloWorld.java build_and_run.sh << 'EOF' -!/data/data/com.termux/files/usr/bin/bash -参数 1 代表传入的文件名(不含后缀) -ecj "1.java" && \ -dx --dex --output="1.dex" "1.class" && \ -chmod 444 "1.dex" && \ -dalvikvm -cp "1.dex" "$1" -EOF - -2. 赋予执行权限: - -bash -chmod +x build_and_run.sh - - - diff --git a/mengyanote-backend/data/mengyanote/博客文章/萌芽小窝序言.md b/mengyanote-backend/data/mengyanote/博客文章/萌芽小窝序言.md index 74c1196..6c708ae 100644 --- a/mengyanote-backend/data/mengyanote/博客文章/萌芽小窝序言.md +++ b/mengyanote-backend/data/mengyanote/博客文章/萌芽小窝序言.md @@ -1,35 +1,42 @@ ## 前言 -> 某不知名夜晚发出的深夜感想 -其实对于我个人来讲,从24年到现在搭建的网站还是挺多的,但都陆陆续续关了停了,比如这个博客,从高考毕业时开始就搭建,玩过wordpress,typecho,hexo,github pages静态博客等等都是自娱自乐,我个人其实说实话,比较懒,越到现在越是这样,但总觉得得折腾点什么,做点什么,在互联网上留下点痕迹,也好给未来的自己留个念想,慢慢地自己技术栈也在不断成熟, -从原来的依赖宝塔面板,到会docker部署,Linux自构建,lxc容器, -从godot游戏开发到转战到互联网,接触到前后端, -从GPT3.5对话聊天到cursor,trae, Windsurf AI IDE 再到Claude code ,codex,opencode,openclaw AI Agent , -从Android刷机接触到Linux,接触到软路由,nas,服务器,serverless,vps, -从初中时玩Minecraft,铁锈战争远程联机接触到frp内网穿透,接触到域名,tailscale,wireguard,easytier异地组网,接触到各种代理协议。 -唉,时代在变化,我也在变化,一下是我仍在活跃维护的一些网站,欢迎来到“我的世界” +于我个人来讲,从24年到现在搭的网站挺多,但都陆陆续续关了停了。比如这个博客,从高考毕业时就开始折腾,玩过 WordPress、Typecho、Hexo、GitHub Pages 静态博客等等,基本都是自娱自乐。 +说实话,我越来越懒,但总觉得得折腾点什么,在互联网上留点痕迹,也好给未来的自己留个念想。 + +慢慢地技术栈也在成熟: +- 从宝塔面板,到 Docker 部署、Linux 自构建、LXC 容器 +- 从 Godot 游戏开发,转到互联网前后端 +- 从 GPT-3.5,到 Cursor、Trae、Windsurf,再到 Claude Code、Codex、OpenCode、OpenClaw AI Agent +- 从 Android 刷机接触到 Linux、软路由、NAS、服务器、Serverless、VPS +- 从初中玩 Minecraft、铁锈战争远程联机,接触到 frp、域名、Tailscale、WireGuard、EasyTier 异地组网和各种代理协议 + +时代在变化,我也在变化。 + +以下是我仍在活跃维护的一些社交网站,时不时在里面发些癫. + +--- ### 萌芽主页: 算是一个网站主入口,里面罗列了我的各种项目 https://www.shumengya.top -https://smyhub.com -https://mengyaprofile.pages.dev +https://www.smyhub.com ### 萌芽笔记 存放各种收集来的和自制的教程,文档,记录,AI时代也方便搭建我的个人知识库 https://note.shumengya.top -https://mengyanote.pages.dev +https://note.smyhub.com ### 萌芽备忘录 既是备忘录,云剪切板,也是我日常吐槽的说说,类似开源版的QQ空间 https://memos.shumengya.top ### 萌芽小窝 -个人博客站点,框架就先使用PHP的typecho轻量博客框架,虽然有点老旧,技术栈也有点落后,但是胜在轻量小巧,实在懒得折腾新的博客框架了,累了 -https://blog.shumengya.top +个人博客站点,追崇技术极简风格 +https://blog.smyhub.com + +--- -未完待续... diff --git a/mengyanote-backend/data/mengyanote/实习求职/Golang岗位一览.md b/mengyanote-backend/data/mengyanote/实习求职/Golang岗位一览.md new file mode 100644 index 0000000..5ad56a1 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/Golang岗位一览.md @@ -0,0 +1,23 @@ + +### **大厂(明确在招Golang后端)** +这些顶级互联网公司近期在BOSS直聘等平台上有真实Golang后端开发岗位发布,主要分布在北京、上海、杭州、深圳、武汉等地,薪资多在20-60K+(视经验、城市而定,常带13-16薪): + +- **字节跳动(ByteDance / 抖音 / TikTok / 火山引擎 / 豆包 / 剪映)**:多个服务端/后端开发岗位(含Golang相关),如TikTok用户增长、剪映CapCut、火山引擎、豆包AI等方向的后端研发。 +- **腾讯(Tencent)**:腾讯云智研发有**Golang后端工程师**岗位(武汉等地),负责SSV实验室等项目后端服务设计、开发、维护。 +- **阿里巴巴 / 阿里云**:阿里云-诚云科技有**Golang后端开发工程师**岗位(杭州等地),薪资25-45K·13-16薪。 +- **京东(JD.com / 京东集团)**:京东云有**Golang后端开发工程师**岗位(北京亦庄等地),负责云数据库研发,高可用/弹性伸缩方向,30-60K·16薪。 +- **百度(Baidu)**:百度网盘智能客服等有**Golang后端研发**岗位(php/golang混合)。 +- **拼多多(Pinduoduo)**:有**Golang后端开发工程师**岗位发布(深圳等地)。 + +### **中厂 / 其他互联网科技公司(较多在招)** +中型互联网/SaaS/AI/游戏/电商公司岗位更多,平台上每天都有新Golang后端职位(薪资15-40K不等),典型例子包括: +- 博彦科技(Boyan)等科技服务型公司(AI产品方向Golang后端)。 +- 小蚕等杭州中型公司(Golang后端开发工程师)。 +- 卓拙科技、一些量化/私募、AI视频/游戏服务器开发公司(大量Golang游戏后端、AI后端岗位)。 +- 其他猎聘/BOSS直聘上常见的“某知名公司”或A轮中型互联网企业(上海、北京等地Golang后端/架构师岗位)。 + +**注意**: +- 大厂Golang岗位往往更偏向云原生、微服务、高并发、AI/云数据库、游戏服务器等方向,3-5年+经验为主,本科以上。 +- 中厂岗位更灵活,部分支持远程或居家(少量)。 +- 招聘信息实时变化,**建议直接去BOSS直聘搜索“Golang 后端”或“Go 后端”**,筛选“大厂/知名互联网”,或者去猎聘/Lagou查看最新JD。很多大厂也通过猎头或官网隐性招聘高级/架构师岗。 +- 目前市场仍有需求,尤其云、AI、游戏、电商方向,但竞争激烈,准备好Go并发、Gin/K8s、微服务等技能。 diff --git a/mengyanote-backend/data/mengyanote/实习求职/大厂校招投递官网.md b/mengyanote-backend/data/mengyanote/实习求职/大厂校招投递官网.md index 7a7e7ff..8152a0c 100644 --- a/mengyanote-backend/data/mengyanote/实习求职/大厂校招投递官网.md +++ b/mengyanote-backend/data/mengyanote/实习求职/大厂校招投递官网.md @@ -19,4 +19,24 @@ - **小红书**:https://job.xiaohongshu.com/campus/position(小红书校招) - **360集团**:https://360campus.zhiye.com/campus/jobs(360校招) - **腾讯音乐**:https://join.tencentmusic.com/campus(腾讯音乐校招) + +### 核心补充(强烈推荐关注) +- **华为**(科技巨头,校招规模大,含互联网/软件/云/AI等方向): + **https://career.huawei.com/cn/campus-recruitment**(官方校园招聘入口) +- **哔哩哔哩(B站)**: + **https://jobs.bilibili.com/home**(含校园招聘专区) +- **OPPO**(消费电子+互联网服务): + **https://careers.oppo.com/campus**(校园招聘) +- **vivo**(同上): + **https://hr.vivo.com/**(官网,有校园招聘板块)或 **https://hr-campus.vivo.com/**(校园专区) + +### 其他值得补充的中型/知名互联网企业 +- **携程**(旅游互联网巨头):通常在官网 careers.ctrip.com 或类似校园板块。 +- **唯品会**:校园招聘一般在官网或招聘平台。 +- **新浪/微博**:有校园招聘,关注官方渠道。 +- **得物(POIZON)**:新兴平台,校招活跃。 +- **阅文集团/腾讯系其他**(部分已覆盖腾讯音乐)。 + + + \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/实习求职/术语科普-HC.md b/mengyanote-backend/data/mengyanote/实习求职/术语科普/术语科普-HC.md similarity index 100% rename from mengyanote-backend/data/mengyanote/实习求职/术语科普-HC.md rename to mengyanote-backend/data/mengyanote/实习求职/术语科普/术语科普-HC.md diff --git a/mengyanote-backend/data/mengyanote/实习求职/术语科普-MT.md b/mengyanote-backend/data/mengyanote/实习求职/术语科普/术语科普-MT.md similarity index 100% rename from mengyanote-backend/data/mengyanote/实习求职/术语科普-MT.md rename to mengyanote-backend/data/mengyanote/实习求职/术语科普/术语科普-MT.md diff --git a/mengyanote-backend/data/mengyanote/实习求职/术语科普-PM.md b/mengyanote-backend/data/mengyanote/实习求职/术语科普/术语科普-PM.md similarity index 100% rename from mengyanote-backend/data/mengyanote/实习求职/术语科普-PM.md rename to mengyanote-backend/data/mengyanote/实习求职/术语科普/术语科普-PM.md diff --git a/mengyanote-backend/data/mengyanote/实习求职/简历/Golang后端开发.md b/mengyanote-backend/data/mengyanote/实习求职/简历/Golang后端开发.md new file mode 100644 index 0000000..4af090d --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/简历/Golang后端开发.md @@ -0,0 +1,40 @@ +# 后端开发工程师 +icon:user 男 / 2005.09.23 | icon:phone 15228246485 | icon:email [m](xxxxxxxx@163.com)ail@smyhub.com +icon:network  smyhub.com| icon:github [github.com/](https://github.com/acmenlei)shumengya | icon:tag  note.shumengya.top +## icon:user 教育背景 +::: start +- **西南石油大学 - 双一流本科 - 网络工程** +::: +**2024-09-01 至 2028-07-01** +::: end + +## icon:technology 专业技能 +- **Go 语言核心并发与性能调优:** 熟悉 Goroutine 调度、Channel 并发模型及 GMP 模型,了解内存逃逸分析与 GC 调优。 +- **Web 服务与微服务开发:** 熟悉使用 Gin、GoFrame 开发高性能 RESTful 服务,掌握中间件与路由分组;熟悉 gRPC + Protocol Buffers 构建微服务 +- **缓存认证与分布式中间件:** 熟悉掌握 Redis(缓存、分布式锁)、JWT/OAuth2 认证、消息队列(RabbitMQ、Kafka)等核心组件,具备高并发场景下的稳定性保障能力 +- **数据库设计与优化:** 熟悉 MySQL 数据库设计、SQL 语句优化、索引优化、联表查询及存储过程;对 MongoDB、PostgreSQL、SQL Server 等 NoSQL 与关系型数据库有一定了解和简单实践 +- **DevOps 与容器化部署:** 熟悉使用 Git、Docker、Nginx 及 LXC 容器化技术,熟悉 Maven、Gradle 构建流程;具备独立完成 Linux(Debian/Ubuntu)服务器部署、监控与运维能力 +- **云计算与边缘部署:** 熟悉使用阿里云、腾讯云云计算平台,同时具备 Vercel、CloudFlare、EdgeOne 等ServerLess边缘计算平台的快速部署与优化能力 +- **前后端协作能力**:熟悉 React、Vue 等主流前端框架,能使用Vibe Coding进行前端页面搭建,样式美化与基础交互开发,掌握前后端分离架构设计与开发,可配合前端完成完整业务闭环 +- **AI 大模型辅助开发:** 深入掌握 MCP、Skills 及大模型 Agent 开发,熟练使用 Codex、Claude Code、OpenCode 等 AI Agent 工具,以及 Cursor、Trae、GitHub Copilot 等AI 编程工具,大幅提升开发效率 + +## icon:project 项目经历 +- **项目名称**:萌芽小店-网关登录的轻量级商城系统 **|** 2026.1 - 2026.03 +- **技术栈**:Go(Gin,GORM),MySQL,RabbitMQ / Redis,Docker,Nginx,Vue,PWA +- **描述**:前后端分离的轻量级商城系统,支持商品展示与下单、自动/手动发货、邮件通知、收藏夹、客服消息、维护模式与 PWA 离线缓存;后台覆盖商品/订单/对话与 SMTP、站点运维。后端分层(Handler + Storage + GORM),对接第三方网关登录与多环境配置、容器化部署;公开接口、用户态与管理员令牌分层鉴权,限购与订单状态约束降低并发超卖风险。引入 Redis:商品列表与站点统计走 Cache-Aside、管理端写后失效,浏览去重用 SET NX 适配多实例,缓存不可用时降级直查数据库;管理后台提供 API/MySQL/Redis 连接与基础信息面板,便于排查。前端路由守卫与接口封装统一调用方式。 +- **项目地址**:https://store.smyhub.com **|** **开源地址**:https://github.com/shumengya/mengyastore +- **项目名称**:SproutClaw-服务器运维Agent助手 **|** 2026.3 - 2026.05 +- **技术栈**:ReAct,Skills,Agent RPC,TypeScript,MCP,Prompt Engineering,RAG +- **描述**:基于Pi-Mono Agent框架定制开发个人 AI 运维开发助手,覆盖代码修改、服务部署、远程服务器操作、会话管理和工具调用等场景。重点开发WebUI界面,为CLI Agent增加浏览器端互动界面,支持流式输出、历史会话、Markdown 渲染、响应式设计;同时定制中文启动界面、状态栏、TPS 统计、多服务器操作Prompt规范和 SSH/GitHub/Gitea/部署等技能能力,开发CloudFlare聚合搜索MCP给予AI网络搜索,和构建部署了一个轻量版基于MarkDown笔记的知识库系统,并暴露成Skills给Agent调用,提升Agent在真实 DevOps 场景中的实用性和自动化效率。 +- **项目地址**:https://sproutclaw.smyhub.com **|** **开源地址**:https://github.com/shumengya/sproutclaw + +## icon:trophy 荣誉奖项 +- 2025年全国大学生数字媒体科技作品及创意竞赛移动应用开发赛道国家二等奖 ,CET-4 + + + +## 自我评价 +热爱编程开发,熟悉 Go 语言、Redis、MySQL 及容器化部署,能独立完成从设计到上线的全链路开发。项目经验覆盖商城、运维助手等场景,注重实际生产与可运维性。具备前端协作与 AI 辅助开发能力,善于学习,乐于团队合作。 + + + diff --git a/mengyanote-backend/data/mengyanote/实习求职/简历/Java后端开发.md b/mengyanote-backend/data/mengyanote/实习求职/简历/Java后端开发.md new file mode 100644 index 0000000..317b571 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/简历/Java后端开发.md @@ -0,0 +1,36 @@ +# 后端开发工程师 +icon:user 男 / 2005.09.23 | icon:phone 15228246485 | icon:email [m](xxxxxxxx@163.com)ail@smyhub.com +icon:tag  smyhub.com| icon:github [github.com/](https://github.com/acmenlei)shumengya | icon:tag  note.shumengya.top +## icon:user 教育背景 +::: start +- **西南石油大学 - 双一流本科 - 网络工程** +::: +**2024-09-01 至 2028-07-01** +::: end + +## icon:technology 专业技能 +- 熟悉Java基础知识,熟悉集合与数据结构,面向对象思维,反射,异常,类加载等核心原理 +- 熟悉SpringBoot、Spring MVC、MyBatis、Spring、Spring AOP、IOC等中间件和框架 +- 熟悉Redis缓存、JWT认证、分布式锁、消息队列基础、 +- 熟悉MySQL数据库设计,SQL语句优化,索引,链表查询,储存过程,对其他数据库类型MongoDB,SQL Server,Postgres有一定了解 +- 熟悉Maven,Gradle,Git,Docker,Nginx,LXC容器化部署 +- 了解掌握React,Vue等前端框架,熟悉前后端分离架构开发 +- 熟悉Debian/Ubuntu等Linux系统运维,具备独立部署和维护服务器能力 +- 熟练掌握阿里云,腾讯云等云计算平台使用,具备Vercel,CloudFlare,EdgeOne等边缘云计算平台部署能力 +- 了解并掌握MCP,Skills,大模型Agent开发,熟练使用Codex,Claude Code,OpenCode等AI CLI工具和Cursor,Trae,Github Copilot进行辅助编程开发 + +## icon:project 项目经历 +- **项目名称**:萌芽小店-网关登录的轻量级商城系统 **|** 2026.1 - 2026.03 +- **技术栈**:Go(Gin,GORM),MySQL,RabbitMQ / Redis,Docker,Nginx,Vue,Vite,Tailwind,PWA +- **描述**:前后端分离的轻量级商城系统,支持商品展示与下单、自动/手动发货、邮件通知、收藏夹、客服消息、维护模式与 PWA 离线缓存;后台覆盖商品/订单/对话与 SMTP、站点运维。后端分层(Handler + Storage + GORM),对接第三方网关登录与多环境配置、容器化部署;公开接口、用户态与管理员令牌分层鉴权,限购与订单状态约束降低并发超卖风险。引入 Redis:商品列表与站点统计走 Cache-Aside、管理端写后失效,浏览去重用 SET NX 适配多实例,缓存不可用时降级直查数据库;管理后台提供 API/MySQL/Redis 连接与基础信息面板,便于排查。前端路由守卫与接口封装统一调用方式。 +- **项目地址**:https://store.smyhub.com **|** **开源地址**:https://github.com/shumengya/mengyastore +- **项目名称**:万象口袋-跨平台信息聚合网站 **|** 2025.08 - 2026.01 +- **技术栈**:Golang,Gin,GORM,MySQL,Redis,React,Vite,Tailwind,Docker,PWA +- **描述**:一站式资讯与工具导航网站:首页与栏目以配置驱动,整合 60s 类 API 浏览、休闲小游戏、实用工具箱及多款 AI 应用(后端 OpenAI 格式兼容与 SSE 流式输出)。后端采用 Go + Gin + GORM,MySQL 自动迁移存站点开关;可选接入 Redis 对高频只读配置做 cache-aside,独立逻辑库与 Key 前缀避免污染其他业务。提供聚合健康检查与仅管理员可调用的诊断快照接口。前端基于 React 与 Vite,Tailwind 统一视觉,axios 封装与路由守卫;对接账户认证中心 JWT,管理员后台支持 AI 上游、前台显隐配置及运行状态监控。支持多环境变量与开发/生产数据库隔离约束,兼顾可运维与交付效率。 +- **项目地址**:https://infogenie.smyhub.com **|** **开源地址**:https://github.com/shumengya/infogenie + +## icon:trophy 荣誉奖项 +- 2025年全国大学生数字媒体科技作品及创意竞赛移动应用开发赛道国家二等奖 ,CET-4 + + + diff --git a/mengyanote-backend/data/mengyanote/实习求职/简历/运维工程师.md b/mengyanote-backend/data/mengyanote/实习求职/简历/运维工程师.md new file mode 100644 index 0000000..fbec8da --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/简历/运维工程师.md @@ -0,0 +1,36 @@ +# 网络运维 +icon:user 男 / 2005.09.23 | icon:phone 15228246485 | icon:email [m](xxxxxxxx@163.com)ail@smyhub.com +icon:tag  smyhub.com| icon:github [github.com/](https://github.com/acmenlei)shumengya | icon:tag  note.shumengya.top +## icon:user 教育背景 +::: start +- **西南石油大学 - 双一流本科 - 网络工程** +::: +**2024-09-01 至 2028-07-01** +::: end + +## icon:technology 专业技能 +- 熟悉Java基础知识,熟悉集合与数据结构,面向对象思维,反射,异常,类加载等核心原理 +- 熟悉SpringBoot、Spring MVC、MyBatis、Spring、Spring AOP、IOC等中间件和框架 +- 熟悉Redis缓存、JWT认证、分布式锁、消息队列基础、 +- 熟悉MySQL数据库设计,SQL语句优化,索引,链表查询,储存过程,对其他数据库类型MongoDB,SQL Server,Postgres有一定了解 +- 熟悉Maven,Gradle,Git,Docker,Nginx,LXC容器化部署 +- 了解掌握React,Vue等前端框架,熟悉前后端分离架构开发 +- 熟悉Debian/Ubuntu等Linux系统运维,具备独立部署和维护服务器能力 +- 熟练掌握阿里云,腾讯云等云计算平台使用,具备Vercel,CloudFlare,EdgeOne等边缘云计算平台部署能力 +- 了解并掌握MCP,Skills,大模型Agent开发,熟练使用Codex,Claude Code,OpenCode等AI CLI工具和Cursor,Trae,Github Copilot进行辅助编程开发 + +## icon:project 项目经历 +- **项目名称**:萌芽小店-网关登录的轻量级商城系统 **|** 2026.1 - 2026.03 +- **技术栈**:Go(Gin,GORM),MySQL,RabbitMQ / Redis,Docker,Nginx,Vue,Vite,Tailwind,PWA +- **描述**:前后端分离的轻量级商城系统,支持商品展示与下单、自动/手动发货、邮件通知、收藏夹、客服消息、维护模式与 PWA 离线缓存;后台覆盖商品/订单/对话与 SMTP、站点运维。后端分层(Handler + Storage + GORM),对接第三方网关登录与多环境配置、容器化部署;公开接口、用户态与管理员令牌分层鉴权,限购与订单状态约束降低并发超卖风险。引入 Redis:商品列表与站点统计走 Cache-Aside、管理端写后失效,浏览去重用 SET NX 适配多实例,缓存不可用时降级直查数据库;管理后台提供 API/MySQL/Redis 连接与基础信息面板,便于排查。前端路由守卫与接口封装统一调用方式。 +- **项目地址**:https://store.smyhub.com **|** **开源地址**:https://github.com/shumengya/mengyastore +- **项目名称**:万象口袋-跨平台信息聚合网站 **|** 2025.08 - 2026.01 +- **技术栈**:Golang,Gin,GORM,MySQL,Redis,React,Vite,Tailwind,Docker,PWA +- **描述**:一站式资讯与工具导航网站:首页与栏目以配置驱动,整合 60s 类 API 浏览、休闲小游戏、实用工具箱及多款 AI 应用(后端 OpenAI 格式兼容与 SSE 流式输出)。后端采用 Go + Gin + GORM,MySQL 自动迁移存站点开关;可选接入 Redis 对高频只读配置做 cache-aside,独立逻辑库与 Key 前缀避免污染其他业务。提供聚合健康检查与仅管理员可调用的诊断快照接口。前端基于 React 与 Vite,Tailwind 统一视觉,axios 封装与路由守卫;对接账户认证中心 JWT,管理员后台支持 AI 上游、前台显隐配置及运行状态监控。支持多环境变量与开发/生产数据库隔离约束,兼顾可运维与交付效率。 +- **项目地址**:https://infogenie.smyhub.com **|** **开源地址**:https://github.com/shumengya/infogenie + +## icon:trophy 荣誉奖项 +- 2025年全国大学生数字媒体科技作品及创意竞赛移动应用开发赛道国家二等奖 ,CET-4 + +[https://store.smyhub.com](https://store.smyhub.com) + diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试star法则.md b/mengyanote-backend/data/mengyanote/实习求职/面试star法则.md new file mode 100644 index 0000000..f856719 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/面试star法则.md @@ -0,0 +1,73 @@ + +### STAR是什么? +STAR是四个英文单词的首字母缩写: + +- **S — Situation(情境/背景)** +- **T — Task(任务/目标)** +- **A — Action(行动/具体做法)** +- **R — Result(结果/成果)** + +面试官问行为类问题时(如“讲一次你解决团队冲突的经历”“描述一次你面对压力的情况”),用STAR结构回答,能避免东拉西扯、逻辑混乱,让回答更有说服力。很多500强公司和结构化面试都喜欢用这个框架来评估候选人。 + +### 每个部分的详细拆解与注意事项 + +1. **S — Situation(情境)** + 描述事情发生的背景,让面试官快速理解“当时是什么情况”。 + **要回答**:时间、地点、涉及的人、面临的问题或挑战、背景因素(比如项目紧急、资源有限、团队矛盾等)。 + **注意**:保持简洁,**2-4句话**即可,不要讲太多无关细节。目的是“搭舞台”,让后面内容有上下文。 + 常见开头:“在我上一份工作中/大三实习时/负责XX项目期间……” + +2. **T — Task(任务)** + 说明你在该情境中承担的具体责任或需要达成的目标。 + **要回答**:你的角色是什么?需要解决什么问题?目标是什么(最好有量化或明确的标准)? + **注意**:区分“我”而不是“我们”。明确你的个人任务,避免和情境混在一起。 + +3. **A — Action(行动)** + **这是整个回答的核心和重点**(通常占回答的50-70%)。 + 详细描述**你具体做了什么**、怎么做的、为什么这么做、遇到的困难及如何克服。 + **要回答**: + - 具体步骤(用“我”开头:我分析了数据、我组织了会议、我开发了方案……) + - 体现你的技能(如沟通、领导力、问题解决、数据分析等) + - 展示思考过程(我为什么选择这个方法?) + + **注意**:不要只说“团队努力”,要突出**你的个人贡献**。用动词开头:领导、协调、优化、分析、推动等。 + +4. **R — Result(结果)** + 说明行动带来的最终成果,最好**量化**(数字最有说服力)。 + **要回答**:达成了什么?提升了多少?节省了多少时间/成本?获得了什么反馈?后续影响? + **注意**:即使结果不是100%完美,也可以说“虽然……但我学到了……后来应用在……”(体现反思和成长)。可以加一句**反思/收获**,让回答更有深度(有些人叫STAR+R或STARS)。 + +**整体回答时长建议**:1-2分钟(视问题复杂度),练习到自然流畅。 + +### 经典例子(完整STAR回答) + +**面试问题**:请分享一次你解决团队冲突的经历。 + +**回答示例**(假设你是产品经理): + +- **S(情境)**:去年我在XX公司负责一个跨部门新功能开发项目,开发团队和设计团队因为交付时间和需求变更产生了严重冲突,导致两周内进度只完成了30%,团队士气很低。 +- **T(任务)**:作为项目负责人,我的任务是化解冲突,重新对齐目标,确保项目在截止日期前上线,同时不影响最终产品质量。 +- **A(行动)**:我先分别和双方核心成员进行1:1沟通,了解各自的痛点和顾虑;然后组织了一次结构化的联合会议,让双方用数据说话(我提前准备了进度甘特图和用户调研数据);我提出折中方案——采用分阶段交付,先上线MVP版本,再迭代设计细节;同时我调整了后续的沟通机制,每周固定sync会议并引入共享文档实时更新。 +- **R(结果)**:冲突在3天内化解,项目最终提前5天上线,用户反馈满意度达92%;双方团队后续合作更顺畅,公司还把这个沟通机制推广到其他项目。我也学到,在跨部门协作中,**提前对齐期望和用数据说话**非常重要。 + +这个回答结构清晰、数据支撑、有个人贡献,面试官能清楚看到你的**沟通能力、领导力、问题解决能力**。 + +### 常见行为面试问题(可提前用STAR准备) + +- 讲一次你面对挑战/高压的情况,如何处理的? +- 描述一次你展示领导力的经历。 +- 遇到团队成员不配合/表现不佳时,你怎么做? +- 讲一次你失败或犯错的经历,从中学到了什么? +- 如何处理多个优先级冲突的任务? + +**准备建议**: +- 提前准备**3-5个高质量故事**(不同维度:领导力、团队合作、问题解决、学习能力、客户导向等),一个故事可稍作调整应对多个问题。 +- 用**具体细节 + 数据**(百分比、金额、天数、人数等)增强可信度。 +- 多练习大声说出来,录音自查是否流畅、是否过多使用“我们”。 +- 如果是应届生,可以用实习、社团、课程项目、志愿者经历代替工作经验。 + +### 额外小Tips +- **比例分配**:S+T约20-30%,A约40-50%,R约20-30%。 +- 如果面试官追问细节(如“你当时怎么想的?”),正好可以补充Action部分的思考过程。 +- 结合**职位JD**(Job Description),优先选择能体现目标岗位核心competency(能力)的故事。 +- 有些公司用**CAR**(Context-Action-Result)或**PAR**,但本质类似,STAR最全面。 diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试经历/27双非本一腾讯IEG游戏安全后台实习面经.md b/mengyanote-backend/data/mengyanote/实习求职/面试经历/27双非本一腾讯IEG游戏安全后台实习面经.md deleted file mode 100644 index 1946aa5..0000000 --- a/mengyanote-backend/data/mengyanote/实习求职/面试经历/27双非本一腾讯IEG游戏安全后台实习面经.md +++ /dev/null @@ -1,58 +0,0 @@ -timeline: -9.16 一面 -9.18 二面 -9.22 三面 -9.23 HR面 -9.24 录用评估 -9.26 Offer - -腾讯一面(1h) - -1. 介绍实习需求(K8S 和 Casbin RBAC 相关) -2. 为啥初创实习两个月离职 -3. Go 为什么支持高并发 -4. GMP模型原理 -5. Goroutine Work-Stealing 的目的 -6. P的角色的作用,如果在M上维护Goroutine队列有什么不好 -7. GMP对CPU密集型任务能提高并发么 -8. IO操作需要CPU么,什么时候需要,磁盘IO和网络IO的区别 -9. Channel的作用和底层实现 -10. Channel的缓冲区在用户态还是内核态 -11. Goroutine阻塞等待的时候由谁来唤醒,需要额外的goroutine来遍历所有的channel么 -12. M上的G0是干嘛的 -13. 介绍select/poll/epoll -14. 网络IO的流程 -15. 了解过Go Runtime么 - -算法:求两个数的最大公约数 - -腾讯二面(1h) - -1. 介绍实习需求,最有挑战的部分 -2. RocksDB了解么,说一下LsmTree -3. 详细介绍一下Raft协议 -4. Raft协议和Paxos协议的区别,有哪些优化 -5. 介绍一下React Agent -6. LangChain 和 LangGraph 的区别 -7. Agent 和 LLM 的区别 -8. Function Call 和 MCP 的区别 -9. RPC的全流程 -10. 负载均衡算法有哪些 -11. 介绍一致性Hash算法,服务扩缩容之后有什么影响 -12. 网络编程 -13. 介绍一下TCP和UDP -14. 介绍一下HTTP各个版本及实现 - -算法: -1. 编辑距离 -2. 两两交换链表中的节点 - -腾讯三面(30min) -1. 介绍实习,你做了什么 -2. 介绍项目 -3. 实习时长,到岗时间,推HR面 - -腾讯HR面(15min) -1. 离职原因 -2. 实习时长,到岗时间 -3. 聊聊天 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅27双非本一腾讯IEG游戏安全后台实习面经.md b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅27双非本一腾讯IEG游戏安全后台实习面经.md new file mode 100644 index 0000000..7becbe8 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅27双非本一腾讯IEG游戏安全后台实习面经.md @@ -0,0 +1,577 @@ +timeline: +9.16 一面 +9.18 二面 +9.22 三面 +9.23 HR面 +9.24 录用评估 +9.26 Offer + +腾讯一面(1h) + +1. 介绍实习需求(K8S 和 Casbin RBAC 相关) +2. 为啥初创实习两个月离职 +## 3. Go 为什么支持高并发 +核心在于 **goroutine + GMP 调度模型**,不是单纯"语言支持并发"这么简单。 +**几个关键点:** +- **goroutine 很轻**:初始栈 2-4KB,可动态扩缩,几十万个 goroutine 开着没问题;线程是 MB 级别,差了两三个数量级 +- **GMP 调度是用户态的**:G(goroutine)、M(OS线程)、P(逻辑处理器),调度在用户空间完成,避免频繁的内核态切换,上下文切换成本低很多 +- **M:N 模型**:多个 goroutine 映射到少量 OS 线程,P 的数量默认等于 CPU 核数,充分利用多核 +- **network poller 集成**:IO 等待时 goroutine 会被挂起,M 可以去跑其他 goroutine,不会因为 IO 阻塞线程 +**踩过的坑:** +goroutine 轻归轻,泄漏问题很隐蔽——channel 没人读、context 没传导致 goroutine 一直挂着。我们线上出过一次 goroutine 数量涨到几十万的事故,最后用 `runtime.NumGoroutine()` 配合 pprof 才定位到。 +**和 Java 对比:** +Java 虚拟线程(Project Loom)思路类似,但 Go 这套从语言层面原生支持,心智负担小很多,不需要理解 Executor/线程池这套。 +本质上 Go 的高并发是**调度器设计 + 运行时配合**的结果,不是魔法。 + +## 1. GMP模型原理 +**三个角色:** +- **G(Goroutine)**:待执行的任务单元,包含栈、状态、函数指针 +- **M(Machine)**:真实的 OS 线程,执行 G 的载体 +- **P(Processor)**:逻辑处理器,持有本地 G 队列,M 必须绑定 P 才能执行 G +**调度流程:** +``` +G 创建 → 放入 P 的本地队列(或全局队列) +M 绑定 P → 从本地队列取 G 执行 +本地队列空 → work stealing,从其他 P 偷一半 +``` +**几个关键机制:** +- **work stealing**:P 本地队列空时去偷其他 P 的,减少全局锁竞争 +- **handoff**:M 因系统调用阻塞时,P 会脱离这个 M,转交给其他 M 继续跑,不让 P 空转 +- **抢占调度**:Go 1.14 之前是协作式(依赖函数调用时插入检查点),之后改成**信号抢占**,能打断死循环的 G,这个改动很重要 +**全局队列的作用:** +本地队列满了(容量256)会放全局队列;P 每隔一段时间也会主动从全局队列取,防止饥饿。 +**一个细节:** +M 和 P 不是固定绑定的,M 阻塞时 P 会"甩掉"它。所以 `GOMAXPROCS` 控制的是 P 的数量,不是线程数——线程数是动态的,可能超过 GOMAXPROCS。 +这块我在做性能调优时看过 runtime 源码,`schedule()` 函数是调度的核心入口,逻辑清晰但细节很多。 + +## 2. Goroutine Work-Stealing 的目的 +**一句话:负载均衡,让所有 P 都有活干,避免资源浪费。** +**具体场景:** +假设有 4 个 P,某个 P 的 G 队列跑完了,其他三个 P 还有大量 G 积压——如果不 stealing,这个空闲 P 就白白浪费了 CPU。 +**stealing 的策略:** +- 从其他 P 的本地队列**偷一半**(不是全部),减少被偷方的影响 +- 优先偷队列尾部的 G(被偷方从头取,减少竞争) +- 偷不到再去全局队列拿 +- 全局队列也空,才考虑进入休眠 +**为什么不用全局队列统一调度?** +全局队列需要加锁,高并发下锁竞争很激烈。本地队列 + work stealing 是**无锁优先**的设计,只有在 stealing 时才需要对目标 P 加锁,粒度小很多。 +**实际效果:** +我们压测时观察过,开了多个 P 之后 CPU 利用率明显更均匀,不会出现某个核跑满、其他核闲着的情况。这个机制对**任务分布不均匀**的场景尤其有价值,比如某些 G 执行很快、某些很慢。 +本质上是用**局部性**(本地队列减少竞争)换**全局均衡**(stealing 补偿倾斜),两者都要。 + +## 3. P的角色的作用,如果在M上维护Goroutine队列有什么不好 +**先说如果队列在 M 上会有什么问题:** +M 是 OS 线程,当它发生**系统调用阻塞**时,整个 M 被卡住——队列里的 G 没人执行,只能干等。要解决这个问题,就得把队列迁移到其他 M,但迁移本身代价很高,而且时机难把握。 +本质矛盾:**M 的生命周期和 G 队列的生命周期耦合在一起了。** +**P 的引入就是解耦:** +- M 阻塞 → P 直接甩掉这个 M,绑到其他空闲 M 上继续跑 +- 队列跟着 P 走,不跟着 M 走 +- 被唤醒的 M 重新找一个空闲 P 绑定,或者没有 P 就休眠 +P 本质上是一个**可迁移的执行上下文**,把"任务队列"和"执行载体"分离开。 +**P 的其他作用:** +- 持有 `mcache`(内存分配缓存),避免每次分配都竞争全局堆锁 +- 控制并发度——`GOMAXPROCS` 就是 P 的数量,而不是线程数 +- work stealing 的粒度是 P,而不是 M,逻辑更清晰 +**一句话总结:** +P 是为了让调度器在 M 阻塞时**不停摆**,同时顺手承担了内存和并发度管理的职责。没有 P 这层,GMP 就退化成简单的 M:N 模型,灵活性差很多。 + +## 4. GMP对CPU密集型任务能提高并发么 +**不能显著提升,这是 GMP 的局限。** +CPU 密集型任务的特点是 goroutine 一直占着 CPU 跑,不会主动让出,P 的数量又限制在 `GOMAXPROCS`(默认等于核数)。所以并发度上限就是 CPU 核数,GMP 再怎么调度也突破不了硬件限制。 +**GMP 真正发挥作用的场景是 IO 密集型:** +IO 等待时 goroutine 挂起,M 可以去跑其他 G,一个 M 可以服务大量 goroutine,吞吐上去了。CPU 密集型没有等待,这套机制用不上。 +**CPU 密集型的问题:** +- Go 1.14 前没有抢占,一个死循环的 G 能把整个 P 卡死 +- 1.14 加了信号抢占才解决,但调度开销本身也是额外成本 +- 大量 CPU 密集型 goroutine 反而会因为频繁上下文切换**降低**性能 +**实践建议:** +CPU 密集型任务,goroutine 数量控制在核数附近就够了,开太多适得其反。我们有个图像处理的服务,worker pool 大小设成 `runtime.NumCPU()` 反而比无限开 goroutine 快不少。 +所以 GMP 解决的核心问题是**IO 并发**,不是计算并行。 + +## 5. IO操作需要CPU么,什么时候需要,磁盘IO和网络IO的区别 +**IO 操作需要 CPU,但不是全程占用。** +大致分两个阶段: +- **发起阶段**:需要 CPU 准备请求、调用系统调用,时间很短 +- **等待阶段**:数据在传输,CPU 不参与,goroutine 挂起 +真正"烧 CPU"的是数据到达后的**处理阶段**,比如解析、序列化、加密。 +**网络 IO vs 磁盘 IO 的核心区别:** +网络 IO 在 Go 里是**完全异步**的,runtime 内置了 netpoller(基于 epoll/kqueue),goroutine 等待网络数据时会被挂起,M 去跑其他 G,等数据来了再唤醒。这套是 Go 原生支持的,效率很高。 +磁盘 IO 就麻烦了——**标准的文件读写是阻塞系统调用**,M 会被卡住,P 会执行 handoff 找新 M 来接管。本质上是用新线程来"补位",线程数可能因此暴涨。Go 没有像网络那样对磁盘 IO 做异步封装。 +**实际踩过的坑:** +有个服务大量读本地文件,压测时发现线程数涨到几百,就是这个原因。后来改成用 `io_uring`(通过 cgo)或者提前把文件内容 load 进内存,才解决。 +所以 GMP 对网络 IO 友好,对磁盘 IO 的支持其实是"凑合"的。 + +## 6. Channel的作用和底层实现 +**作用:goroutine 间通信 + 同步,Go 并发哲学"用通信共享内存"的核心载体。** +**底层结构 `hchan`:** +```go +type hchan struct { + qcount uint // 当前元素数 + dataqsiz uint // 环形缓冲区容量 + buf unsafe.Pointer // 环形缓冲区 + sendx uint // 发送索引 + recvx uint // 接收索引 + recvq waitq // 阻塞的接收者队列 + sendq waitq // 阻塞的发送者队列 + lock mutex +} +``` +核心是**环形缓冲区 + 两个等待队列**。 +**发送/接收逻辑:** +- buf 有空间 → 直接写入缓冲区,不阻塞 +- buf 满了 → 发送方 goroutine 进入 `sendq`,挂起 +- buf 空了 → 接收方进入 `recvq`,挂起 +- 有等待的接收方时,发送可以**直接拷贝到对方栈**,跳过缓冲区,减少一次内存拷贝 +**无缓冲 channel 的特殊性:** +没有 buf,发送和接收必须同时就绪,本质是**同步握手**,常用来做事件通知。 +**踩过的坑:** +- 向 closed channel 发送会 panic,但读 closed channel 会返回零值,这个不对称性坑过我一次 +- channel 不是银弹,高频小消息场景锁竞争反而比 `sync.Mutex` 高,我们有个计数器场景换成 atomic 之后性能提升明显 +**无缓冲 vs 有缓冲选择:** +需要严格同步用无缓冲,解耦生产消费速率用有缓冲,容量设多少要根据实际压测,拍脑袋容易出问题。 + +## 7. Channel的缓冲区在用户态还是内核态 +**用户态。** +`hchan` 整个结构体包括 buf 缓冲区,都是 Go runtime 在堆上分配的普通内存,和内核没有关系。goroutine 之间的数据拷贝也完全在用户空间完成,不涉及系统调用。 +这也是 channel 比 pipe 快的原因之一——pipe 是内核对象,读写都要经过内核态,有上下文切换成本。channel 就是用户态的内存拷贝,轻很多。 +**goroutine 挂起时呢?** +goroutine 进入 `sendq`/`recvq` 等待,这个挂起是 Go 调度器做的,也是用户态行为,不是 OS 的 sleep/block。M 不会被卡住,可以继续跑其他 G。 +只有当 M 执行了真正的阻塞系统调用(比如磁盘 IO),才会陷入内核态阻塞。channel 等待不会触发这个。 + +## 8. Goroutine阻塞等待的时候由谁来唤醒,需要额外的goroutine来遍历所有的channel么 +**不需要额外的 goroutine 来轮询,是发送/接收方直接唤醒的。** +**唤醒机制:** +goroutine 阻塞在 channel 上时,会把自己包装成 `sudog` 挂到 `sendq` 或 `recvq` 里,然后调用 `gopark` 让出 P,进入等待状态。 +唤醒发生在**对端操作时**: +- 有人往 channel 发数据 → runtime 从 `recvq` 取出等待的 G,调用 `goready` 把它放回 P 的运行队列 +- 有人从 channel 取数据 → 从 `sendq` 取出等待的发送方唤醒 +整个过程是**点对点的直接唤醒**,不存在后台轮询。 +**select 的情况稍复杂:** +select 多个 channel 同时等待时,goroutine 会把自己的 `sudog` 挂到每个 channel 的等待队列上。哪个 channel 先就绪,就由那个 channel 的操作方来唤醒,同时把这个 G 从其他 channel 的队列里摘除。 +**网络 IO 的唤醒不一样:** +网络等待是 netpoller 负责,runtime 有个后台线程跑 epoll,数据到了才唤醒对应的 goroutine。这是 channel 唤醒和网络 IO 唤醒的本质区别——前者是对端触发,后者是 poller 触发。 + +## 9. M上的G0是干嘛的 +**每个 M 都有一个特殊的 G0,专门用来执行调度逻辑,不跑用户代码。** +普通 G 运行用户代码,用的是自己的栈。但调度器本身也需要执行代码——比如 `schedule()`、goroutine 的创建销毁、栈扩容等——这些不能在用户 G 的栈上跑,否则栈管理会乱掉。 +G0 的栈直接用的是 M 对应 OS 线程的栈,容量比普通 G 大得多,固定不扩缩。 +**G0 干的事:** +- 执行 `schedule()` 找下一个可运行的 G +- goroutine 切换时的现场保存和恢复 +- 系统调用前后的处理 +- 栈扩容(`morestack`) +- GC 相关的一些操作 +**切换流程:** +``` +用户 G 运行 → 触发调度(主动让出/抢占/系统调用) +→ 切换到 G0 栈 +→ G0 执行 schedule() 找下一个 G +→ 切换回新的用户 G +``` +本质上 G0 是 M 的"管理员",每次调度都要经过它中转一次。理解 G0 对看 runtime 源码很关键,很多函数开头你会看到 `getg().m.g0` 这种写法。 + +## 10. 介绍select/poll/epoll +**三者都是 IO 多路复用,解决"一个线程监听多个 fd"的问题,核心差异在扩展性。** + +**select:** + +- 传入 fd_set(bitmap),内核遍历所有 fd 检查就绪状态 +- 限制:fd 数量上限 1024,每次调用都要把 fd_set 从用户态拷贝到内核态 +- 返回后还要自己遍历找哪个 fd 就绪,O(n) 复杂度 + +**poll:** + +- 用链表替代 bitmap,突破了 1024 限制 +- 但本质和 select 一样,每次调用仍然是全量拷贝 + 内核全量遍历 +- O(n) 没变,fd 多了一样慢 + +**epoll:** + +- 用 `epoll_create` 在内核创建一个事件表,`epoll_ctl` 增删 fd,fd 信息常驻内核,不用每次拷贝 +- 就绪时内核通过回调把事件放入就绪链表,`epoll_wait` 只返回就绪的 fd +- O(1) 获取就绪事件,fd 数量对性能影响很小 + +**epoll 两个触发模式:** + +- **LT(水平触发)**:fd 就绪没处理,下次 wait 还会通知,默认模式,不容易漏事件 +- **ET(边缘触发)**:只通知一次,必须一次性读完,性能更高但实现复杂,Nginx 用这个 + +**和 Go 的关系:** + +Go netpoller 底层在 Linux 上就是 epoll,`epoll_ctl` 注册网络 fd,有事件时唤醒对应 goroutine。这也是为什么 Go 网络 IO 能支撑大量并发连接的底层原因。 + +## 11. 网络IO的流程 +**以 Go 服务收到一个 TCP 请求为例,从上到下:** + +**应用层发起 read:** + +- goroutine 调用 `conn.Read()` +- 底层 fd 是非阻塞的,数据没到就返回 `EAGAIN` +- goroutine 把自己挂到 netpoller,调用 `gopark` 让出 P + +**内核层等待数据:** + +- 网卡收到数据包,DMA 直接写入内核缓冲区,不经过 CPU +- 内核协议栈处理:物理帧 → IP 包 → TCP 段 → 放入 socket 接收缓冲区 +- epoll 的回调触发,把这个 fd 标记为就绪 + +**netpoller 唤醒:** + +- Go runtime 后台有个线程轮询 `epoll_wait` +- 发现 fd 就绪 → 找到对应挂起的 goroutine → 调用 `goready` 放回运行队列 + +**goroutine 恢复执行:** + +- 从内核 socket 缓冲区把数据拷贝到用户态 buf(这里有一次内存拷贝) +- `conn.Read()` 返回,继续跑业务逻辑 + +**整个流程的关键点:** + +- 网卡到内核是 **DMA**,不占 CPU +- 内核到用户态有**一次拷贝**,零拷贝技术(sendfile、mmap)就是在优化这一步 +- goroutine 等待期间 M 不阻塞,去跑其他 G,这是 Go 高并发的关键 + +**write 方向类似**,只是反过来——用户态数据拷贝到内核发送缓冲区,内核负责实际发送,应用不用等发送完成。 + +## 12. 了解过Go Runtime么 +了解,前面聊的 GMP、netpoller、channel 其实都是 Runtime 的一部分,这里系统说一下我理解的几个核心模块。 + +**调度器(Scheduler)** 就是 GMP 那套,前面聊得比较深了,不展开。 + +**内存分配器** 基于 tcmalloc 思路,按对象大小分 tiny/small/large 三类。P 持有 `mcache`,小对象分配无锁直接从 mcache 拿,mcache 不够再从 `mcentral` 补,再不够从 `mheap` 申请。这套设计核心就是**减少锁竞争**,高并发下分配性能很好。 + +**GC(垃圾回收)** 三色标记 + 混合写屏障,Go 1.14 之后 STW 压缩到亚毫秒级。我们有个低延迟服务对 GC 比较敏感,调过 `GOGC` 和 `GOMEMLIMIT`,也做过对象池来减少 GC 压力。 + +**netpoller** 前面刚聊,基于 epoll/kqueue 封装,是 Go 网络高性能的基础。 + +**栈管理** goroutine 栈初始 2-8KB,不够时触发 `morestack` 进行**连续栈拷贝扩容**,早期是分段栈,切换开销大,后来换成连续栈。 + +**坦白说:** + +Runtime 源码我主要看过调度器和内存分配这两块,GC 的具体实现细节(比如写屏障的精确时机)我了解的不算特别深,停留在原理层面。 + +## 算法:求两个数的最大公约数 + +腾讯二面(1h) + +1. 介绍实习需求,最有挑战的部分 +## 2. RocksDB了解么,说一下LsmTree +了解,用过 RocksDB 做过嵌入式存储,也研究过它的存储引擎设计。 + +**LSM Tree 核心思路:** + +把随机写转成顺序写,用读性能换写性能。 + +**写入流程:** + +``` +写入 WAL(顺序写,保证持久化) +→ 写入内存 MemTable(跳表结构) +→ MemTable 满了 → 冻结成 Immutable MemTable +→ 后台线程 flush 到磁盘,生成 L0 SST 文件 +→ 后台 Compaction 合并各层 SST +``` + +**分层结构:** + +- L0:直接从内存 flush,文件间 key 范围可能重叠,读需要查多个文件 +- L1 以下:Compaction 后保证同层文件 key 不重叠,有序 +- 每层容量是上一层的倍数(默认10倍),数据逐层下沉 + +**读的代价:** + +最坏要从 MemTable → L0 → L1 → ... 逐层查,所以引入了 **Bloom Filter** 快速排除不存在的 key,**Block Cache** 缓存热数据。 + +**Compaction 的问题:** + +写放大是 LSM 的硬伤,一条数据可能被反复合并写多次。RocksDB 提供 Level Compaction 和 Universal Compaction 两种策略,前者读性能好,后者写放大小,需要根据场景选。 + +**对比 B+ Tree:** + +B+ Tree 随机写多,适合读多写少;LSM 顺序写,适合写多场景,Kafka、TiKV 底层都是这个思路。 + +我们用 RocksDB 时踩过 L0 文件堆积导致写停顿的问题,本质是 Compaction 速度跟不上写入速度,最后调了 `max_write_buffer_number` 和 Compaction 线程数才稳住。 + +## 3. 详细介绍一下Raft协议 +**Raft 解决的问题:分布式系统中多节点如何就"同一份数据"达成一致。** + +**核心角色:** + +- **Leader**:唯一接受写请求,负责日志复制 +- **Follower**:被动接收,响应 Leader 和 Candidate +- **Candidate**:选举中间态 + +**Leader 选举:** + +Follower 超过 `election timeout` 没收到心跳 → 变 Candidate → 自增 term,发起投票请求。 + +投票规则: + +- 每个节点每个 term 只投一票 +- Candidate 的日志必须比投票方**至少一样新**(term 更大,或 term 相同但 index 更大) +- 获得多数票 → 成为 Leader + +这个日志新旧比较是关键,保证选出来的 Leader 一定有最完整的日志。 + +**日志复制:** + +``` +Client 写请求 → Leader 追加日志(未提交) +→ 并行发 AppendEntries 给所有 Follower +→ 多数节点写入成功 → Leader commit +→ 返回客户端成功 +→ 下次心跳通知 Follower commit +``` + +**几个关键细节:** + +- **term** 是逻辑时钟,节点收到更大 term 立刻退回 Follower +- commit 只需要**多数派**,不需要全部节点,少数节点挂掉不影响可用性 +- Leader 只能提交**当前 term 的日志**,老 term 的日志靠新日志捎带提交,这个规则很绕但很重要,避免已提交日志被覆盖 + +**脑裂处理:** + +网络分区后可能出现两个 Leader,但旧 Leader 所在分区节点数少,无法获得多数派确认,写入不会 commit。分区恢复后旧 Leader 发现更大 term,自动退位,未提交的日志会被覆盖。 + +**和 Paxos 对比:** + +Paxos 理论上更通用,但工程实现极其复杂,各家实现差异大。Raft 限制了 Leader 唯一性、日志只能从 Leader 流向 Follower,大幅简化了理解和实现。etcd、TiKV 都是 Raft,没听说谁在生产用 Paxos 原版。 + +**实践中的坑:** + +`election timeout` 设置很关键,太小频繁选举,太大故障恢复慢。我们用 etcd 时遇到过网络抖动引发反复选举导致服务不可用,最后调大了 timeout 并优化了网络才稳定。 + +## 4. Raft协议和Paxos协议的区别,有哪些优化 +**核心区别:** + +Paxos 解决的是单值共识(Single Decree Paxos),Multi-Paxos 才能做日志复制,但论文里没有完整描述,各家实现差异极大。Raft 从设计之初就面向工程,限制更多但实现更统一。 + +几个具体差异: + +- **Leader 限制**:Raft 强 Leader,日志只从 Leader 流向 Follower;Paxos 允许任意节点提 Proposal,灵活但复杂 +- **日志连续性**:Raft 要求日志必须连续提交,不能有空洞;Paxos 允许乱序,需要额外机制填洞 +- **选举限制**:Raft 要求 Leader 必须有最新日志才能当选;Paxos 选出 Leader 后还需要 prepare 阶段补齐日志 +- **可理解性**:Raft 论文标题就叫"In Search of an Understandable Consensus Algorithm",这不是噱头,工程实现上确实差很多 + +**Raft 常见优化:** + +**PreVote** 解决网络分区恢复后,孤立节点 term 疯涨导致重新加入时触发不必要选举的问题。正式投票前先问一轮"我有没有资格参选",term 不增加。 + +**ReadIndex / LeaseRead** 读请求走 Leader 也需要确认自己还是 Leader,ReadIndex 要求 Leader 发一轮心跳确认多数派,LeaseRead 基于时钟租约跳过这轮确认,延迟更低但依赖时钟精度。 + +**Pipeline 和批量提交** AppendEntries 不等上一条 ACK 就发下一条,日志打包批量发送,提升吞吐。 + +**Joint Consensus 成员变更** 直接切换成员配置可能出现两个多数派,Joint Consensus 用两阶段过渡,先进入新旧配置共存状态再切换,etcd 实现了这个。 + +**Raft 的硬伤:** + +强 Leader 是双刃剑——简化了实现,但 Leader 是瓶颈,所有写都过一点。Multi-Raft(TiKV 的做法)把数据分 region,每个 region 一个 Raft 组,绕开了单 Leader 的吞吐限制,这是目前工程上比较成熟的解法。 + +## 5. 介绍一下React Agent +**ReAct = Reasoning + Acting,核心思路是让 LLM 交替输出"思考"和"行动",而不是一次性生成答案。** +**基本循环:** + +``` +Thought: 分析当前情况,决定下一步 +Action: 调用工具(搜索、查数据库、执行代码...) +Observation: 获取工具返回结果 +Thought: 基于结果继续推理 +... 循环直到得出最终答案 +``` + +这个循环让模型能处理需要多步推理和外部信息的复杂任务,单次推理搞不定的问题拆成多步来做。 +**和纯 Chain-of-Thought 的区别:** +CoT 只是让模型"想清楚再回答",但信息来源还是训练数据。ReAct 加入了 Action,能实时获取外部信息,解决知识截止和实时性问题。 +**工程实现的关键点:** +- **工具定义要清晰**:工具描述写得差,模型选错工具是最常见问题,我们花了不少时间打磨 tool description +- **终止条件**:循环必须有最大步数限制,模型有时会陷入无意义的重复调用 +- **Observation 长度控制**:工具返回内容太长直接塞进 context 很快撑爆,需要截断或摘要 +- **错误处理**:工具调用失败要告诉模型让它重试或换策略,而不是直接崩掉 +**实践中的问题:** +延迟是硬伤,每次 Action 都要等模型推理 + 工具执行,多轮下来 latency 很高。我们做过并行工具调用的优化——如果当前 Thought 判断多个工具互不依赖,同时发起调用,能省不少时间。 +另外模型能力对 ReAct 效果影响很大,小模型经常出现 Thought 和 Action 格式乱掉、工具选择混乱的问题,7B 以下基本不好用。 + +## 6. LangChain 和 LangGraph 的区别 +**一句话:LangChain 是线性链,LangGraph 是有状态的图,解决的复杂度不在一个量级。** +**LangChain 的定位:** +早期做 LLM 应用的胶水层,Chain 就是把 prompt → LLM → 输出解析 → 下一步 串起来。简单场景够用,但流程是固定的有向无环图,没法做条件分支、循环、回退。 +**LangGraph 解决什么:** +本质是个状态机框架,节点是函数,边可以是条件判断,整个流程是有环图。 +核心概念: +- **State**:贯穿整个流程的共享状态,每个节点读写它 +- **Node**:处理单元,可以是 LLM 调用、工具执行、任意函数 +- **Edge**:普通边或条件边,条件边根据 State 决定走哪个分支 +这样就能做 ReAct 循环、Human-in-the-loop、多 Agent 协作这些 LangChain 实现起来很别扭的场景。 +**实际体验:** +LangChain 抽象层太厚,早期版本 API 变动频繁,debug 很痛苦,出了问题不知道哪层出的。我们后来复杂流程直接用 LangGraph 或者干脆自己写状态机,反而更可控。 +LangGraph 的问题是学习曲线陡,State 设计不好后期很难维护,节点间依赖不清晰容易变成"意大利面图"。 +**选型建议:** +简单 RAG、单轮问答用 LangChain 够了。需要多步决策、循环、条件分支、多 Agent 就上 LangGraph,或者考虑 AutoGen、CrewAI 这些更面向 Multi-Agent 的框架。 + +7. Agent 和 LLM 的区别 +**LLM 是大脑,Agent 是能行动的完整个体。** +LLM 本质是个函数:输入 token,输出 token,无状态,没有记忆,不能主动做任何事。它只能"说",不能"做"。 +Agent 是在 LLM 外面套了一圈工程能力: +- **感知**:能接收外部输入(用户消息、工具返回、环境状态) +- **规划**:用 LLM 推理下一步该做什么 +- **行动**:调用工具、执行代码、操作系统 +- **记忆**:短期(context window)+ 长期(向量数据库/外部存储) +- **循环**:能持续运行直到任务完成,不是一次性的 +**类比:** +LLM 是一个知识渊博但被关在房间里的专家,只能回答你传纸条的问题。Agent 是这个专家 + 手脚 + 工具箱,能自己去查资料、打电话、写文件。 +**工程视角的本质差异:** +LLM 调用是无状态的单次 inference,Agent 是有状态的持续运行进程,需要管理状态流转、错误重试、工具调用生命周期。 +复杂度完全不在一个量级,这也是为什么 Agent 系统难落地——LLM 幻觉、工具调用失败、状态管理混乱,每一个都是工程难题。 + +## 8. Function Call 和 MCP 的区别 +**Function Call 是模型能力,MCP 是工具生态协议,层次不同。** +**Function Call:** +LLM 原生支持的能力,模型输出结构化的工具调用意图(JSON格式),由调用方解析后执行,结果再喂回模型。本质是约定了一种"模型表达调用意图"的格式。 +每家模型格式不一样,OpenAI、Claude、Gemini 的 function call schema 都有差异,工具定义和执行逻辑都耦合在业务代码里。 +**MCP(Model Context Protocol):** +Anthropic 推的开放协议,解决的是**工具标准化和复用**问题。核心思路是把工具实现抽成独立的 MCP Server,通过标准协议暴露,任何支持 MCP 的客户端都能接入。 +``` +Client(Agent框架)← MCP协议 → MCP Server(工具实现) +``` +工具不再散落在各个业务代码里,而是独立服务,可以被不同 Agent、不同模型复用。 +**核心区别:** +- Function Call 解决"模型怎么表达调用意图",MCP 解决"工具怎么标准化暴露和复用" +- 两者不互斥,MCP Server 里的工具最终还是通过 Function Call 机制被模型调用 +- MCP 更像是工具层的 REST API 标准化,降低了工具开发和接入成本 +**实践感受:** +MCP 理念很好,但生态还早期,稳定性和安全性都有问题——MCP Server 权限控制基本靠自觉,工具描述质量参差不齐。我们内部工具还是自己封装 Function Call,MCP 在观望。 + +9. RPC的全流程 +**RPC 目标:让远程调用看起来像本地函数调用,屏蔽网络细节。** +**完整流程:** +``` +调用方(Client) + → Stub 序列化参数 + → 建立连接 / 从连接池取连接 + → 发送请求(网络传输) + → 服务端 Stub 反序列化 + → 调用实际函数 + → 序列化返回值 + → 网络回传 + → 客户端反序列化 + → 返回给调用方 +``` +**几个关键环节:** +**序列化** 选型影响很大,JSON 可读性好但体积大、性能差;Protobuf 二进制紧凑、schema 强约束,是 gRPC 默认选择;Thrift 类似。我们从 JSON 换 Protobuf 之后,序列化耗时降了 60% 左右。 +**连接管理** 每次调用建新连接代价太高,生产上必须连接池。gRPC 基于 HTTP/2,单连接多路复用,天然解决了连接数问题。 +**服务发现** Client 怎么知道调哪个实例?一般接注册中心(etcd/Consul/Nacos),Client 拿到实例列表后本地做负载均衡,常见策略是轮询、一致性哈希、最小连接数。 +**超时和重试** 超时必须端到端设计,不能只设单次调用超时。重试要注意幂等性,非幂等接口盲目重试会出数据问题,我们踩过一次支付接口重试导致重复扣款。 +**拦截器/中间件** 日志、链路追踪、限流、鉴权都在这层做,gRPC 的 interceptor 机制很适合做这些横切关注点。 +**和 HTTP 的本质区别:** +HTTP 是文本协议面向资源,RPC 面向方法调用,性能更高,强类型约束,适合内部服务间通信。但 HTTP/REST 跨语言跨团队更友好,对外暴露接口还是 REST 居多。 + +## 10. 负载均衡算法有哪些 +**常见算法:** + +**轮询(Round Robin)** 最简单,依次分发。适合请求处理时间相近、机器配置均匀的场景。 + +**加权轮询** 给不同机器设权重,配置高的多分流量。Nginx 默认用平滑加权轮询,避免某个节点短时间内接太多请求。 + +**随机 / 加权随机** 比轮询实现更简单,大量请求下统计上趋于均匀,够用。 + +**最少连接数(Least Connections)** 选当前活跃连接最少的节点,适合请求处理时间差异大的场景,比轮询更"公平"。 + +**一致性哈希** 按 key(用户ID、请求特征)哈希到固定节点,同一个 key 始终打到同一台机器。适合有状态服务、缓存亲和性场景。虚拟节点解决节点数变化时的数据倾斜问题。 + +**P2C(Pick of 2 Choices)** 随机选两个节点,取负载更低的那个。比最少连接数实现简单,统计上效果接近,gRPC 默认用这个思路。 + +**自适应负载均衡** 结合延迟、错误率、CPU 等指标动态调整权重,B站开源的 Kratos 框架里有实现,适合机器配置差异大或负载不均匀的场景。 + +**实践经验:** + +大多数场景轮询够用,引入复杂算法前先确认瓶颈在哪。我们有个服务请求处理时间方差很大(从 10ms 到 5s 都有),换成最少连接数之后热点问题明显改善。 + +一致性哈希要小心哈希环倾斜,不加虚拟节点在节点少的时候分布很不均匀,踩过这个坑。 + +## 11. 介绍一致性Hash算法,服务扩缩容之后有什么影响 +**核心思路:** +把哈希空间构成一个首尾相连的环(0 到 2^32-1),节点和请求都哈希到环上,请求顺时针找最近的节点。 +``` + Node A (hash: 100) + / +环 → 请求(hash: 80) → 顺时针 → Node A + \ + Node B (hash: 200) +``` +**扩缩容影响:** +这是一致性哈希最大的优势——**只影响相邻节点的数据,不影响全局。** +加入新节点 C,插入到 A 和 B 之间: +- 原本打到 B 的部分请求,现在打到 C +- A 和其他节点完全不受影响 +- 迁移数据量是 1/N,普通哈希是全量重新分布 +删除节点 B: +- B 的请求全部顺时针转移到下一个节点 +- 其他节点不受影响 +**虚拟节点解决数据倾斜:** +真实节点少时,哈希环上分布不均匀,某些节点压力远大于其他节点。解决方案是每个物理节点映射多个虚拟节点(一般 100-200 个),让分布更均匀。 +节点权重也可以通过虚拟节点数量来体现,配置高的机器多分虚拟节点。 +**实际问题:** +扩容时新节点刚上线,数据还没预热,缓存命中率会有一段时间下降,对下游数据库有冲击。我们处理方式是**慢启动**——新节点先接少量流量,逐步提权重,给缓存预热时间。 +另外一致性哈希适合无状态 + 缓存亲和性场景,真正有状态的数据分片(比如分库分表)还是要配合数据迁移方案,不能只靠路由层解决。 + + +12. 网络编程 +13. 介绍一下TCP和UDP +**核心区别一句话:TCP 可靠有序有连接,UDP 不可靠无连接,性能换可靠性。** +**TCP:** +三次握手建连,四次挥手断连,保证: +- **可靠传输**:ACK 确认 + 超时重传 +- **有序**:seq 序号保证乱序重排 +- **流量控制**:滑动窗口,避免发太快把接收方打爆 +- **拥塞控制**:慢启动 + 拥塞避免,感知网络状况动态调整发送速率 +代价是连接状态维护、握手延迟、重传开销。 +**UDP:** +无连接,直接发包,没有确认、重传、排序。优点是延迟低、无队头阻塞、无连接建立开销。 +适合场景:实时音视频(丢帧比卡顿好)、DNS 查询、游戏同步。 +**一个值得聊的点——QUIC:** +HTTP/3 底层用 QUIC,本质是 UDP 上自己实现了可靠传输,但解决了 TCP 的几个问题: +- **队头阻塞**:TCP 一个包丢了整条连接阻塞,QUIC 多路复用流之间互不影响 +- **连接迁移**:TCP 连接和四元组绑定,切网络就断;QUIC 用 Connection ID,切网络连接不断 +- **0-RTT 握手**:复用之前的连接信息,首次之后建连更快 +我们有个移动端业务弱网环境下体验差,接入 QUIC 之后请求成功率提升明显,队头阻塞问题确实改善很多。 +**选型简单原则:** +需要可靠性用 TCP,需要低延迟且能容忍丢包用 UDP,需要两者兼顾考虑 QUIC。 + +14. 介绍一下HTTP各个版本及实现 +**HTTP/1.0** +每次请求独立 TCP 连接,请求完就断。并发全靠多开连接,overhead 极大。 +**HTTP/1.1** +主要改进: +- **Keep-Alive**:默认长连接,复用 TCP 连接 +- **Pipeline**:可以连续发多个请求不等响应,但响应必须按顺序返回,有**队头阻塞**问题,实际浏览器基本没启用 +- **分块传输**:支持流式响应,不用提前知道 Content-Length +队头阻塞是核心硬伤,浏览器绕过方式是对同一域名开 6 个并行 TCP 连接。 +**HTTP/2** +解决 1.1 的性能问题: +- **多路复用**:单连接上多个 stream 并行,解决了应用层队头阻塞 +- **头部压缩(HPACK)**:HTTP 头重复字段多,压缩后体积减小明显 +- **Server Push**:服务端主动推资源,实际用得少 +- **二进制分帧**:不再是文本协议,解析更高效 +但 TCP 层队头阻塞没解决——一个包丢了,所有 stream 都要等重传。 +**HTTP/3** +底层换成 QUIC(UDP),彻底解决队头阻塞: +- stream 之间独立,一个 stream 丢包不影响其他 +- 连接迁移,切网络不断连 +- 0-RTT 复用历史连接信息 +代价是 QUIC 实现复杂,很多中间件(防火墙、负载均衡)对 UDP 支持不好,落地有阻力。 +**实践感受:** +HTTP/2 在我们内部 RPC 场景收益明显,gRPC 默认就是 HTTP/2。HTTP/3 在移动端弱网收益大,PC 端强网环境提升有限,我们只在面向移动端的接入层启用了。 + +算法: +1. 编辑距离 +2. 两两交换链表中的节点 + +腾讯三面(30min) +1. 介绍实习,你做了什么 +2. 介绍项目 +3. 实习时长,到岗时间,推HR面 + +腾讯HR面(15min) +1. 离职原因 +2. 实习时长,到岗时间 +3. 聊聊天 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅4.13字节一面.md b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅4.13字节一面.md new file mode 100644 index 0000000..a25bbd2 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅4.13字节一面.md @@ -0,0 +1,298 @@ + +自我介绍 +## 1.rag项目背景 +*RAG(Retrieval-Augmented Generation,检索增强生成)是一种把“信息检索”和“文本生成”结合起来的 AI 方法:当用户提问时,系统会先从外部知识库(如文档、数据库或私有数据)中检索出相关内容,再将这些内容连同问题一起交给大模型生成答案。这样做可以避免模型凭空编造信息,让回答更加准确、可控,并且能够利用最新或专属的数据。* + +## 2.混合检索怎么做的 +*混合检索(Hybrid Retrieval)是将关键词检索(如基于 BM25 的搜索)和向量语义检索(如基于 FAISS 或 Milvus)结合起来的一种方法:系统会同时用“字面匹配”和“语义相似”两种方式查找相关内容,然后将结果合并并通过加权或重排序(rerank)筛选出最相关的部分,最后提供给大模型生成答案。这样既保证了检索的精确性,又提升了对自然语言语义的理解能力,从而显著提高整体检索和问答效果。* + +## 3.直接用用户问题检索吗 +*不会直接用用户原问题去检索。实际工程里一般会先做一层简单的 Query 处理,比如改写或补全,把口语问题变成更适合检索的表达,尤其是多轮对话场景会把上下文合进去。这样可以提升召回的稳定性和相关性。* +*简单场景可以直接用原问题,但线上系统基本都会做这一步。* + +## 4.有没有做过相关性能评估,召回率是多少 +*第一,指标怎么定义。* +*一般不会只说“召回率”,而是用 **Recall@K / Context Recall** 这类指标,比如 Top5、Top10 命中率。* +*本质就是:相关文档有没有被检索出来。* +*第二,实际效果区间。* +*在我做过的项目里(偏企业知识库场景):* +- 向量检索 baseline:Recall@5 大概 **60%~70%** +- 加 Query Rewrite + 混合检索:能到 **75%~85%** +- 再加 rerank:Top3 命中率能稳定在 **80%+** +这个数不是固定的,主要取决于三点: +- 数据质量(chunk 切分、embedding) +- query 处理(有没有改写/扩展) +- 是否做 rerank + +## 5.长期记忆和短期记忆是怎么做的 +*短期记忆主要是对当前会话的上下文管理,一般用 sliding window 保留最近几轮对话,超出长度后会做摘要压缩,保证关键信息还在,同时控制 token 成本。* +*长期记忆是把用户的关键信息做结构化沉淀,比如偏好、历史行为、重要事实等,存到数据库或者向量库里。后续对话时通过检索把相关内容再注入到上下文里,本质上是一个轻量的 RAG。* +*实际工程里两者会结合用:短期保证对话连贯,长期保证跨会话的个性化和记忆能力。* + +## 6.长期记忆是怎么存储的 +*长期记忆一般不会只用一种存储,而是分层做。* +*一类是结构化信息,比如用户偏好、标签、配置,这种直接存关系型数据库或 KV,读写稳定、查询也快。* +*一类是非结构化记忆,比如对话中抽取出来的事实、经历,会做 embedding 存到向量库里,方便后续按语义检索。* +*有些场景还会做一层“记忆抽取”,不是把原始对话全存,而是用模型把关键信息提炼成短文本再入库,避免噪音和冗余。* +*整体就是:结构化走数据库,语义记忆走向量库,查询时按需检索再注入上下文。* + +## 7.意图识别是怎么做的,直接用prompt吗 +*不会只靠 prompt。* +*简单场景可以用 prompt 做分类,比如让模型输出固定标签,这种上线快,但稳定性一般。* +*工程上更常见是两种方式结合:* +*一是轻量模型或规则做第一层意图分类(比如分类模型或关键词规则),保证稳定和可控;* +*二是大模型做补充或兜底,处理复杂、模糊意图。* +*如果对一致性要求高,还会做 schema 约束(固定输出格式),甚至把意图拆成多维标签(任务类型、领域、是否需要检索等)。* +*整体思路是:**能规则/小模型解决的先挡住,大模型负责复杂情况,而不是完全依赖 prompt。*** + +## 8.这里面用的mcp有哪些 +*如果说在这个链路里用到的 MCP,我理解主要是做**能力编排和外部调用的标准化**,不是核心算法部分。* +*比较常见的几个点:* +- ***检索侧 MCP**:把向量检索、关键词检索封装成统一接口,模型通过 MCP 去调用,而不是写死逻辑* +- ***记忆 MCP**:长期记忆的读写,比如用户信息查询、记忆写入,做成工具接口给模型调用* +- ***意图/工具路由 MCP**:模型根据意图决定调哪个能力,比如查知识库、查用户信息、调用业务接口* +- ***外部系统 MCP**:比如查询订单、触发业务操作,这些都通过 MCP 做标准化接入* +*实际好处是两点:* +*一是**解耦**,模型和后端能力分开演进;* +*二是**可控性**,哪些能力能调用、怎么调用,都可以在 MCP 层做约束。* +*整体上,MCP更像是“工具层/能力层”的统一协议,让大模型在 RAG、记忆、意图这些环节里可以稳定地调用外部系统。* + +## 9.有了解skills吗,和mcp的区别 +*skills是渐进性披露 MCP更像是一整套工具调用级 他们最显著的区别是token消耗上,skills更像是一本书的目录,当需要使用这个功能时,大模型才去学习了解掌握,所以现在有个很大的趋势是用skills+cli工具逐步替代臃肿的不必要的MCP工具* +- 用 Skills 控制上下文规模 +- 用 CLI/外部执行保证能力落地 +- 避免把大量工具 schema 一次性塞进 prompt +*但MCP 也不会完全被替代,在**多团队协作、复杂系统集成、强治理场景**下,它的标准化优势还是很有价值。* + +## 10.使用claude code、cursor这类ai编程工具,你有哪些使用心得 +这个问题挺实际的,我日常用 Cursor 比较多,Claude Code 也在用,说几个真实感受: +### 用得顺手的场景 +- **样板代码生成**:CRUD、proto 定义、单测骨架,直接让它生成,省去大量机械劳动 +- **跨文件重构**:Cursor 的 `@codebase` 索引能理解上下文,改个接口签名然后让它同步所有调用方,效率很高 +- **不熟悉的技术栈**:比如临时写个 Python 脚本或者前端页面,AI 补全基本够用 +### 踩过的坑 +- **过度信任生成结果**:早期会直接 accept,后来发现它会"幻觉式地"引用不存在的方法或者用错 API 版本,现在习惯 diff 必看 +- **上下文窗口限制**:项目大了之后它对全局架构的理解会飘,给出的方案有时和现有设计冲突,需要手动喂上下文 +- **让它做设计决策是浪费**:拿 AI 讨论"要不要用 MQ"这种问题,答案基本是废话,这类问题还是得自己想 +### 我现在的用法 +把它当**高级自动补全 + 初稿生成器**,而不是"架构师"。 +核心流程:自己想清楚结构 → 让 AI 填肉 → 人工 review 逻辑正确性。 + +## 11.claude code泄漏的源码有了解吗,具体有哪些好的做法 +了解这个事件,3月31日 npm 包里意外打包了 `.map` 文件,51万行 TypeScript 直接曝光。说说从中能学到的 agent 工程化做法: +### 几个值得借鉴的设计 +**1. 三层记忆架构(解决 context entropy)** +- `MEMORY.md` 作为轻量索引,每行约 150 字符的"指针",**永久加载**到 context +- 不是 store-everything 的 RAG,而是精炼后的结构化事实 +- agent 被明确指令:把自己的记忆当 **hint 而非事实**,执行前要 verify +**2. autoDream — 空闲时的记忆整合** +后台 subagent 在用户 idle 时运行,做 4 件事: +``` +Orient → Gather → Consolidate → Prune +``` +把零散观察合并、消除矛盾、把模糊 insight 固化为 fact。这个思路对长期 agent 很实用,我们自己做 RAG 时其实也该有类似的"写回"机制。 +**3. KAIROS — 主动式 daemon 模式** +从 reactive 转向 always-on,后台监听日志、主动触发任务。这是 agent 从"工具"进化为"协作者"的关键设计。 +**4. ULTRAPLAN — 任务卸载** +复杂规划任务 offload 给远程 Opus 会话,最长跑 30 分钟。本质是**分级调度**——轻任务本地快模型,重任务找强模型。 + +--- +对我启发最大的是**记忆设计**这块——当前大多数 agent 框架的记忆要么是 full context 堆砌,要么是向量召回,Anthropic 这个"指针索引 + 定期整合"的方式在工程上更可控。 +另外泄露还暴露了一个有意思的数字:内部模型 Capybara v8 的 false claims rate 是 29-30%,反而比 v4 的 16.7% 更差——说明 agentic 场景下 scaling 不是银弹,工程侧的约束同样关键。 + +## 12.本地部署Ollama模型,并发调用是一个接一个的响应还是并发的 + +### Ollama 默认行为:串行 +默认情况下同一个模型**排队处理**,并发请求会 queue 住,一个跑完再跑下一个。 +原因是模型加载后占满显存,单卡跑多个并发推理会 OOM 或效率极差。 +### 但可以并发,关键参数: +```bash +# 控制同时加载的模型实例数(默认1) +OLLAMA_NUM_PARALLEL=4 + +# 控制同时保持加载的模型数量 +OLLAMA_MAX_LOADED_MODELS=2 +``` +设了 `NUM_PARALLEL` 之后,同一模型可以真正并发推理,但代价是**每个请求的速度会下降**,显存压力翻倍。 +### 实际踩坑 +- 消费级显卡(比如 4090 24G)跑 `llama3:70b-q4` 本来就压线,开并发直接崩 +- 小模型(7B/8B)+ 量化的情况下,`NUM_PARALLEL=2~4` 比较可行 +- CPU offload 模式下并发更差,延迟会乘倍 +### 生产场景怎么做 +如果真要高并发,Ollama 不是最优选: + +|方案|适合场景| +|---|---| +|vLLM|高并发首选,PagedAttention 真并发| +|llama.cpp server|轻量,支持 `--parallel`| +|Ollama|本地开发、单用户场景够用| + +vLLM 的 continuous batching 才是真正解决并发的方案,Ollama 更多是开发体验优先的工具。 + + +## 13.Mysql的事务级别 +四个级别,从低到高: +### 隔离级别对比 + +|级别|脏读|不可重复读|幻读| +|---|---|---|---| +|READ UNCOMMITTED|✅ 有|✅ 有|✅ 有| +|READ COMMITTED|❌ 无|✅ 有|✅ 有| +|REPEATABLE READ|❌ 无|❌ 无|⚠️ 部分| +|SERIALIZABLE|❌ 无|❌ 无|❌ 无| + +MySQL 默认是 **REPEATABLE READ**。 +### 几个细节 +**RR 下幻读问题**:InnoDB 用 **Gap Lock + Next-Key Lock** 解决了大部分幻读,但不是完全没有—— + +```sql +-- 事务A +SELECT * FROM t WHERE id > 10; -- 读到3条 + +-- 事务B insert + commit + +-- 事务A +UPDATE t SET val=1 WHERE id > 10; -- 影响了4行 +SELECT * FROM t WHERE id > 10; -- 现在能看到4条了,幻读出现 +``` +当前读(`SELECT FOR UPDATE`)会加 Next-Key Lock,快照读(普通 `SELECT`)走 MVCC 不加锁。 +**RC 在互联网场景更常用**:很多公司把隔离级别调成 RC,原因是: +- Gap Lock 少,死锁概率低 +- 并发性能更好 +- 业务上可以接受不可重复读 +**MVCC 是 RR 的底层实现**:每个事务有自己的 Read View,通过 undo log 版本链实现快照读,不加锁也能隔离。 + +## 14.怎么防止幻读 +### 两种手段 +**1. MVCC(快照读)** +普通 `SELECT` 走版本链,事务开始时生成 Read View,后续其他事务插入的数据对你不可见。 +不加锁,性能好,但只能解决**快照读**场景的幻读。 +**2. 加锁(当前读)** +```sql +-- 间隙锁,锁住 (10, 20) 区间,其他事务无法在此范围插入 +SELECT * FROM t WHERE id > 10 FOR UPDATE; + +-- 插入意向锁会被 Gap Lock 阻塞 +INSERT INTO t (id) VALUES (15); -- 被阻塞 +``` +InnoDB 在 RR 下自动加 **Next-Key Lock**(Record Lock + Gap Lock),锁住记录本身和前面的间隙。 +### 两种方案的边界 + +|场景|方案|说明| +|---|---|---| +|只读查询,允许历史快照|MVCC|无锁,性能好| +|读后要写,强一致|`SELECT FOR UPDATE`|加 Next-Key Lock| +|最严格|SERIALIZABLE|所有读自动加锁| + +--- + +### 踩坑点 +```sql +-- 先快照读(MVCC,不加锁) +SELECT * FROM t WHERE id > 10; + +-- 再当前读(触发加锁) +UPDATE t SET val=1 WHERE id > 10; + +-- 此时能读到其他事务新插入的行 —— 幻读! +``` +**混用快照读和当前读是幻读的常见来源**,业务上如果要防幻读,从一开始就用 `FOR UPDATE` 保持一致。 + +## 15.Mysql的索引结构介绍下 +### InnoDB 用 B+ 树 +核心结构特点: +- 非叶子节点只存 **key**,不存数据 +- 叶子节点存完整数据,且**双向链表**串联 +- 树高一般 3~4 层,千万级数据 3 次 IO 能查到 +``` + [30] + / \ + [10,20] [40,50] + / | \ / | \ +[数据][数据][数据][数据][数据] ← 叶子节点双向链表 +``` +--- +### 聚簇索引 vs 非聚簇索引 +**聚簇索引**(主键索引):叶子节点存的是**整行数据**,一张表只有一个。 +**二级索引**:叶子节点存的是**主键值**,查完还要回表。 +```sql +-- 走二级索引,需要回表 +SELECT * FROM t WHERE name = 'xx'; + +-- 覆盖索引,不回表 +SELECT id, name FROM t WHERE name = 'xx'; +``` +--- +### 为什么不用其他结构 + +|结构|问题| +|---|---| +|二叉树/红黑树|树太高,IO 次数多| +|Hash|不支持范围查询| +|B 树|非叶子节点存数据,扇出小,树更高;范围查询要回溯| + +B+ 树叶子链表天然支持范围扫描,这是核心优势。 +### 几个实际问题 +**索引失效常见场景:** +- 最左前缀没满足 +- 对索引列做函数/运算:`WHERE YEAR(create_time) = 2024` +- 隐式类型转换:字段是 varchar,查询传 int +**索引过多的代价:**写操作要维护所有 B+ 树,insert/update 变慢,选择性低的字段建索引意义不大。 + +## 16.跳表有了解吗 +了解,Redis 的 ZSet 底层就用了跳表。 +### 结构 +多层链表叠加,每层是下层的"快速通道": +``` +L3: 1 -------------------------> 50 +L2: 1 -----------> 20 --------> 50 +L1: 1 ----> 10 --> 20 --> 30 --> 50 +L0: 1 -> 5 -> 10 -> 15 -> 20 -> 25 -> 30 -> 50 (原始链表) +``` +查找时从最高层开始,逐层下降,时间复杂度 **O(log n)**。 + +--- +### 核心操作 +**插入时随机决定层高**,每层晋升概率 50%(Redis 用 25%),期望层高 O(log n)。 +不需要像 B 树那样做复杂的旋转平衡,**实现简单**是跳表的重要优势。 + +--- +### 对比 B+ 树 + +| |跳表|B+ 树| +|---|---|---| +|实现复杂度|低|高| +|范围查询|链表顺序遍历,很快|叶子链表,也快| +|内存占用|指针多,稍高|紧凑| +|磁盘友好|❌|✅ 页对齐| + +**跳表更适合内存场景**,B+ 树更适合磁盘 IO 场景,这是 Redis 选跳表、MySQL 选 B+ 树的核心原因。 + +### Redis ZSet 为什么用跳表不用 B+ 树 +除了内存友好,还有一点:**ZRANGE 这类范围操作**在跳表上实现非常直观,代码简单;另外跳表支持 O(log n) 的 rank 查询,实现排行榜很自然。 +有意思的是 ZSet 小数据量时还会用 **ziplist/listpack** 而不是跳表,超过阈值才转换,典型的空间换时间权衡。 + +## 17.为什么mysql不用跳表 +### 本质是磁盘 IO 模型不同 +跳表是**指针链式结构**,节点在内存中随机分布,每次指针跳转都可能是一次随机内存访问。 +放到磁盘上就是**随机 IO**,每次节点访问都可能触发一次磁盘寻道,代价极高。 + +### B+ 树专门为磁盘设计 +- 节点 = 页(InnoDB 默认 16KB),一次 IO 读一页,捞出大量 key +- 非叶子节点只存 key,**扇出极大**,3~4 层覆盖千万数据 +- 叶子节点页内连续,范围扫描是**顺序 IO** +``` +B+ 树一次 IO:读 16KB,包含几百个 key +跳表一次跳转:读一个节点,可能就几十字节,然后随机跳下一个地址 +``` + +### 量化对比 +千万数据: +- B+ 树:树高 3~4,最多 **3~4 次 IO** +- 跳表:O(log n) ≈ 23 跳,每跳随机 IO,**不可接受** + +### 一句话总结 +>跳表的随机指针在内存里没问题,到磁盘上随机 IO 会把性能打死。B+ 树是为页式存储专门设计的结构。 +这也是为什么**内存型数据库**(Redis、MemTable)偏爱跳表,**磁盘型数据库**清一色 B/B+ 树。 + +手撕没做出来,非leetcode原题 diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅Go后端开发-字节一面.md b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅Go后端开发-字节一面.md new file mode 100644 index 0000000..5332d48 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅Go后端开发-字节一面.md @@ -0,0 +1,195 @@ +Go后端开发-字节一面 +## 1.自我介绍 +好的。 +我目前有大概 6 年后端开发经验,前几年在一家中型电商公司做业务后端,后来转到一家做 SaaS 的公司担任 Tech Lead,带过 5 人左右的小团队。 +技术栈主要是 Go,早期也写过一段时间 Java。做过的系统包括高并发订单、Feed 流、消息推送这类,QPS 峰值在几万级别,算不上超大规模但该踩的坑基本都踩过。 +最近一年多在做 AI 工程化方向,主要是 RAG 系统落地和 Agent 框架搭建,也做过一些推理侧的优化——主要是降延迟和控成本,用过 vLLM,对 KV Cache、量化这些有实践经验,不算特别深但都是真实上线过的。 +对字节感兴趣主要是两点:一是规模,字节的系统体量能接触到我现在遇不到的问题;二是 AI 基础设施这块,字节在这方面投入很大,我想在更大的平台上深耕这个方向。 +有什么想深入了解的可以直接问。 + +## 2.如果让你编译一个golang的程序,在linux下执行包含哪些系统调用 +**编译阶段**(编译器本身运行时) +```bash +go build main.go +``` +编译器进程本身会用到: +- `execve`:启动编译器进程 +- `open/read`:读取源文件、依赖包 +- `write`:写出目标文件 +- `mmap`:映射文件到内存 +- `fork/clone`:编译器可能启动子进程(汇编器、链接器) +**程序启动阶段**(./main 执行时) +Go runtime 初始化涉及: +- `execve`:启动进程 +- `mmap`:申请虚拟地址空间(堆、栈) +- `mprotect`:设置内存页权限 +- `clone`:创建系统线程(M) +- `sigaction`:注册信号处理(GC、抢占调度依赖信号) +- `futex`:线程同步 +**运行阶段** +- `mmap/munmap`:堆内存扩缩 +- `read/write`:IO 操作 +- `epoll_create/epoll_wait`:netpoller 底层 +- `nanosleep`:调度器 sleep +- `rt_sigreturn`:信号处理返回 +**验证方式** +```bash +strace ./main 2>&1 | head -50 +``` +实际跑一下一目了然,Go 程序启动时 mmap 调用特别多,VSZ 看起来大就是这个原因。这块我是结合 strace 实际观察过,但内核源码层面没深入读过。 + +## 3.一个http请求的整个路由过程 +**从网络到业务代码的完整路径** + +**1. 网卡 → 内核网络栈** + +``` +网卡收包 → DMA 写入 Ring Buffer +→ 硬中断通知 CPU → 软中断(NET_RX) +→ TCP/IP 协议栈处理 → 数据写入 socket 接收缓冲区 +``` + +**2. epoll 唤醒** Go netpoller 基于 epoll: + +``` +epoll_wait 检测到 fd 可读 +→ 唤醒对应 goroutine +→ 从 socket buffer 读取数据 +``` + +**3. HTTP 解析** + +``` +读取原始字节流 +→ 解析请求行(Method、Path、Version) +→ 解析 Header +→ 读取 Body +``` + +**4. 路由匹配** 以 gin 为例: + +``` +注册时构建路由树(Radix Tree) +请求到来 → 按 Method 找对应树 +→ 前缀匹配找到 handler +→ 提取路径参数(:id、*path) +``` + +**5. 中间件链执行** + +``` +middleware1 → middleware2 → handler → middleware2 → middleware1 +``` + +洋葱模型,gin 用 index 递增控制: + +```go +c.Next() // 推进到下一个handler +c.Abort() // 中断链路 +``` + +**6. 响应写回** + +``` +handler 写 ResponseWriter +→ 序列化响应体 +→ 写入 socket 发送缓冲区 +→ 内核发送 +``` + +踩过的坑:路由注册顺序影响匹配,`/user/profile` 和 `/user/:id` 同时注册时要注意静态路由优先级高于参数路由,gin 的 radix tree 处理了这个但自己实现路由容易踩。 + +4.拷打项目(拷打的非常多) +## 5.http2和http3的区别,http3不可靠的udp怎么实现可靠 +**HTTP/2 vs HTTP/3 核心区别** + +||HTTP/2|HTTP/3| +|---|---|---| +|传输层|TCP|QUIC(基于 UDP)| +|队头阻塞|解决了 HTTP 层,TCP 层还有|彻底解决| +|握手|TLS 1~2 RTT|0~1 RTT| +|连接迁移|不支持|支持(换网络不断连)| + +**HTTP/2 的遗留问题** + +多路复用解决了 HTTP 层队头阻塞,但 TCP 是字节流,一个包丢失整条连接都要等重传——**TCP 层队头阻塞**依然存在,弱网环境下反而比 HTTP/1.1 更差。 + +**HTTP/3 = HTTP over QUIC** + +QUIC 在 UDP 之上自己实现了可靠传输: + +**可靠性保障机制** + +``` +序列号:每个 QUIC 包有唯一 Packet Number,单调递增 +ACK:接收方确认,发送方维护发送窗口 +重传:超时或收到 NACK 触发重传 +流量控制:连接级 + 流级别双重控制 +``` + +**彻底解决队头阻塞** + +``` +Stream A: [pkt1] [pkt2丢失] [pkt3] +Stream B: [pkt4] [pkt5] +``` + +Stream A 丢包只阻塞 Stream A,Stream B 不受影响——因为 QUIC 在用户态管理每个 Stream 的状态。 + +**0-RTT 握手** + +``` +首次连接:1-RTT(比TLS1.3 over TCP 少一次) +再次连接:0-RTT,直接带上之前的会话票据 +``` + +**连接迁移** TCP 连接由四元组标识,换 IP 就断了。QUIC 用 **Connection ID** 标识连接,切换网络(WiFi → 4G)连接不中断。 + +这块我了解原理,生产环境还没大规模用过 HTTP/3,主要是服务端支持和中间件兼容性还不够成熟。 + +6.手撕代码:二叉树的右视图 +## 7.mysql给一个比较长的读取操作,例如对全表进行读取,再读取的同时来了一个写入操作会发生什么;快照会把所有的数据都copy一份吗;每一次读取都需要把undo log回放一遍吗 +**全表读 + 并发写入会发生什么** + +MySQL InnoDB 默认 RR 隔离级别,普通 SELECT 走 **MVCC 一致性读**: + +``` +全表扫描开始 → 创建 ReadView(记录当前活跃事务列表) +→ 读取每行时,根据 ReadView 判断该版本是否可见 +→ 期间写入操作正常进行,不阻塞读 +→ 读到的始终是事务开始时的快照版本 +``` + +写入不会被读阻塞,读也不会被写阻塞——这是 MVCC 的核心价值。 + +**快照会 copy 所有数据吗** + +**不会**,这是个常见误解。 + +ReadView 只是一个轻量结构: + +```go +// 伪代码 +ReadView { + creator_trx_id // 创建者事务ID + trx_ids // 创建时活跃事务列表 + up_limit_id // 活跃事务最小ID + low_limit_id // 下一个事务ID +} +``` + +数据还在原地,版本链挂在行记录上,通过 undo log 串起来。读取时按需回溯,不是提前 copy。 + +**每次读都要回放整个 undo log 吗** + +**不是回放,是版本链查找**,而且不一定每次都要遍历: + +``` +行记录 → trx_id 可见?直接读 + → 不可见 → 找 roll_pointer → 上一版本 + → 再判断可见性 → 直到找到可见版本 +``` + +代价取决于版本链长度——长事务 + 高频更新的行,版本链可能很长,读性能会下降。 + +**踩过的坑**:长事务不提交导致 undo log 无法清理,版本链越来越长,后来读这些热点行明显变慢。监控里加了 `INFORMATION_SCHEMA.INNODB_TRX` 告警,发现长事务及时干掉。 diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节-后端开发面经.md b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节-后端开发面经.md new file mode 100644 index 0000000..71eb610 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节-后端开发面经.md @@ -0,0 +1,350 @@ +## 1.介绍rabbit MQ在多级缓存架构中的使用 +在多级缓存(本地缓存 + Redis + DB)架构里,RabbitMQ 主要解决**缓存一致性**和**异步更新**两个问题。 + +### 核心用法 + +**1. 缓存失效广播** + +数据变更时,通过 RabbitMQ fanout exchange 广播失效消息,各节点收到后删除本地缓存(Caffeine/Guava)。比直接用 Redis pub/sub 的好处是有消息持久化,节点重启后能消费积压消息补偿。 + +**2. 异步 DB → 缓存同步** + +写操作先更新 DB,发一条消息到队列,消费者异步刷新 Redis。这样写链路不阻塞,但要接受短暂的不一致窗口。 + +### 踩过的坑 + +- **重复消费导致缓存写脏**:消费者要做幂等,通常带上数据版本号,只更新版本更新的数据 +- **消息顺序问题**:同一个 key 的多次变更如果乱序消费,可能写入旧值。解法是按 key hash 到同一个 queue,或消费时比较时间戳/版本 +- **本地缓存更新延迟**:fanout 消息到达有延迟,这段时间内各节点本地缓存不一致是正常的,业务上要能接受 + +### 我的判断 + +RabbitMQ 在这个场景里适合**写频率不极高**的业务。如果是高频写场景(比如计数器),引入 MQ 反而加重链路复杂度,直接 TTL 过期 + 旁路缓存更简单。 + +你们这边缓存一致性要求是强一致还是最终一致? + +## 2.多级缓存先删redis还是本地缓存 +**先删本地缓存,再删 Redis。** + +### 原因 + +多级缓存的读取顺序是:本地缓存 → Redis → DB + +如果先删 Redis: + +- 其他节点的本地缓存还存着旧数据 +- 并发请求打过来,直接从本地缓存返回旧值,Redis 已经没了也不会回源 +- 本地缓存的脏数据会一直存到 TTL 过期 + +先删本地缓存的好处: + +- 本地缓存 miss 后会去查 Redis,Redis 此时还有数据,不会击穿到 DB +- 再删 Redis 后,下次请求会回源 DB 重建缓存,数据是最新的 + +### 但这还不够 + +单机删自己的本地缓存没问题,**其他节点的本地缓存怎么办**? + +这就是上一个问题里 RabbitMQ fanout 的价值——广播失效消息,让所有节点同步删本地缓存。 + +顺序就变成: + +1. 删当前节点本地缓存 +2. 发 MQ 消息 → 其他节点消费后删各自本地缓存 +3. 删 Redis(或者更新 Redis) + +### 一个细节 + +步骤 2 和 3 之间有个时间窗口,其他节点本地缓存还没删完,Redis 已经删了,这时候并发读会击穿到 DB。**高并发场景要加分布式锁或者 singleflight 防击穿。** + +## 3.热门票库存缓存一致性如何保证?分布式架构如何完成票务扣减? +这是个经典的热点库存问题,核心矛盾是**高并发读写 + 不能超卖 + 性能不能崩**。 + +### 库存缓存一致性 + +直接用 Redis 作为库存的**主存储**,DB 异步对账,不走传统的"缓存 + DB 双写"模式。 + +原因:热门票瞬间 QPS 可能十万级,DB 完全扛不住,Redis 原子操作才是核心防线。 + +```lua +-- Lua 脚本保证原子扣减 +if redis.call('get', KEYS[1]) >= ARGV[1] then + return redis.call('decrby', KEYS[1], ARGV[1]) +else + return -1 -- 库存不足 +end +``` + +Lua 脚本保证**check-and-decrement 原子性**,避免 ABA 问题。 + +### 分布式扣减流程 + +1. **预扣减**:Redis Lua 脚本扣库存,成功则生成订单流水写 MQ +2. **异步落库**:消费 MQ,写 DB 订单记录 + 更新 DB 库存 +3. **超时回滚**:订单 15 分钟未支付,MQ 延迟消息触发回滚,Redis 库存 +1 + +### 踩过的坑 + +- **Redis 单 key 热点**:单个 key 的 QPS 上限约 10w,超过会成瓶颈。解法是**库存分片**,把 1000 张票拆成 10 个 key 各存 100,随机路由,最后汇总 +- **Redis 宕机库存丢失**:开启 AOF + 主从,且 DB 是最终兜底,定期对账补偿 +- **MQ 消费失败导致库存不一致**:消费端做幂等(订单号去重),失败重试,最终靠对账任务兜底 + +### 我的判断 + +强一致在这个场景基本做不到,**目标是最终一致 + 不超卖**。允许极短暂的库存数字不准,但绝不能多卖。对账任务是最后的安全网,不能省。 + +## 4.为什么使用rabbit MQ,你的技术考量是什么? +坦白说,**RabbitMQ 不一定是最优选**,我来说说我的判断依据。 + +### 选 RabbitMQ 的理由 + +- **消息可靠性**:支持持久化、confirm 机制、死信队列,适合"不能丢消息"的场景,比如库存回滚、订单状态流转 +- **灵活的路由**:fanout/topic/direct exchange 原生支持,缓存失效广播用 fanout 很自然 +- **成熟的生态**:运维工具完善,团队上手成本低 + +### 但它的缺点我很清楚 + +- **吞吐量天花板低**:单机约 10w msg/s,Kafka 能到百万级 +- **不适合消息堆积**:设计上不是为海量堆积优化的,堆积多了性能会下降 +- **不支持消息回溯**:消费完就没了,Kafka 可以按 offset 重放 + +### 票务场景我会这样选 + +|场景|我的选择|原因| +|---|---|---| +|库存扣减异步落库|RabbitMQ|消息量不大,可靠性优先| +|缓存失效广播|Redis pub/sub 或 RocketMQ|允许少量丢失,延迟更低| +|订单日志/埋点|Kafka|海量、需要回溯| + +### 我的判断 + +选 MQ 核心看三点:**消息可靠性要求、吞吐量量级、是否需要回溯**。 + +票务核心链路我更倾向 **RocketMQ**——国内互联网场景更常见,延迟消息原生支持(订单超时回滚直接用),吞吐比 RabbitMQ 高,运维也比 Kafka 简单。 + +你们现在用的是哪个? + +## 5.令牌前置过滤器怎么设计的呢? +这里说的"令牌前置过滤器"是指 **API Gateway 层的 Token 认证过滤器**吧?我按这个方向回答。 + +### 核心设计 + +``` +请求 → Gateway Filter → Token验证 → 路由到下游服务 + ↓ + Redis 缓存 Token + ↓ + 必要时回源 Auth服务 +``` + +过滤器做的事:提取 Token → 验签 → 鉴权 → 透传用户信息 + +### 关键实现点 + +**1. JWT 本地验签,不回源** + +用公钥本地验 JWT 签名,避免每次请求都打 Auth 服务。只有 Token 在黑名单里才需要走 Redis 查一次。 + +**2. Token 黑名单** + +退出登录、强制下线的 Token 写入 Redis,key 是 `blacklist:{jti}`,TTL 对齐 Token 过期时间。过滤器验签通过后,再查一次黑名单。 + +**3. 用户信息透传** + +验证通过后把 userId、roles 等写入请求 Header,下游服务直接取,不用重复解析 Token。 + +### 踩过的坑 + +- **时钟偏移导致 Token 误判过期**:JWT exp 校验加 30s 容忍窗口 +- **黑名单 Redis 挂了怎么办**:降级策略是只做本地验签,跳过黑名单检查,安全性降级但服务不中断 +- **Filter 执行顺序**:认证 Filter 要在限流、日志 Filter 之前,否则匿名流量也被计入限流配额 + +### 性能考量 + +本地验签是纳秒级,Redis 黑名单查询是微秒级,整体加在 Gateway 的延迟 **< 1ms**,可以接受。 + +你们是 Spring Cloud Gateway 还是自研的? + +## 6.redis的数据结构有哪些,适用场景? +### 核心数据结构 + 场景 + +**String** + +- 最基础,计数器、分布式锁、缓存单个对象 +- 库存扣减就是 `INCRBY` / `DECRBY` + +**Hash** + +- 存对象字段,比 String 序列化整个对象更节省内存,支持单字段更新 +- 适合用户信息、商品详情,字段频繁部分更新的场景 + +**List** + +- 有序、可重复,支持两端操作 +- 消息队列(简单场景)、最新动态列表、评论分页 + +**Set** + +- 无序不重复,支持交并差集 +- 去重场景:UV 统计、抽奖已参与用户、共同好友 + +**ZSet(Sorted Set)** + +- 带分数的有序集合,按 score 排序 +- 排行榜、延迟队列(score 存时间戳)、热搜榜 + +**Bitmap** + +- 本质是 String,按位操作 +- 用户签到(365 天只占 46 字节)、活跃用户统计 + +**HyperLogLog** + +- 概率统计基数,误差约 0.81%,内存固定 12KB +- UV 统计,不需要精确值时替代 Set + +**Stream**(Redis 5.0+) + +- 持久化消息队列,支持消费者组 +- 轻量级替代 MQ,但功能比 RabbitMQ 弱很多 + +### 我常踩的坑 + +- **ZSet 排行榜**:成员数量大时 `ZRANGE` 全量拉取很慢,要分页 +- **Hash vs String**:Hash 字段超过 128 个或 value 超过 64 字节会从 ziplist 转 hashtable,内存会涨,要注意 + +## 7.项目中最具有挑战的地方?是否还有别的方法? +结合我们前面聊的票务场景,说一个我觉得最有挑战的:**热点库存的分片一致性问题**。 + +### 挑战在哪 + +库存分片成 10 个 key 后,扣减是分散的,但有几个棘手问题: + +- **路由不均**:随机路由在瞬间高并发下,某个分片可能先扣完,其他分片还有余量,导致用户看到"无票"但实际有票 +- **汇总库存不准**:各分片单独扣,总库存数字是聚合计算的,实时性差 +- **回滚对齐**:超时回滚时要还回原来扣的那个分片,订单必须记录扣了哪个分片 + +### 我的解法 + +订单创建时记录 `shard_id`,回滚精准还回对应分片。查询总库存用 Lua 脚本聚合所有分片,允许短暂不精确。 + +路由策略从随机改成**轮询 + 分片余量感知**,定期探活各分片剩余量,优先路由到余量多的。 + +### 还有别的方法吗 + +**方案二:Redis Cluster 自动分片** + +不手动分片,直接用 Cluster 让 Redis 自己做 slot 分布。好处是运维简单,坏处是 Lua 脚本跨 slot 不支持,原子性保证变难。 + +**方案三:放弃 Redis,用数据库行锁** + +SELECT FOR UPDATE 直接锁库存行,强一致,但 QPS 撑不住,只适合低并发场景。 + +**方案四:本地内存扣减 + 异步同步** + +每个节点预分配一批库存到本地,扣完再申请。延迟最低,但节点宕机会丢已分配未消费的库存,需要心跳 + 回收机制。 + +### 我的判断 + +方案四在极致性能场景下值得考虑,但工程复杂度高,**容错逻辑写起来很脏**。大多数场景 Redis 分片已经够用,过度优化没必要。 + +## 9.字符编码 +这个问题范围比较广,我说几个后端开发中真正重要的点。 + +### 核心概念 + +- **ASCII**:128 个字符,单字节,英文够用 +- **Unicode**:字符集,定义了全球字符的**码点**(如 `U+4E2D` = '中') +- **UTF-8**:Unicode 的**编码方式**,变长 1-4 字节,兼容 ASCII,是互联网事实标准 +- **UTF-16**:Java/Go 内部字符串用的,`char` 是 2 字节,处理 emoji 等增补字符需要代理对 +- **GBK**:国标,双字节,老系统常见,和 UTF-8 不兼容 + +### 后端常踩的坑 + +**1. MySQL 的 utf8 不是真 UTF-8** + +MySQL 的 `utf8` 最多 3 字节,存不了 emoji(需要 4 字节)。要用 `utf8mb4`,这个坑踩过一次就忘不了。 + +**2. Go 的 string 和 rune** + +```go +s := "中文" +len(s) // 6,字节数 +len([]rune(s)) // 2,字符数 +``` + +按字节切字符串会截断多字节字符,中文场景要用 `[]rune`。 + +**3. 服务间编解码不一致** + +老系统用 GBK,新系统用 UTF-8,HTTP 接口对接时乱码。解法是在边界层统一转码,内部全用 UTF-8。 + +### 一个细节 + +UTF-8 的**BOM**(`EF BB BF`)在 Linux 下会导致脚本执行异常,Windows 记事本默认带 BOM,跨平台协作时要注意。 + +有具体想深入的方向吗?比如 Go 的字符串底层或者 MySQL 编码问题? + +10.如何学习 +## 11.手撕:查询字符数组中与目标数组匹配的起始位置 +经典字符串匹配,直接上 **KMP**,暴力 O(nm) 在面试里说得过去但不够亮眼。 + +### 思路 + +KMP 核心是复用已匹配信息,通过 `next` 数组避免回溯,时间复杂度 **O(n+m)**。 + +### 代码(Go) + +```go +func search(source, target []byte) []int { + n, m := len(source), len(target) + result := []int{} + if m == 0 || n < m { + return result + } + + // 构建 next 数组 + next := make([]int, m) + next[0] = 0 + for i, j := 1, 0; i < m; i++ { + for j > 0 && target[i] != target[j] { + j = next[j-1] + } + if target[i] == target[j] { + j++ + } + next[i] = j + } + + // 匹配 + for i, j := 0, 0; i < n; i++ { + for j > 0 && source[i] != target[j] { + j = next[j-1] // 回退但不回溯 i + } + if source[i] == target[j] { + j++ + } + if j == m { + result = append(result, i-m+1) + j = next[j-1] // 继续找下一个匹配 + } + } + return result +} +``` + +### 关键点 + +- `next[i]` 表示 target 前 i+1 个字符中,**最长公共前后缀长度** +- 匹配失败时 `j = next[j-1]`,i 不回退,这是 KMP 的精髓 +- 找到一次匹配后 `j = next[j-1]` 而不是 `j = 0`,支持**重叠匹配** + +### 验证 + +``` +source: a b a b c a b a b +target: a b a b +output: [0, 4] ✓ +``` + +暴力解我也能写,但 KMP 在 target 有大量重复前缀时优势明显,比如 `aaaaaab` 这种模式,暴力退化严重。 \ No newline at end of file diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节Agent中台一面凉经.md b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节Agent中台一面凉经.md new file mode 100644 index 0000000..b72e75e --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节Agent中台一面凉经.md @@ -0,0 +1,1459 @@ +字节|后端开发(Agent中台)一面凉经 +1自我介绍 +2Agent项目是实习项目还是个人项目?有没有上线? +3拷打实习(10min) +## 大模型微调,你的训练数据集是如何构建的?数据量有多大? +**背景** + +我们做的是垂直领域的指令微调(SFT),场景是内部知识问答 + 特定格式输出,不是通用能力增强。 + +**数据构建方式** + +**1. 人工标注 + 模板生成** + +- 业务侧整理了几百条高质量种子数据(问题+答案对) +- 用 GPT-4 做数据增强,一条种子扩展出 10~20 条变体 +- 人工复核筛掉明显错误的 + +**2. 从业务日志挖掘** + +- 线上用户真实提问 + 人工审核的回答 +- 质量高但获取慢,大概占总量 30% + +**3. 合成数据** + +``` +文档 → GPT-4 生成 QA 对 → 人工抽样验证 +``` + +用于知识覆盖,但质量参差不齐,筛选成本不低 + +**数据量** + +最终用了大概 **2~3 万条**指令数据,领域比较窄,数据量不算大。 + +说实话开始以为要几十万才够,后来发现高质量数据 1~2 万条在垂直场景下效果已经不错,数据质量远比数量重要。 + +**踩过的坑** + +- 早期数据格式不统一,prompt 模板和 base model 训练时用的不一致,导致微调后输出格式乱——后来统一用模型原始 chat template +- 数据里混入了矛盾样本(同一问题不同答案),模型输出不稳定,加了去重和一致性校验才解决 + + +## 在构建数据集的过程中,遇到了哪些挑战?花了多长时间? +**主要挑战** + +**1. 数据质量参差不齐** + +GPT-4 生成的增强数据看起来流畅,但细节经常有偏差: + +- 业务术语用错 +- 回答格式不符合我们要求 +- 偶尔出现幻觉内容混入 + +早期没有严格校验,直接训练,模型输出一塌糊涂,才意识到要加过滤流程。 + +**2. 标注一致性难保证** + +多人标注同一类问题,答案风格差异很大——有人回答简洁,有人喜欢展开,模型学完之后输出风格飘忽。 + +后来写了详细的标注规范 + 做了标注员对齐,还加了 **inter-annotator agreement** 检查,一致性低的样本打回重标。 + +**3. 数据分布不均衡** + +某些高频场景数据多,长尾场景几乎没有,模型对长尾问题表现很差。 + +解决方式是主动补采样,但长尾场景本身数据难获取,是个持续的问题。 + +**4. 数据和模型的 prompt 格式对齐** + +前面提到过,base model 用的 chat template 和我们构建数据时的格式对不上,这个坑花了将近一周才定位到根因。 + +**时间** + +整个数据集从零到可用大概花了 **6~8 周**: + +- 前两周:定规范、搭标注流程、种子数据整理 +- 中间三周:批量生成 + 人工审核迭代 +- 后两周:发现质量问题、清洗、补数据 + +说实话比预期长了不少,最初估的是一个月。数据工程这块往往是被低估最严重的部分,真正训练模型的时间反而短。 + +## 6你之前的实习经历偏后端工程,你未来的职业规划更倾向于纯后端开发,还是希望从事与AI/大模型结合的工作? +明确倾向于 **AI 工程化方向**,但不是放弃后端,而是以后端为底座做 AI 落地。 + +**我的判断** + +纯后端的技术边界越来越清晰,大多数问题都有成熟解法。但 AI 工程化还处于早期——推理优化、Agent 可靠性、RAG 效果、成本控制,这些问题在工业界还没有标准答案,对我来说更有挑战性。 + +**为什么不转算法** + +我对模型训练、论文创新没有特别强的热情,更喜欢解决系统性问题——怎么把模型能力稳定高效地服务化,怎么在延迟和成本之间找平衡,怎么让 Agent 在复杂业务流程里跑得住。这些本质上还是工程问题,后端背景反而是优势。 + +**短期目标** + +在推理服务和 Agent 框架这层深挖,具体想做的方向: + +- 推理侧:调度优化、KV Cache 管理、投机采样这些 +- 应用侧:多步 Agent 的可靠性、工具调用的容错 + +**长期** + +希望能参与到 AI 基础设施的建设,而不只是在应用层调 API。字节在这块的投入规模,是我投这个岗位的核心原因之一。 + +后端是我的根,AI 工程化是我想走的路,两者对我来说不矛盾。 + +## 7详细讲一下Golang中Channel的概念和作用,它是否是并发安全的? +**Channel 是什么** + +Go 并发通信的核心原语,本质是一个**带锁的环形队列**,用于 goroutine 之间传递数据。 + +> "Don't communicate by sharing memory, share memory by communicating" + +**基本用法** + +```go +ch := make(chan int, 10) // 有缓冲 +ch := make(chan int) // 无缓冲,同步阻塞 + +ch <- 1 // 发送 +v := <-ch // 接收 +close(ch) // 关闭 +``` + +**有缓冲 vs 无缓冲** + +- 无缓冲:发送方阻塞直到接收方ready,强同步语义 +- 有缓冲:队列未满可以直接写入,解耦生产消费速度 + +**底层结构** + +```go +type hchan struct { + buf unsafe.Pointer // 环形队列 + sendx uint // 发送索引 + recvx uint // 接收索引 + sendq waitq // 阻塞的发送者队列 + recvq waitq // 阻塞的接收者队列 + lock mutex // 互斥锁 +} +``` + +**是否并发安全** + +**是的**,channel 内部有 mutex 保护,多个 goroutine 同时读写是安全的。 + +但有几个坑: + +**向已关闭的 channel 发送会 panic** + +```go +close(ch) +ch <- 1 // panic: send on closed channel +``` + +**重复关闭会 panic** + +```go +close(ch) +close(ch) // panic +``` + +**读已关闭的 channel 不会 panic** + +```go +v, ok := <-ch // ok=false 表示已关闭且空 +``` + +**常见使用模式** + +```go +// 退出信号 +done := make(chan struct{}) +go func() { + <-done // 等待退出 +}() +close(done) // 广播退出 + +// pipeline +func gen(nums ...int) <-chan int { ... } +func sq(in <-chan int) <-chan int { ... } +``` + +**踩过的坑** + +- goroutine 泄漏:channel 没人消费,发送方永久阻塞 +- 关闭时机不明确,多个发送方时用 `sync.WaitGroup` + 单独的 closer goroutine 管理 +- 有缓冲 channel 设置过大当消息队列用,丢失了背压能力,服务过载时内存暴涨 + +实际上我更倾向于:**简单同步用 channel,复杂共享状态用 mutex**,不要为了"Go风格"强行用 channel。 + +## Channel和传统的锁(Mutex)在实现并发控制时有什么区别?各自的适用场景是什么? +**本质区别** + +- Mutex:保护共享数据,同一时刻只有一个 goroutine 访问 +- Channel:传递数据所有权,通过通信来同步 + +**实现机制对比** + +```go +// Mutex:保护共享状态 +type Counter struct { + mu sync.Mutex + count int +} +func (c *Counter) Inc() { + c.mu.Lock() + defer c.mu.Unlock() + c.count++ +} + +// Channel:转移数据所有权 +ch := make(chan int, 1) +ch <- 0 // 初始值 +val := <-ch +val++ +ch <- val +``` + +**性能对比** + +Mutex 比 Channel 快,Channel 底层也有锁,还有额外的调度开销: + +``` +Mutex: ~20ns +Channel: ~100ns +``` + +高频操作(计数器、缓存)用 Mutex 更合适。 + +**适用场景** + +**用 Mutex:** + +- 保护简单共享状态(计数、map、缓存) +- 临界区很小、持锁时间短 +- 性能敏感路径 + +**用 Channel:** + +- goroutine 间传递任务、结果 +- 生产者消费者模型 +- 超时控制、退出信号 +- pipeline 数据流 + +```go +// Channel 天然适合的场景 +select { +case result := <-ch: + // 处理结果 +case <-time.After(3 * time.Second): + // 超时处理 +case <-ctx.Done(): + // 取消 +} +``` + +**我的实际判断** + +不要教条,看场景: + +- 并发安全的 map → `sync.Map` 或 Mutex,不要用 channel 模拟 +- worker pool → channel 分发任务,Mutex 保护统计数据 +- 退出广播 → `close(done)` channel,比 Mutex + bool 优雅得多 + +踩过的坑:早期团队里有人用 channel 实现了一个"并发安全 map",用单个 goroutine 串行处理所有读写请求,正确但性能很差——这种场景直接用 `sync.RWMutex` 即可,读多写少时性能差距很大。 + +## 讲一下GMP模型 +上面已经聊过 GMP 了,简单补充几个没展开的点: + +**调度循环细节** + +M 执行 G 的完整循环: + +``` +schedule() → execute(g) → goexit → schedule() +``` + +G 执行完不是销毁,是放回 P 的本地队列或全局队列复用。 + +**全局队列 vs 本地队列** + +``` +本地队列:每个 P 最多 256 个 G +全局队列:溢出时放这里,或者 go 关键字创建时负载均衡 +``` + +M 优先从本地队列取,减少锁竞争。 + +**几个容易忽略的点** + +`runtime.Gosched()`:主动让出 P,当前 G 放回队列,让其他 G 运行——CPU 密集型任务里偶尔需要手动插入。 + +`LockOSThread()`:绑定 G 到固定 M,CGO 调用或需要线程局部存储时用,用完记得 `UnlockOSThread`。 + +**P 数量的影响** + +```bash +GOMAXPROCS=1 # 单线程,无并行 +GOMAXPROCS=N # N个P并行,一般等于CPU核数 +``` + +容器环境要用 `automaxprocs`,否则读到宿主机核数,P 过多反而增加调度开销。 + +之前说过的 work stealing、handoff、抢占调度这些就不重复了,有哪个点想深入聊? + +## 当P的本地队列为空或者不为空时,它会怎么去调度G(协程)?TTRedis支持哪些数据结构 +两个问题一起答。 + +**P 的调度策略** + +**本地队列不为空** 直接从队头取 G 执行,最常见路径,无锁操作。 + +**本地队列为空,按优先级依次尝试:** + +``` +1. 每61次调度强制从全局队列取一个G(避免全局队列饥饿) +2. 从本地队列取 +3. 本地队列空 → 从全局队列取一批 +4. 全局队列也空 → netpoller 检查就绪的网络G +5. 都没有 → work stealing,随机偷其他P队列的一半 +6. 偷不到 → M 休眠,P 放回空闲池 +``` + +第1步的61次是源码里写死的魔法数字,保证全局队列不会被饿死。 + +## Redis 支持的数据结构 + +| 类型 | 底层实现 | 典型场景 | +| ----------- | -------------------------- | --------- | +| String | SDS | 缓存、计数器 | +| List | quicklist | 消息队列、最新列表 | +| Hash | ziplist/hashtable | 对象存储 | +| Set | intset/hashtable | 去重、交并差 | +| ZSet | ziplist/skiplist+hashtable | 排行榜、延迟队列 | +| Stream | rax树+listpack | 消息流 | +| HyperLogLog | 概率结构 | UV统计 | +| Bitmap | 位数组 | 签到、布隆过滤器 | +| Geo | ZSet封装 | 地理位置 | + +**底层实现会自动切换**:以 ZSet 为例,元素少且值短时用 ziplist 节省内存,超过阈值(默认128个元素或64字节)自动转 skiplist。 + +跳表这块值得多说一句——Redis 选跳表而不是红黑树,原因是跳表实现简单、范围查询友好、并发改造容易,插入删除平均 O(logN) 和红黑树一样但常数更小。 + +## 为什么Redis的速度这么快 +**几个核心原因** +**1. 纯内存操作** 所有数据在内存,读写不涉及磁盘 IO,内存访问纳秒级 vs 磁盘毫秒级,量级差距。 +**2. 单线程处理命令** +``` +所有命令串行执行 → 无锁竞争 → 无上下文切换开销 +``` +单线程不是瓶颈,因为瓶颈在网络 IO 而不是 CPU。 +Redis 6.0+ 引入多线程处理网络 IO,但命令执行仍是单线程。 +**3. IO 多路复用** +``` +epoll 监听所有客户端连接 +→ 有事件才处理,不阻塞等待 +→ 单线程处理数千并发连接 +``` +这是单线程能扛高并发的关键。 +**4. 高效数据结构** +- SDS 比 C 字符串获取长度 O(1) +- ziplist 内存紧凑,CPU cache 友好 +- 跳表范围查询高效 +- 数据结构随数据量自动切换,小数据用紧凑结构 +**5. 非阻塞持久化** +``` +RDB:fork子进程做快照,主进程不阻塞 +AOF:异步刷盘,不阻塞主流程 +``` +**6. 简单的网络协议 RESP** 文本协议,解析极简,序列化开销小。 +**一句话总结** +内存 + 单线程无锁 + epoll + 精心设计的数据结构,几个优化叠加的结果,缺一不可。 +踩过的坑:以为单线程就不会有性能问题,结果一个 `KEYS *` 扫全库直接把 Redis 卡死几秒——单线程意味着一个慢命令会阻塞所有请求,生产环境禁用 KEYS,用 SCAN 替代。 + +## 如何实现一个类似淘宝搜索框的实时商品名称模糊搜索功能? +**整体方案** + +实时搜索框本质是:**低延迟 + 前缀/模糊匹配 + 高并发读**,核心用 ES 做搜索引擎。 + +**架构分层** + +``` +用户输入 → 前端防抖(200ms) → 网关限流 +→ 搜索服务 → ES集群 + ↑ + 商品数据同步 +``` + +**ES 索引设计** + +```json +{ + "mappings": { + "properties": { + "title": { + "type": "text", + "analyzer": "ik_max_word", + "search_analyzer": "ik_smart" + }, + "title_suggest": { + "type": "completion" + }, + "title_pinyin": { + "type": "text", + "analyzer": "pinyin" + } + } + } +} +``` + +- `ik` 分词器处理中文 +- `completion` 类型专门做前缀补全,底层是 FST,速度极快 +- 拼音字段支持拼音搜索 + +**前缀补全 vs 模糊搜索** + +``` +用户输入"耐克" → completion suggest → 毫秒级返回候选词 +用户输入完整词 → match/multi_match → 全文检索 +``` + +搜索框补全用 completion,真正搜索结果用 match。 + +**数据同步** + +``` +MySQL商品表 → binlog → Canal → MQ → 消费写ES +``` + +增量同步,延迟秒级可接受。 + +**关键优化点** + +- 前端防抖 200ms,减少请求量 +- 热门搜索词 Redis 缓存,不打 ES +- ES 搜索结果 Redis 短暂缓存(TTL 5s) +- 搜索词权重:销量、点击率影响排序 + +**踩过的坑** + +ES 的 `wildcard` 查询(`*耐克*`)性能极差,全表扫描,千万级数据直接超时——要用分词 + match,不要用 wildcard 做模糊搜索。 + +## 实时输入联想与输入完成后点击搜索在技术实现上有什么本质区别? +**本质区别:目标不同** + +- 联想补全:猜你想输入什么,返回**候选词列表** +- 点击搜索:找匹配的**商品结果集** + +**联想补全** + +``` +目标:快、准、少 +返回:5~10个候选词 +延迟要求:< 50ms +``` + +技术实现: + +- ES `completion` 字段,FST 数据结构,前缀匹配极快 +- Redis `ZRANGEBYLEX` 也能做前缀补全 +- 只匹配词,不做复杂相关性计算 + +```json +GET /products/_search +{ + "suggest": { + "title_suggest": { + "prefix": "耐克", + "completion": { "field": "title_suggest", "size": 8 } + } + } +} +``` + +**点击搜索** + +``` +目标:全、准、排序好 +返回:分页商品列表(标题、图片、价格、销量) +延迟要求:< 200ms +``` + +技术实现: + +- ES `multi_match` 多字段全文检索 +- 相关性评分 + 业务权重(销量、转化率)综合排序 +- 需要过滤(价格区间、品类、库存) +- 分页、高亮关键词 + +```json +{ + "query": { + "function_score": { + "query": { "multi_match": { "query": "耐克运动鞋", "fields": ["title^3", "brand", "category"] }}, + "functions": [ + { "field_value_factor": { "field": "sales_count", "modifier": "log1p" }} + ] + } + } +} +``` + +**两者核心差异对比** + +||联想补全|点击搜索| +|---|---|---| +|数据结构|FST/Trie|倒排索引| +|匹配方式|前缀匹配|分词全文检索| +|排序依据|搜索热度|相关性+业务权重| +|返回内容|词条|完整商品文档| +|缓存策略|激进缓存|短暂缓存或不缓存| + +**联想的特殊处理** + +个性化联想:结合用户历史搜索,Redis 存用户近期搜索词,和热门词合并排序。 + +踩过的坑:联想和搜索共用同一个 ES 查询,联想被复杂的 function_score 拖慢到 200ms+,用户体验很差——后来联想单独走轻量接口,甚至部分热词直接走 Redis,搜索走完整 ES 查询,彻底分开。 + +## 实时搜索通常使用什么网络协议(如WebSocket)?你了解或有使用过吗?讲一下 +**实时搜索用 WebSocket 其实是误区** + +搜索框联想本质是**请求-响应**模型,不需要服务端主动推送,用普通 HTTP 就够了,加上前端防抖控制请求频率。 + +WebSocket 适合的是**服务端需要主动推送**的场景。 + +**HTTP vs WebSocket 选择** + +||HTTP|WebSocket| +|---|---|---| +|连接模型|短连接/长连接|持久双向连接| +|发起方|客户端|双向均可| +|适合搜索联想|✅|❌ 杀鸡用牛刀| +|适合实时推送|❌|✅| + +**WebSocket 原理** + +基于 HTTP 握手升级: + +``` +GET /ws HTTP/1.1 +Upgrade: websocket +Connection: Upgrade +Sec-WebSocket-Key: xxx + +← 101 Switching Protocols +``` + +握手后连接升级为全双工 TCP 连接,后续帧格式是二进制的 WebSocket 帧,不再是 HTTP。 + +**Go 实现关键片段** + +```go +// 服务端用 gorilla/websocket +upgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +func wsHandler(w http.ResponseWriter, r *http.Request) { + conn, _ := upgrader.Upgrade(w, r, nil) + defer conn.Close() + + for { + _, msg, err := conn.ReadMessage() + if err != nil { break } + conn.WriteMessage(websocket.TextMessage, msg) + } +} +``` + +**我实际用过的场景** + +- **消息推送**:用户收到新订单、新消息实时通知 +- **大模型流式输出**:SSE 和 WebSocket 都用过,LLM 场景更多用 SSE + +**SSE vs WebSocket** + +LLM 输出场景我更推荐 SSE: + +``` +服务端单向推流,实现简单 +自动重连,HTTP 协议天然穿透代理 +不需要双向通信就别用 WebSocket +``` + +```go +// SSE 实现极简 +w.Header().Set("Content-Type", "text/event-stream") +fmt.Fprintf(w, "data: %s\n\n", chunk) +flusher.Flush() +``` + +踩过的坑:WebSocket 连接数暴涨时内存压力大,每个连接在 Go 里至少两个 goroutine(读/写),连接数到几万时 goroutine 数量可观——要做连接数限制和心跳超时清理,否则僵尸连接会把内存撑爆。 + +## 请详细说明微信扫码登录的完整流程和背后发生的原理 +**核心思路** + +手机端已登录态,PC 端借助二维码完成身份传递,本质是**已认证设备为新设备授权**。 + +**完整流程** + +**第一步:PC 端生成二维码** + +``` +PC打开登录页 → 请求服务端生成 ticket +服务端:ticket = UUID,存 Redis,TTL 120s,状态=待扫描 +返回二维码(内容是携带ticket的URL) +PC 端轮询或 SSE 监听 ticket 状态 +``` + +**第二步:手机扫码** + +``` +微信扫描二维码 → 解析出 ticket URL +→ 请求服务端:上报 ticket + 手机登录态 token +服务端:验证手机 token 有效 +→ ticket 状态改为"已扫描,待确认" +→ PC 端轮询拿到状态变更,展示"已扫描,请在手机确认" +``` + +**第三步:手机确认** + +``` +用户点击"确认登录" +→ 手机上报 ticket + 用户授权 +服务端: + 生成 PC 端 session token + ticket 状态改为"已确认" + ticket 绑定 PC session +``` + +**第四步:PC 端完成登录** + +``` +PC 轮询发现状态=已确认 +→ 用 ticket 换取 PC session token +→ 登录成功,ticket 作废 +``` + +**状态机** + +``` +待扫描 → 已扫描 → 已确认 → 已使用/已过期 +``` + +**PC 端如何感知状态变化** + +三种方案: + +**短轮询**:每2秒请求一次,简单但浪费 + +```go +// 前端 +setInterval(() => fetch('/check?ticket=xxx'), 2000) +``` + +**长轮询**:服务端 hold 住请求直到状态变化或超时,减少无效请求 + +**SSE**:服务端主动推,更优雅 + +```go +w.Header().Set("Content-Type", "text/event-stream") +// 状态变化时推送 +fmt.Fprintf(w, "data: {\"status\":\"confirmed\"}\n\n") +flusher.Flush() +``` + +微信实际用的是长轮询,不是 WebSocket。 + +**安全关键点** + +- ticket 一次性,用完即废 +- ticket 有效期短(120s),过期重新生成 +- 手机确认步骤防止二维码被恶意截图后登录 +- HTTPS 防止 ticket 被中间人截获 +- ticket 和 PC session 绑定,防止 ticket 被其他设备使用 + +**Redis 数据结构** + +``` +key: qr:ticket:{uuid} +value: {status, uid, pc_session, create_time} +TTL: 120s +``` + +踩过的坑:早期实现忘记让 ticket 在换取 session 后立即失效,同一个 ticket 可以换多次 session——要确保换取操作原子性,用 Redis lua 脚本做 check-and-delete。 + +## 在微服务架构中,服务发现和负载均衡是如何实现的? +**服务发现** + +解决的问题:服务实例动态增减,调用方如何知道当前有哪些可用节点。 + +**两种模式** + +**客户端发现**:调用方自己查注册中心,自己做负载均衡 + +``` +服务A → 查 Consul/Etcd → 拿到实例列表 → 自己选一个 +``` + +**服务端发现**:调用方只请求 LB,LB 去查注册中心 + +``` +服务A → Nginx/Envoy → 注册中心 → 转发 +``` + +我们用的是客户端发现,配合 gRPC + Etcd。 + +**注册与发现流程** + +``` +服务启动 → 注册到 Etcd(key=/services/order/实例ID, value=IP:Port) + → 设置 TTL,定期续约心跳 +服务下线 → 主动注销 or TTL 到期自动删除 + +调用方 → Watch /services/order 前缀 + → 实例变化时实时更新本地缓存 +``` + +**负载均衡策略** + +```go +// 常见策略 +轮询(Round Robin) // 均匀分配,简单 +加权轮询 // 按实例配置权重 +最少连接 // 选当前连接数最少的 +一致性哈希 // 同一请求打同一节点,适合有状态服务 +随机 // 简单场景够用 +``` + +gRPC 内置了轮询,自定义策略实现 `balancer.Builder` 接口即可。 + +**健康检查** + +光靠心跳不够,实例可能假活(进程在但服务异常): + +``` +主动探测:注册中心定期 HTTP/TCP 探活 +被动探测:调用失败率超阈值,熔断器介入 +``` + +我们结合两者:Etcd 心跳 + 客户端调用失败自动摘除节点,摘除后定期重试恢复。 + +**结合熔断限流** + +``` +调用链:服务A → 本地实例列表 → 熔断器判断 → 负载均衡选节点 → 发请求 +``` + +熔断器(我们用 go-resilience)在负载均衡之前,避免把请求打到已知故障节点。 + +**踩过的坑** + +Etcd 续约心跳间隔设太长,实例宕机后 TTL 还没到期,流量继续打过去报错——心跳间隔和 TTL 要合理配比,我们用 TTL=15s,心跳=5s,宕机最多15秒才被摘除,业务侧要有重试兜底。 + +## 服务注册中心(如Nacos,Consul)是如何工作的?服务实例如何注册和保活(如通过心跳机制)? +**注册中心核心职责** + +服务实例的"电话簿"——记录谁在线、在哪里、健不健康。 + +**注册流程** + +``` +服务启动 → 发送注册请求(IP、Port、服务名、元数据) +→ 注册中心写入存储 +→ 通知订阅该服务的调用方 +``` + +**保活机制** + +**心跳模式(Consul / Nacos默认)** + +``` +实例定期发心跳 → 注册中心更新最后存活时间 +超过 TTL 未收到心跳 → 标记为不健康 → 从可用列表摘除 +``` + +```go +// 客户端心跳伪代码 +ticker := time.NewTicker(5 * time.Second) +for range ticker.C { + err := registry.Heartbeat(instanceID) + if err != nil { + // 重新注册 + registry.Register(instance) + } +} +``` + +**租约模式(Etcd)** + +``` +注册时创建 Lease(TTL=15s) +key 绑定到 Lease +客户端定期 KeepAlive 续约 +Lease 过期 → 绑定的 key 自动删除 +``` + +Etcd 租约比心跳更原子,key 和存活状态强绑定。 + +**Nacos vs Consul 对比** + +||Nacos|Consul| +|---|---|---| +|一致性协议|AP(临时实例)/ CP(持久实例)|Raft(CP)| +|健康检查|客户端心跳|客户端心跳 + 服务端主动探测| +|配置中心|内置|需要额外方案| +|生态|Java/Spring Cloud 友好|云原生/多语言友好| + +Nacos 临时实例默认 AP,注册中心宕机部分节点时仍可用但可能读到旧数据。Consul 强 CP,宕机超半数节点则不可用但数据一致。 + +**服务发现推送机制** + +调用方如何感知实例变化: + +**推模式**:注册中心主动推变更,实时性好 + +- Nacos:长连接推送 +- Etcd:Watch 机制 + +**拉模式**:调用方定期拉取,简单但有延迟 + +- Consul:定期轮询或 blocking query + +**脑裂问题** + +网络分区时注册中心节点互相认为对方挂了: + +- Consul Raft 要求多数派,少数派节点拒绝服务 +- Nacos AP 模式允许分区继续服务,合并时以最新版本为准 + +**踩过的坑** + +心跳超时设置不合理,网络抖动导致实例被误摘除,流量瞬间集中到剩余节点打挂——后来加了摘除保护:单次摘除比例超过 20% 时触发告警不自动摘除,人工介入。Nacos 有内置的自我保护模式处理这个问题。 + +## 讲一下Agent中的“长短期记忆” +**为什么需要长短期记忆** + +LLM 本身无状态,每次调用独立。Agent 要完成多步任务、跨会话记住用户,必须自己管理记忆。 + +**短期记忆** + +本质是 **context window 内的对话历史**: + +```python +messages = [ + {"role": "system", "content": "你是xxx助手"}, + {"role": "user", "content": "我叫张三"}, + {"role": "assistant", "content": "好的张三"}, + {"role": "user", "content": "我叫什么"}, # 能回答,在窗口内 +] +``` + +问题是 context 有长度限制,对话长了要做截断或压缩: + +- **滑动窗口**:只保留最近 N 轮 +- **摘要压缩**:用 LLM 把早期对话压缩成摘要塞回 context +- **重要信息提取**:只保留关键事实,丢弃闲聊 + +**长期记忆** + +跨会话持久化,存在外部存储: + +``` +用户说"我对Go比较熟" +→ 提取实体:用户偏好=Go语言 +→ 写入长期记忆存储 +→ 下次会话召回注入 context +``` + +存储方案: + +- 结构化事实 → 直接存 DB(用户画像、偏好) +- 非结构化内容 → 向量数据库(历史对话、文档) + +**召回机制** + +长期记忆不能全量塞进 context,要按需检索: + +``` +当前输入 → embedding → 向量检索相关记忆 +→ 排序筛选 → 注入 context +``` + +```python +# 伪代码 +relevant_memories = vector_store.search( + query=current_input, + top_k=5, + filter={"user_id": uid} +) +context = system_prompt + relevant_memories + recent_history +``` + +**两者结合的实践** + +``` +长期记忆:用户基本信息、偏好、历史结论(持久化) +短期记忆:当前任务上下文、最近对话(会话级) +工作记忆:当前步骤的中间结果(单次推理内) +``` + +**踩过的坑** + +- 记忆召回不准:embedding 模型和业务语义不匹配,检索出不相关记忆反而干扰模型——换了领域适配的 embedding 模型改善明显 +- 记忆污染:用户说的错误信息被当成事实存入长期记忆,后续一直用错——写入前要做置信度过滤,不确定的信息不持久化 +- context 拼装顺序影响效果:长期记忆放太前容易被遗忘,一般放在 system prompt 紧接着的位置效果最好 + +## 什么样的信息应该放在长期记忆,什么样的信息放在短期记忆? +**核心判断标准** + +> 这条信息在下一次会话还有用吗?跨任务还有价值吗? + +有 → 长期记忆,没有 → 短期记忆。 + +**短期记忆放什么** + +当前任务的上下文,会话结束就没价值的: + +- 当前对话的问答历史 +- 本次任务的中间推理步骤 +- 临时的用户指令("这次回答简短一点") +- 正在处理的文档内容 + +**长期记忆放什么** + +跨会话仍然有价值的稳定信息: + +``` +用户维度: +- 基本信息(姓名、职业、语言偏好) +- 技术栈偏好("用户熟悉Go,不熟悉Rust") +- 沟通风格("喜欢简洁回答,不要废话") + +任务维度: +- 已完成任务的结论和决策 +- 用户明确表达过的长期目标 +- 项目背景、团队约定 +``` + +**判断边界的几个原则** + +**时效性**: + +``` +"今天天气怎么样" → 短期,明天就过期 +"我不喜欢冗长的回答" → 长期,每次都适用 +``` + +**普适性**: + +``` +"这个函数帮我用递归实现" → 短期,仅限本次 +"用户偏好迭代而非递归" → 长期,影响后续所有代码建议 +``` + +**确定性**: + +``` +用户猜测性的话 → 不存长期("我可能会用Python") +用户明确表达的 → 存长期("我们团队统一用Python") +``` + +**实践中的模糊地带** + +有些信息介于两者之间,我们的处理方式: + +- 设置**衰减机制**:长期记忆加时间戳,超过30天未被引用的降权或清理 +- **用户确认**:不确定是否值得长期存储时,直接问用户 +- **分级存储**:高置信长期记忆 vs 低置信待验证记忆,召回时区分使用 + +踩过的坑:早期什么都往长期记忆塞,导致召回时噪音太多,反而干扰模型判断。后来加了写入门槛——只有明确的事实性陈述才写入,推测性内容一律不存。 + +## 当对话轮数很多,上下文窗口不足时,有哪些处理策略?(如截断、压缩) +**核心问题** + +context 有限,但有效信息可能分散在很长的对话历史里,简单截断会丢关键信息。 + +**几种策略** + +**1. 滑动窗口截断** + +最简单,只保留最近 N 轮: + +```python +messages = messages[-20:] # 只保留最近20轮 +``` + +优点:实现零成本 缺点:早期重要信息直接丢失,用户说过的偏好、背景全忘了 + +**2. 摘要压缩** + +定期把早期对话压缩成摘要: + +```python +# 超过阈值时触发 +if token_count > threshold: + summary = llm.summarize(old_messages) + messages = [{"role": "system", "content": f"历史摘要:{summary}"}] + + recent_messages +``` + +保留语义,丢弃细节。问题是摘要本身有信息损失,且摘要质量依赖模型能力。 + +**3. 重要信息提取** + +不做摘要,主动提取关键事实存入长期记忆: + +``` +对话中识别出 → 用户偏好、决策结论、任务背景 +→ 结构化存储 +→ 每次会话开始时召回注入 system prompt +``` + +比摘要更精准,但需要额外的提取步骤。 + +**4. 向量检索相关历史** + +不是把所有历史塞进去,而是按当前问题检索最相关的片段: + +```python +relevant_history = vector_store.search( + query=current_input, + top_k=5 +) +``` + +适合历史很长但当前问题只需要部分上下文的场景。 + +**5. 分层管理** + +几种策略组合使用: + +``` +最近5轮 → 完整保留(短期记忆) +5~20轮 → 摘要压缩 +20轮以上 → 关键事实提取到长期记忆 +超长文档 → 向量化,按需检索 +``` + +**实践中的选择** + +- 简单问答机器人 → 滑动窗口够用 +- 需要记住用户偏好 → 关键信息提取 + 长期记忆 +- 长文档分析任务 → 向量检索 +- 复杂多步 Agent → 分层管理 + +**踩过的坑** + +摘要压缩时机不对——每轮都触发摘要开销太大,攒够一定轮数再压缩更合理。另外摘要模型用的太小,摘要质量差,关键细节丢失导致后续回答出错,后来摘要也用主力模型来做。还有就是压缩后 token 确实省了,但模型推理质量明显下降——context 压缩是有损的,要在成本和效果之间找平衡点。 + +## 如果要进行记忆压缩,通常有哪些方法? +上面刚聊过一部分,补充展开更细的维度。 + +**压缩方法分类** + +**1. 抽取式压缩(Extractive)** + +不改写,直接从原文挑重要句子保留: + +``` +原对话10句 → 选出3句关键句 → 拼接作为压缩结果 +``` + +优点:无信息失真,原话保留 缺点:压缩率有限,上下文可能不连贯 + +实现方式:TF-IDF 打分、或让模型给每句话打重要性分数。 + +**2. 生成式压缩(Abstractive)** + +让 LLM 重新表述,生成更紧凑的摘要: + +```python +prompt = """ +对话历史:{history} +请提取以下信息: +1. 用户的核心目标 +2. 已确认的关键事实 +3. 未解决的问题 +输出JSON格式 +""" +``` + +压缩率高,但有幻觉风险——模型可能改变原意。 + +**3. 结构化提取** + +不做自然语言摘要,直接抽取实体和关系: + +```json +{ + "user_profile": {"name": "张三", "role": "后端工程师"}, + "decisions": ["使用Go语言", "数据库选PostgreSQL"], + "pending": ["部署方案待确定"], + "preferences": ["回答要简洁"] +} +``` + +召回时直接用结构化数据拼 prompt,比自然语言摘要更可控。 + +**4. 层级压缩** + +对长对话做多级摘要: + +``` +每5轮 → 小摘要 +每5个小摘要 → 中摘要 +中摘要积累 → 大摘要 +``` + +类似文章的段落→章节→全书摘要,保留不同粒度的信息。 + +**5. 遗忘机制** + +模拟人类记忆衰减,给每条记忆加权重: + +```python +importance = base_score * decay_factor(time) * access_frequency +# 低于阈值的记忆降权或删除 +``` + +最近访问、高频引用的记忆权重高,冷记忆自然淘汰。 + +**实践中怎么选** + +``` +对话轮数不多(<50轮)→ 生成式摘要够用 +需要精确召回事实 → 结构化提取 +超长会话(>100轮) → 层级压缩 + 遗忘机制 +用户偏好类信息 → 结构化提取写长期记忆,不做摘要 +``` + +**踩过的坑** + +生成式摘要最大的问题是**摘要本身消耗 token**,压缩一次可能花掉几百 token,频繁压缩反而不划算——要设合理触发阈值,不能每轮都压缩。另外结构化提取依赖 prompt 质量,字段定义不清晰时模型乱填,加了 JSON Schema 校验 + 失败重试才稳定下来。 + +## 了解过Agent的设计范式吗?有哪些? +**主流 Agent 设计范式** + +**1. ReAct(Reasoning + Acting)** + +最经典,交替进行推理和行动: + +``` +Thought: 我需要查用户订单 +Action: query_db(user_id=123) +Observation: 返回订单列表 +Thought: 需要进一步过滤 +Action: filter(status=pending) +Observation: 3条待处理订单 +Answer: 用户有3条待处理订单 +``` + +优点:透明可解释,每步有据可查 缺点:步骤多时延迟高,中间某步出错容易雪崩 + +**2. Plan and Execute** + +先规划所有步骤,再逐步执行: + +``` +Planning阶段: + 1. 查用户信息 + 2. 查订单 + 3. 汇总报告 + +Execute阶段:按计划逐步执行 +``` + +适合任务明确、步骤固定的场景,比 ReAct 更稳定,但计划阶段对复杂任务容易规划不准。 + +**3. Reflection** + +执行后自我反思,发现问题重试: + +``` +执行 → 输出结果 → 自我评估 +→ 发现问题 → 修正重试 +``` + +常和其他范式组合,单独用很少。 + +**4. Multi-Agent** + +多个专职 Agent 协作: + +``` +Orchestrator(主控) + ├── ResearchAgent(信息收集) + ├── CodeAgent(代码生成) + └── ReviewAgent(结果审核) +``` + +两种拓扑: + +- **Pipeline**:串行,上一个输出是下一个输入 +- **协作网络**:Agent 间互相调用,更灵活但更难调试 + +**5. Memory-Augmented Agent** + +上面聊过的长短期记忆体系,本质是增强 Agent 的状态管理能力,不是独立范式,常作为其他范式的补充。 + +**实践中怎么选** + +``` +单步任务、工具调用明确 → ReAct 够用 +复杂多步、任务结构固定 → Plan and Execute +需要高质量输出 → 加 Reflection +任务需要并行、专业分工 → Multi-Agent +``` + +**我们实际用的** + +做内部知识问答 Agent 用的是 ReAct + Memory: + +- ReAct 控制推理和工具调用 +- 每次会话结束提取关键信息写长期记忆 +- 工具集包括:RAG检索、SQL查询、API调用 + +踩过的坑:ReAct 链路长时模型容易在中间步骤"跑偏",比如本来要查订单,推理几步后开始查无关内容——加了步骤数上限 + 每步结果校验,超限直接截断返回已有结论。Multi-Agent 调试很痛苦,某个子 Agent 输出格式不对会静默传递错误,最后主控拿到的是垃圾数据,排查链路很长。 + +## 你设计的Agent是怎么实现ReAct模式的?详细讲讲 +**整体架构** + +``` +用户输入 → Orchestrator → ReAct循环 → 工具执行 → 最终输出 +``` + +**ReAct 核心循环** + +```go +func (a *Agent) Run(ctx context.Context, input string) (string, error) { + messages := a.buildInitialMessages(input) + + for step := 0; step < a.maxSteps; step++ { + resp, err := a.llm.Chat(ctx, messages) + + // 解析模型输出 + action := parseAction(resp) + + // 纯文本回答,结束循环 + if action.Type == "finish" { + return action.Answer, nil + } + + // 执行工具 + observation, err := a.executeTool(ctx, action) + + // 把工具结果追加回消息 + messages = append(messages, + assistantMsg(resp), + toolResultMsg(observation), + ) + } + return "超过最大步骤", nil +} +``` + +**Prompt 设计是核心** + +``` +System: +你是一个助手,可以使用以下工具: +- search_knowledge(query): 搜索知识库 +- query_order(user_id): 查询订单 +- calculate(expr): 计算 + +输出格式严格按照: +Thought: <推理过程> +Action: <工具名> +Action Input: + +或者任务完成时: +Thought: <推理> +Final Answer: <答案> +``` + +格式约束是稳定性的关键,模型乱输出格式会导致解析失败。 + +**工具注册与执行** + +```go +type Tool interface { + Name() string + Description() string + Execute(ctx context.Context, input string) (string, error) +} + +// 注册 +agent.RegisterTool(&SearchTool{vectorStore: vs}) +agent.RegisterTool(&OrderQueryTool{db: db}) + +// 执行时按名字路由 +func (a *Agent) executeTool(ctx context.Context, action Action) (string, error) { + tool, ok := a.tools[action.ToolName] + if !ok { + return "工具不存在", nil // 返回给模型,让它修正 + } + return tool.Execute(ctx, action.Input) +} +``` + +**解析模型输出** + +```go +func parseAction(output string) Action { + // 正则提取 Action 和 Action Input + if strings.Contains(output, "Final Answer:") { + return Action{Type: "finish", Answer: extractFinalAnswer(output)} + } + toolName := extractAction(output) + toolInput := extractActionInput(output) + return Action{Type: "tool", ToolName: toolName, Input: toolInput} +} +``` + +解析这块要做容错,模型输出格式偶尔不规范,加了模糊匹配兜底。 + +**稳定性保障** + +几个实际加的机制: + +**步骤上限 + 强制终止** + +```go +if step >= maxSteps { + // 把已有 observation 喂给模型,强制要它总结 + return a.forceFinish(ctx, messages) +} +``` + +**工具执行超时** + +```go +toolCtx, cancel := context.WithTimeout(ctx, 10*time.Second) +defer cancel() +result, err := tool.Execute(toolCtx, input) +``` + +**循环检测** + +```go +// 连续两步调用相同工具+相同参数,判定为循环 +if isDuplicateAction(currentAction, lastAction) { + messages = append(messages, systemMsg("你已经执行过这个操作,请换个思路")) +} +``` + +**踩过的坑** + +模型输出 JSON 参数时经常带单引号或多余字段,硬解析直接崩——改成先尝试严格解析,失败了让模型重新输出一次,最多重试2次。 + +工具返回内容太长时直接塞回 context 会撑爆窗口,加了工具输出截断:超过 500 token 的结果先摘要再返回给模型。 + +最难排查的是**模型推理跑偏**:用户问A,模型推理几步后开始解决B,中间没有明显错误但最终答非所问。后来在每步加了一个轻量校验——把原始问题和当前步骤一起喂给模型问"是否还在解决原问题",偏离时强制拉回。这个额外开销不小,只在高价值场景开启。 + + +## 手撕:实现一个并发任务处理器:给定一个包含100个任务ID的列表,要求控制最大并发数为3,模拟并 +直接给代码,这是个经典的 worker pool 模式: + +**方案一:channel 做信号量** + +```go +package main + +import ( + "fmt" + "sync" + "time" +) + +func main() { + taskIDs := make([]int, 100) + for i := range taskIDs { + taskIDs[i] = i + 1 + } + + sem := make(chan struct{}, 3) // 控制并发数 + var wg sync.WaitGroup + + for _, id := range taskIDs { + wg.Add(1) + sem <- struct{}{} // 占一个槽,满了阻塞 + + go func(taskID int) { + defer wg.Done() + defer func() { <-sem }() // 释放槽 + + processTask(taskID) + }(id) + } + + wg.Wait() + fmt.Println("all done") +} + +func processTask(id int) { + fmt.Printf("processing task %d\n", id) + time.Sleep(100 * time.Millisecond) // 模拟外部调用 +} +``` + +**方案二:固定 worker pool(更适合生产)** + +```go +func workerPool(taskIDs []int, concurrency int) { + taskCh := make(chan int, len(taskIDs)) + var wg sync.WaitGroup + + // 启动固定数量 worker + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for id := range taskCh { + processTask(id) + } + }() + } + + // 投递任务 + for _, id := range taskIDs { + taskCh <- id + } + close(taskCh) // 关闭后 worker 自然退出 + + wg.Wait() +} +``` + +**两种方案对比** + +||信号量|Worker Pool| +|---|---|---| +|goroutine 数量|最多100个|固定3个| +|实现复杂度|简单|稍复杂| +|适合场景|任务量不大|长期运行、任务量大| + +**生产环境补充** + +```go +// 加错误收集 +type Result struct { + ID int + Err error +} + +errCh := make(chan Result, len(taskIDs)) + +go func(taskID int) { + defer wg.Done() + defer func() { <-sem }() + + if err := processTask(taskID); err != nil { + errCh <- Result{ID: taskID, Err: err} + } +}(id) +``` + +实际项目里我更倾向 worker pool 方案——goroutine 数量可控,不会因为任务量暴增把内存撑爆。信号量方案简单但本质上还是创建了 N 个 goroutine,任务量大时内存开销不可忽视。 diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节一面 4.15.md b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节一面 4.15.md new file mode 100644 index 0000000..eef0725 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节一面 4.15.md @@ -0,0 +1,320 @@ +又是项目拷打全程,只有几个八股 +## 1.rocket,mq延迟队列怎么设计的 +**RocketMQ 延迟消息的设计** + +RocketMQ 不支持任意时间延迟,只支持**固定18个延迟级别**: + +``` +1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h +``` + +**核心实现** + +发送延迟消息时,Broker 不直接投递到目标 Topic,而是: + +``` +1. 消息写入内置 Topic:SCHEDULE_TOPIC_XXXX + 对应 18 个 Queue,每个 Queue 对应一个延迟级别 +2. 定时任务每 100ms 扫描每个 Queue +3. 到期的消息重新投递到原始 Topic +4. 消费者正常消费原始 Topic +``` + +**为什么不支持任意时间** + +任意时间延迟需要按时间排序,本质是优先队列,大量消息下性能和存储压力都很大。固定级别用固定 Queue 顺序消费,实现简单,性能可控。 + +**如果业务需要任意延迟怎么办** + +几个方案: + +- **时间轮**:自己实现,内存级别,精度高但不持久化 +- **Redis ZSet**:score 存执行时间戳,定时轮询 `ZRANGEBYSCORE`,简单实用 +- **RocketMQ 5.0**:新版本支持任意时间延迟消息,底层用时间轮 + CommitLog 实现,解决了这个痛点 + +``` +Redis方案: +ZADD delay_queue +// 定时任务 +ZRANGEBYSCORE delay_queue 0 → 取出到期消息投递 +``` + +生产上我们用 Redis ZSet 方案做过订单超时取消,精度 1 秒够用,实现成本低。RocketMQ 5.0 的任意延迟我调研过但还没上生产。 + +## 2.延迟队列怎么存储的 +**RocketMQ 延迟消息存储** + +消息最终都走 **CommitLog**,延迟消息也不例外,只是多了一层转发。 + +**写入流程** + +``` +Producer发送延迟消息 + ↓ +Broker写入 CommitLog(所有消息统一存这里) + ↓ +ReputMessageService 异步转发 + ↓ +不写原始Topic的ConsumeQueue +而是写 SCHEDULE_TOPIC_XXXX 的ConsumeQueue +(18个Queue对应18个延迟级别) +``` + +**ConsumeQueue 存什么** + +每条索引记录固定 20 字节: + +``` +CommitLog Offset(8字节)+ Size(4字节)+ TagHashCode(8字节) +``` + +定时任务通过 ConsumeQueue 找到消息在 CommitLog 的位置,读出来投递。 + +**定时调度** + +``` +ScheduleMessageService: +- 每个延迟级别对应一个 Timer 任务 +- 记录每个 Queue 的消费进度 offset +- 到期消息从 CommitLog 读出,重新投递到原始 Topic 的 ConsumeQueue +- 更新 offset,持久化到磁盘 +``` + +**关键点** + +- 消息**只写一次** CommitLog,不做物理拷贝,转发只是写新的 ConsumeQueue 索引,IO 开销小 +- 定时任务挂了重启后,从持久化的 offset 继续消费,不丢消息 +- 这也解释了为什么只有固定级别——每个级别一个 Queue,顺序写顺序读,性能最优 + +RocketMQ 5.0 任意延迟底层加了个**时间轮**,按秒级 bucket 组织消息,到期再捞出来走同样的投递流程。 + +## 3.Redis和本地缓存的更新策略 +**两级缓存架构** + +``` +请求 → 本地缓存(L1) → Redis(L2) → DB +``` + +本地缓存用 Guava Cache 或 Caffeine,Redis 作二级。 + +**各自的更新策略** + +**Redis 更新** 主流用 Cache Aside: + +- 读:先读 Redis,miss 再查 DB,回填 Redis +- 写:更新 DB,删除 Redis,依赖下次读重建 + +也可以用 Canal 监听 binlog 异步更新,业务代码无感知。 + +**本地缓存更新** + +难点在这里——本地缓存在每个节点内存里,DB 或 Redis 更新后**无法主动推送**。 + +几个方案: + +**方案1:短 TTL 自然过期** + +- 本地缓存设很短的过期时间(几秒到几十秒) +- 简单,接受短暂不一致 +- 适合变更不频繁的数据 + +**方案2:MQ 广播通知** + +``` +更新DB → 发MQ广播消息 → 所有节点消费 → 删除本地缓存 +``` + +- 一致性好,延迟低 +- 节点多时 MQ 消息量大,需要注意消费积压 + +**方案3:Redis Pub/Sub** + +``` +更新DB → Redis publish失效通知 → 所有订阅节点删除本地缓存 +``` + +- 比 MQ 轻量,适合缓存失效通知这种场景 +- 缺点:Pub/Sub 不持久化,订阅者不在线会丢消息 + +**踩过的坑** + +本地缓存 + Redis 双写时,要注意**更新顺序**:先删本地缓存,再删 Redis,顺序反了可能导致本地缓存读到旧值回填 Redis。 + +另外本地缓存容量要控制好,Caffeine 设最大条数 + 弱引用,避免 OOM。 + +## 4.mysql慢查询怎么处理 +**定位阶段** + +开启慢查询日志: + +```sql +slow_query_log = ON +long_query_time = 1 -- 超过1s记录 +``` + +用 `mysqldumpslow` 或 `pt-query-digest` 分析日志,找出高频慢 SQL。 + +**分析阶段** + +`EXPLAIN` 是核心工具,重点看几个字段: + +```sql +EXPLAIN SELECT * FROM orders WHERE user_id=1 AND status=0; +``` + +- **type**:`ALL` 全表扫描要警惕,理想是 `ref`/`range`/`const` +- **key**:实际用到的索引,NULL 说明没走索引 +- **rows**:预估扫描行数,越大越危险 +- **Extra**:`Using filesort`、`Using temporary` 出现就要优化 + +**常见原因和解法** + +**索引问题** + +- 没建索引 → 补索引 +- 索引失效:函数操作、隐式类型转换、`%`前缀模糊查询 → 改写 SQL +- 索引选择错误 → `FORCE INDEX` 或调整统计信息 + +**SQL 写法问题** + +```sql +-- 差:对索引列用函数 +WHERE DATE(created_at) = '2024-01-01' +-- 好:范围查询 +WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02' +``` + +**数据量问题** + +- 深分页:`LIMIT 100000, 20` 扫描大量数据 → 改用游标分页 + +```sql +WHERE id > last_id LIMIT 20 +``` + +- 大表 JOIN → 小表驱动大表,或拆成多次查询 + +**其他方向** + +- 读写分离:慢查询打到从库 +- 分库分表:数据量级到了就得拆 +- 加缓存:热点查询不打 DB + +生产上遇到最多的是索引失效和深分页,`EXPLAIN` 基本能定位 80% 的问题。 + +## 5.索引未命中有什么可能,怎么处理 +**常见索引未命中原因** + +**1. 对索引列做函数或运算** + +```sql +-- 未命中 +WHERE YEAR(created_at) = 2024 +WHERE age + 1 = 18 +-- 改写 +WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01' +WHERE age = 17 +``` + +**2. 隐式类型转换** + +```sql +-- phone 是 varchar,传了 int,触发隐式转换 +WHERE phone = 13800138000 -- 未命中 +WHERE phone = '13800138000' -- 命中 +``` + +这个坑很隐蔽,`EXPLAIN` 看到 type=ALL 但明明有索引,大概率是这个问题。 + +**3. 模糊查询前缀 %** + +```sql +WHERE name LIKE '%张' -- 未命中 +WHERE name LIKE '张%' -- 命中 +``` + +必须前缀匹配才能走索引,实在需要全模糊考虑 ES。 + +**4. 联合索引不满足最左前缀** + +```sql +-- 索引 (A, B, C) +WHERE B=1 AND C=1 -- 未命中,跳过了A +WHERE A=1 AND C=1 -- A命中,C未命中 +``` + +**5. OR 连接非索引列** + +```sql +WHERE id=1 OR name='张三' -- name无索引,整体退化全表扫 +-- 改写成 UNION +SELECT * FROM t WHERE id=1 +UNION +SELECT * FROM t WHERE name='张三' +``` + +**6. NOT IN / NOT EXISTS** 优化器认为范围太大,可能放弃索引,改写成 JOIN 或 EXISTS。 + +**7. 数据区分度太低** + +```sql +WHERE status = 1 -- status只有0/1,50%数据,优化器认为全表扫更快 +``` + +优化器基于成本选择,区分度低的列索引意义不大。 + +**排查方法** + +```sql +EXPLAIN SELECT ... +-- 看 type、key、Extra +-- type=ALL + key=NULL → 未命中 +-- Extra有 Using where 但 key 有值 → 命中索引但有回表过滤 +``` + +生产踩过最多的是隐式类型转换,代码里 ORM 参数类型没对齐,`EXPLAIN` 一看立刻定位。 + +## 手撕:给你x元,用1元,5元,10元,20元,50元100元的无限硬币,能有多少种组合方式 +经典完全背包的变种,组合数问题。 + +**思路** + +dp[i] 表示凑成金额 i 的组合数,coins 无限用。 + +```go +func coinCombinations(amount int) int { + coins := []int{1, 5, 10, 20, 50, 100} + dp := make([]int, amount+1) + dp[0] = 1 // 金额为0,有1种方案(什么都不选) + + // 外层遍历硬币,内层遍历金额 + // 保证组合数(不考虑顺序) + for _, coin := range coins { + for j := coin; j <= amount; j++ { + dp[j] += dp[j-coin] + } + } + return dp[amount] +} +``` + +**关键点:循环顺序** + +外层枚举硬币,内层枚举金额——这样每种硬币作为"新加入"的选择,保证不重复计排列。 + +反过来外层枚举金额,内层枚举硬币,求的是**排列数**(1+5 和 5+1 算两种),不是组合数。 + +**验证** + +``` +amount=10: +1+1+...+1, 1+4+5, 5+5, 10 ... +dp[10] = 4 +``` + +**复杂度** + +- 时间:O(n * amount),n 是硬币种数 +- 空间:O(amount) + +这道题面试时容易在循环顺序上翻车,本质是完全背包求方案数,记住**组合数外层coin内层amount**就不会错。 diff --git a/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节一面.md b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节一面.md new file mode 100644 index 0000000..b408ec4 --- /dev/null +++ b/mengyanote-backend/data/mengyanote/实习求职/面试经历/Golang后端/✅字节一面.md @@ -0,0 +1,558 @@ +字节一面(亲测没做出算法题也能过) +先做一下自我介绍。 +实习项目拷打 +## Redis ZSet底层怎么实现?跳表结构讲一下。 +ZSet 底层有两种编码,按数据量自动切换: + +**listpack(原 ziplist)** + +- 元素数 < 128 且所有值长度 < 64 字节时用 +- 紧凑内存布局,连续存储,查询是 O(n) 但数据少无所谓 + +**skiplist + dict 组合** + +- 超过阈值后转换,两个结构同时维护 +- dict 提供 O(1) 的 score 查找 +- skiplist 支持 O(log n) 的范围查询(ZRANGE、ZRANGEBYSCORE) + +**跳表核心结构** + +``` +level 3: head → [20] ----------------------→ tail +level 2: head → [20] → [50] --------------→ tail +level 1: head → [20] → [50] → [70] → [90] → tail +``` + +- 每个节点随机生成层高(概率 p=0.25,最大 64 层) +- 查找时从最高层开始,逐层下降,跳过不必要节点 +- 每个节点还有 `span` 记录跨度,支持 O(log n) 的 rank 查询 + +**为什么不用红黑树?** + +跳表实现简单、范围查询更直观,锁粒度也更容易控制。Redis 作者 antirez 自己说过"skiplist 在实践中和红黑树性能差不多,但代码简单多了"。 + +有个细节值得注意:ZSet 的 `span` 字段是我觉得设计比较精巧的地方,ZRANK 能做到 O(log n) 就靠它。 + +## ZSet按分数范围查找的时间复杂度?范围过大会不会退化? +**时间复杂度:O(log n + k)** + +- log n:跳表定位到起始节点 +- k:遍历范围内的 k 个元素 + +**会不会退化?** + +严格来说不叫"退化",k 是结果集大小,线性遍历是不可避免的——你要返回 10w 个元素,任何数据结构都得输出 10w 个。 + +真正的问题是**大范围查询的实际风险**: + +- **网络传输瓶颈**:结果集太大,序列化 + 传输才是性能杀手,跳表本身反而不是瓶颈 +- **Redis 单线程阻塞**:ZRANGEBYSCORE 一次返回 100w 条,会长时间占用主线程,影响其他命令 +- **内存分配压力**:结果集构建需要一次性分配大块内存 + +**实践中的应对方案** + +用 `ZRANGEBYSCORE key min max LIMIT offset count` 做分页,或者 Redis 6.2+ 的 `ZRANGEBYLEX` + cursor 方式。 + +我们之前有个排行榜场景,有人直接 ZRANGE 0 -1 拉全量,数据涨到 50w 之后开始出现慢查询告警。后来改成按 score 分段拉取,每段 1000 条,问题解决了。 + +所以跳表结构本身没问题,**大范围是工程问题,不是算法问题**。 + +## 只查某个key是否在ZSet 里,时间复杂度是多少? +用 `ZSCORE key member` 或 `ZRANK key member`。 + +**ZSCORE:O(1)** + +ZSet 内部同时维护了一个 dict(member → score 的哈希表),判断存在性直接查 dict,跟普通 HashMap 一样。 + +**ZRANK:O(log n)** + +要返回排名,必须走跳表,所以是 log n。 + +--- + +如果只是判断"在不在",用 ZSCORE 就够了,返回 nil 即不存在。没必要用 ZRANK,多一个数量级的开销。 + +这也是 ZSet 同时维护 skiplist + dict 两个结构的意义所在——空间换时间,两种查询各走各的最优路径。 + +## 按key 查询慢怎么优化?(Hash+ZSet方案) +这个问题我理解是:**业务上需要同时支持"按属性过滤"和"按分数排序",单用 ZSet 不够用**的场景。 + +**典型痛点** + +ZSet 只能按 score 排序,无法按 member 的其他字段过滤。比如"查年龄在 20-30 之间、按积分排名的用户",ZSet 单独做不了。 + +**Hash + ZSet 组合方案** + +``` +Hash: user:{uid} → {name, age, score, ...} # 存完整对象 +ZSet: rank:global → uid (score=积分) # 存排序关系 +ZSet: rank:age:25 → uid (score=积分) # 按维度分桶 +``` + +查询流程: + +1. 从 ZSet 拿到排序后的 uid 列表 +2. pipeline/mget 批量从 Hash 取详情 +3. 业务层做二次过滤 + +**关键优化点** + +- **pipeline 批量取 Hash**,避免 N 次网络往返,这个收益很明显 +- **ZSet 按维度分桶**,比如 `rank:city:beijing`,缩小查询集合 +- **写扩散**:写入时同时更新多个 ZSet,读性能换写复杂度 + +**踩过的坑** + +分桶粒度不好把握——桶太细,ZSet 数量爆炸;桶太粗,范围查询结果集还是大。我们当时最终在热门维度上做分桶,长尾维度降级到业务层过滤。 + +本质上这是**用冗余索引换查询性能**,和 MySQL 加联合索引思路一样。 + +## 写入时怎么保证Hash和ZSet的原子性? +**几种方案,各有 trade-off** + +**1. MULTI/EXEC 事务** + +```redis +MULTI +HSET user:123 name "xx" score 100 +ZADD rank:global 100 123 +EXEC +``` + +- 保证命令原子执行,不会被其他命令插入 +- **但**:不支持回滚,中间某条命令语法错误其他照常执行 +- 不是真正的 ACID 事务,更像是"批量提交" + +**2. Lua 脚本(推荐)** + +```lua +redis.call('HSET', KEYS[1], 'score', ARGV[1]) +redis.call('ZADD', KEYS[2], ARGV[1], ARGV[2]) +``` + +- Redis 单线程执行 Lua,天然原子 +- 比 MULTI/EXEC 更灵活,可以加条件判断 +- 缺点:脚本调试麻烦,出问题难排查 + +**3. 接受最终一致性** + +老实说,很多业务场景其实不需要强原子性。 + +- 先写 Hash,再写 ZSet,加重试保证最终一致 +- 或者引入消息队列,异步补偿 +- Hash 和 ZSet 短暂不一致,业务能否容忍?大部分排行榜场景其实可以 + +**我的判断** + +Lua 脚本是最常用的强一致方案,但要评估必要性。如果是积分排行榜这种场景,1秒内最终一致完全够用,别过度设计。 + +真正的原子性需求更多出现在**扣减库存、资金操作**这类场景,排行榜用 Lua 保险但不一定必须。 + +## 为什么ZSet不直接设计成Hash+链表? +好问题,直接说核心矛盾: + +**链表的致命问题是范围查询** + +链表查找第 k 个元素是 O(n),无法二分。ZSet 最核心的操作是 `ZRANGEBYSCORE`、`ZRANK` 这类**有序范围查询**,链表完全撑不住。 + +**对比一下** + +|操作|链表|跳表| +|---|---|---| +|按 score 范围查|O(n)|O(log n + k)| +|插入|O(1)(知道位置)|O(log n)| +|ZRANK|O(n)|O(log n)| + +链表唯一的优势是插入 O(1),但**前提是你已经知道插入位置**——而维护有序链表的插入位置本身就要 O(n) 查找,优势消失。 + +**为什么不用数组 + 二分?** + +数组二分查找是 O(log n),但插入需要移动元素,O(n) 的写代价在高频写场景不可接受。 + +**跳表的本质** + +跳表是在链表上加了多级索引,用空间换时间,把链表的 O(n) 查询拉到 O(log n),同时保留了链表插入灵活的特性。可以理解为**穷人版的平衡树**,但实现复杂度低得多。 + +所以 Hash + 链表根本无法满足 ZSet 的核心使用场景,这不是优化问题,是结构性缺陷。 + +## Redis有哪些持久化方式? +**两种:RDB 和 AOF,以及混合模式** + +**RDB(快照)** + +- 定时 fork 子进程,把内存数据序列化成二进制文件 +- 优点:文件紧凑、恢复快、对主进程影响小(COW 机制) +- 缺点:两次快照之间的数据会丢,宕机最多丢几分钟数据 + +**AOF(追加日志)** + +- 每条写命令追加到日志文件,重启时重放 +- 三种刷盘策略:`always` / `everysec` / `no` +- `everysec` 是默认,最多丢 1 秒数据,性能和安全的折中 +- 缺点:文件比 RDB 大,恢复慢;AOF rewrite 会有短暂 CPU 压力 + +**混合持久化(Redis 4.0+,推荐)** + +AOF rewrite 时,先写 RDB 格式的全量快照,后面追加增量 AOF 日志。恢复速度接近 RDB,数据完整性接近 AOF。 + +**实践判断** + +- 纯缓存场景:RDB 够用,甚至可以关持久化 +- 对数据丢失敏感:混合模式 +- 我们线上用的混合模式 + `everysec`,重启恢复速度比纯 AOF 快很多,丢数据窗口控制在 1 秒内 + +有一个坑:RDB fork 时内存占用会瞬间翻倍(COW 最坏情况),内存打满过的,要给系统留足余量。 + +## 讲一下RDB的bgsave写时复制原理。 +**bgsave 流程** + +1. 主进程调用 `fork()`,产生子进程 +2. 子进程负责将内存数据写入临时 RDB 文件,完成后替换旧文件 +3. 主进程继续处理客户端请求,**不阻塞**(fork 本身有短暂阻塞) + +**COW 核心原理** + +fork 之后,父子进程共享同一份物理内存页,页表都指向同一块物理地址。 + +``` +fork 后: +父进程页表 ──┐ + ├──→ 同一块物理内存页(只读标记) +子进程页表 ──┘ + +父进程写入时: +OS 触发 COW → 复制该内存页 → 父进程写副本,子进程读原页 +``` + +- **只有被修改的页才会复制**,不是全量复制 +- 子进程始终看到 fork 时刻的内存快照,数据一致性有保证 + +**实际踩过的坑** + +写入压力大时,COW 复制的页很多,内存增量可能达到原来的 50%~100%。我们有台机器 Redis 内存用了 12G,bgsave 期间物理内存飙到接近 20G,触发了 OOM killer 把 Redis 干掉了。 + +后来的应对: + +- 给宿主机预留 30%+ 的内存 buffer +- 业务低峰期触发 bgsave,减少写入量,降低 COW 触发频率 +- 监控 `latest_fork_usec`,fork 耗时过长就告警 + +**另一个细节** + +fork 本身是同步的,会阻塞主进程。内存越大,fork 耗时越长——我们遇到过 10G 内存的实例 fork 耗时 200ms+,对延迟敏感的业务是个隐患。 + +## MySQL索引有哪些类型?聚簇索引怎么实现? +**索引类型** + +按数据结构: + +- **B+ Tree**:主流,InnoDB 默认,支持范围查询 +- **Hash**:Memory 引擎,精确查找 O(1),不支持范围 +- **全文索引**:FULLTEXT,针对文本搜索 +- **R-Tree**:空间索引,GIS 场景 + +按逻辑角色: + +- 聚簇索引(主键) +- 二级索引(普通、唯一、联合、前缀) + +**聚簇索引实现** + +InnoDB 里,**数据本身就是按主键组织的 B+ Tree**,叶子节点存的是完整行数据。 + +``` +非叶子节点:[主键值 | 子节点指针] +叶子节点: [主键值 | 整行数据] ←→ [主键值 | 整行数据] + (双向链表,支持范围扫描) +``` + +- 一张表只能有一个聚簇索引 +- 没有显式主键时,InnoDB 会用第一个非空唯一索引,再没有就隐式生成 rowid + +**二级索引的关键细节** + +叶子节点存的是**主键值**,不是行数据。查询时需要**回表**——先走二级索引找到主键,再去聚簇索引查完整行。 + +覆盖索引就是为了避免回表,把查询所需字段都放进联合索引的叶子节点里。 + +**一个常见坑** + +主键建议用自增整数,不要用 UUID。UUID 随机写入会导致 B+ Tree 页频繁分裂,写性能差,且索引文件更大。我们之前有个表用 UUID 做主键,写入 TPS 上来后页分裂严重,改成雪花 ID 之后好多了。 + +## 为什么用B+树而不是B树? +**核心差异就两点** + +**1. 数据只存在叶子节点** + +B 树的非叶子节点也存数据,B+ 树非叶子节点只存键值和指针。 + +直接结果:**B+ 树非叶子节点能放更多键值**,树更矮,同样数据量下 IO 次数更少。 + +InnoDB 页大小 16KB,B+ 树一个非叶子节点能放几百个键,三层树就能索引千万级数据,通常只需 2~3 次磁盘 IO。 + +**2. 叶子节点双向链表** + +``` +叶子层:[10|data] ↔ [20|data] ↔ [30|data] ↔ [40|data] +``` + +范围查询 `WHERE id BETWEEN 20 AND 40`,找到起始节点后**顺序遍历链表**即可,不需要回到根节点重新查找。 + +B 树没有这个链表,范围查询要反复中序遍历,IO 跳跃严重。 + +**总结对比** + +||B 树|B+ 树| +|---|---|---| +|非叶子节点|存数据|只存键| +|树高|相对高|更矮| +|范围查询|差|链表顺序扫,好| +|单点查询|理论略快(中间节点可能命中)|必须到叶子| + +单点查询 B 树理论上有优势,但概率低,收益不稳定。数据库范围查询是核心场景,B+ 树的链表设计完胜。 + +这个选择本质上是**为磁盘 IO 模式优化的**——顺序读远比随机跳跃读便宜。 + +## 树高更少为什么查询更快?数据从哪里读到Buffer Pool? +**树高更矮 → IO 次数更少** + +根本原因是**磁盘 IO 是数据库性能瓶颈**,不是 CPU。 + +B+ 树每个节点对应一个磁盘页(InnoDB 16KB),查找一个节点 = 一次磁盘 IO。树高 3 层最多 3 次 IO,树高 5 层就是 5 次——磁盘随机 IO 延迟在 ms 级,差 2 次 IO 就是几毫秒的差距。 + +内存访问是 ns 级,所以**节点一旦在 Buffer Pool 里,树高几乎不重要**,IO 才是命门。 + +**数据怎么进 Buffer Pool** + +``` +查询请求 + → 检查 Buffer Pool 中是否有该页(page_id 查哈希表) + → 命中:直接读内存返回 + → 未命中: + 1. 从 Buffer Pool 分配空闲页(没有则 LRU 淘汰) + 2. 读取 .ibd 文件中对应的 16KB 数据页 + (OS 层:文件系统 → page cache → read() 系统调用) + 3. 载入 Buffer Pool,更新页表 + 4. 返回数据 +``` + +**几个关键细节** + +- Buffer Pool 用**改进版 LRU**,分冷热区(3/8 冷区),防止全表扫描把热数据挤走 +- 预读机制:线性预读会提前把相邻页加载进来,范围查询受益明显 +- 脏页不立刻写磁盘,由后台线程异步刷盘(WAL 保证崩溃安全) + +**一个实际问题** + +Buffer Pool 命中率低才是慢查询的真正元凶。我们排查过一个慢查询,explain 看索引都走对了,但响应时间抖动很大——最后发现是 Buffer Pool 太小,热数据频繁被淘汰,每次都要重新从磁盘读。调大 `innodb_buffer_pool_size` 到物理内存 70% 后稳定了。 + +## B树和B+树还有什么区别?(叶子节点双向链表、范围查询) +这个问题我在上面"为什么用 B+ 树"那个回答里基本都覆盖了,要不你看看还有哪个点没讲清楚? + +如果要补充的话,有一个点之前没展开: + +**叶子节点链表对写入的影响** + +B+ 树叶子层维护双向链表,意味着插入新节点时不只是树结构调整,还要**维护前后指针**。页分裂时需要同时更新相邻页的指针,涉及多个页的修改,这也是为什么大量随机插入会触发频繁页分裂、性能下降的原因之一。 + +自增主键的优势就在这里——顺序插入只在链表尾部追加,分裂少。 + +其他区别基本就是上面说的: + +- 非叶子节点存不存数据 +- 树高和 IO 次数 +- 范围查询链表遍历 vs B 树中序遍历 + +如果有具体想深挖的点可以直接问。 + +## Java HashMap底层结构? +**JDK 8 之后的结构:数组 + 链表 + 红黑树** + +``` +table[] + [0] → null + [1] → Node{key,value,hash,next} → Node → ... + [2] → TreeNode(红黑树,链表长度 > 8 时转换) + ... + [n] +``` + +**核心参数** + +- 初始容量:16,负载因子:0.75 +- 扩容触发:元素数 > 容量 × 0.75,**2 倍扩容** +- 链表 → 红黑树:链表长度 > 8 **且**数组长度 ≥ 64 +- 红黑树 → 链表:节点数 < 6 + +**hash 计算细节** + +```java +hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) +``` + +高 16 位异或低 16 位,让高位参与散列,减少碰撞。数组下标:`hash & (n-1)`,等价于取模但更快。 + +**为什么阈值是 8?** + +链表长度符合泊松分布,长度达到 8 的概率约 0.00000006,正常散列下几乎不会触发红黑树转换。真触发了说明 hashCode 实现有问题或者被恶意攻击(hash 碰撞攻击)。 + +**扩容的关键优化** + +JDK 8 扩容时不重新计算 hash,利用 `hash & oldCap` 判断新位置:结果为 0 留在原位,为 1 移到 `原位 + oldCap`。比 JDK 7 的重 hash 高效很多。 + +线程安全问题:JDK 7 头插法扩容会形成环形链表死循环,JDK 8 改成尾插法解决了这个问题,但并发写仍然不安全,该用 ConcurrentHashMap 的场景别用 HashMap。 + +## HashMap什么时候从链表转红黑树?为什么要满足数组长度>64? +**转换条件是两个同时满足** + +1. 链表长度 > 8 +2. 数组长度 ≥ 64 + +**为什么要加数组长度 ≥ 64 这个条件?** + +核心逻辑:**链表长,不一定是 hash 碰撞严重,可能只是数组太小**。 + +假设数组长度只有 16,元素落到同一个桶的概率天然就高,链表变长是正常现象。这时候正确的解法是**扩容**,把元素重新散列到更多桶里,而不是转红黑树。 + +转红黑树的代价: + +- TreeNode 占用空间是普通 Node 的 **2 倍** +- 维护红黑树平衡有额外开销 + +如果数组小就无脑转红黑树,既浪费内存,又没解决根本问题(桶太少)。 + +**实际触发流程** + +``` +链表长度超过 8 + → 数组长度 < 64? + → 触发扩容(resize),重新散列 + → 扩容后链表自然变短,大概率不需要转树 + → 数组长度 ≥ 64? + → 真正转红黑树(treeifyBin) +``` + +**所以本质是** + +数组长度 ≥ 64 是一个**前置保障**——确认扩容已经无法缓解碰撞问题,链表长是真的 hash 分布差,这时候才值得付出红黑树的空间和维护代价。 + +实践中如果 key 的 hashCode 实现不好,或者初始容量设太小又不预估扩容,容易触发这个路径。 + +## 退化回链表的阈值是多少? +**阈值是 6** + +红黑树节点数 ≤ 6 时退化回链表。 + +**为什么不是 8,要留 2 的缓冲?** + +防止在阈值附近频繁横跳。如果转换和退化都用 8,元素在 8 附近反复增删,就会不断触发链表 → 红黑树 → 链表的转换,每次都有开销。 + +6 和 8 之间留了 2 个元素的缓冲区,避免这种抖动。 + +这个设计思路在很多系统里都能看到,比如 TCP 拥塞控制的阈值也会类似处理——**状态切换有滞后性,是刻意为之**。 + +## 浏览器输入URL到页面展示全过程? +**网络层** + +1. DNS 解析 → 本地缓存 → 递归查询,TTL 设置不合理会有缓存穿透问题 +2. TCP 三次握手,HTTPS 还要 TLS 握手(1.2 需 2-RTT,1.3 优化到 1-RTT) +3. 发送 HTTP 请求,服务端处理后返回响应 + +**浏览器渲染层** + +1. 解析 HTML → DOM Tree,解析 CSS → CSSOM +2. 合并成 Render Tree → Layout(计算位置)→ Paint → Composite +3. JS 会阻塞 DOM 解析,所以 `