diff --git a/.obsidian/community-plugins.json b/.obsidian/community-plugins.json index 19ce225d..3fd6628e 100644 --- a/.obsidian/community-plugins.json +++ b/.obsidian/community-plugins.json @@ -26,5 +26,6 @@ "memos-ai-sync", "memos-sync", "translate", - "obsidian-douban-plugin" + "obsidian-douban-plugin", + "obsidian-icon-folder" ] \ No newline at end of file diff --git a/.obsidian/icons/font-awesome-brands.zip b/.obsidian/icons/font-awesome-brands.zip new file mode 100644 index 00000000..62c4322d Binary files /dev/null and b/.obsidian/icons/font-awesome-brands.zip differ diff --git a/.obsidian/icons/font-awesome-regular.zip b/.obsidian/icons/font-awesome-regular.zip new file mode 100644 index 00000000..62c4322d Binary files /dev/null and b/.obsidian/icons/font-awesome-regular.zip differ diff --git a/.obsidian/icons/simple-icons.zip b/.obsidian/icons/simple-icons.zip new file mode 100644 index 00000000..093e1bb8 Binary files /dev/null and b/.obsidian/icons/simple-icons.zip differ diff --git a/.obsidian/plugins/obsidian-icon-folder/data.json b/.obsidian/plugins/obsidian-icon-folder/data.json new file mode 100644 index 00000000..d801b21f --- /dev/null +++ b/.obsidian/plugins/obsidian-icon-folder/data.json @@ -0,0 +1,31 @@ +{ + "settings": { + "migrated": 6, + "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": "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/.obsidian/plugins/obsidian-icon-folder/main.js b/.obsidian/plugins/obsidian-icon-folder/main.js new file mode 100644 index 00000000..271b2ffc --- /dev/null +++ b/.obsidian/plugins/obsidian-icon-folder/main.js @@ -0,0 +1,7158 @@ +/* +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/.obsidian/plugins/obsidian-icon-folder/manifest.json b/.obsidian/plugins/obsidian-icon-folder/manifest.json new file mode 100644 index 00000000..6e916c33 --- /dev/null +++ b/.obsidian/plugins/obsidian-icon-folder/manifest.json @@ -0,0 +1,10 @@ +{ + "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/.obsidian/plugins/obsidian-icon-folder/styles.css b/.obsidian/plugins/obsidian-icon-folder/styles.css new file mode 100644 index 00000000..a74fdd44 --- /dev/null +++ b/.obsidian/plugins/obsidian-icon-folder/styles.css @@ -0,0 +1,120 @@ +.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/.obsidian/plugins/rss-reader/data.json b/.obsidian/plugins/rss-reader/data.json index fb322107..fae70d9d 100644 --- a/.obsidian/plugins/rss-reader/data.json +++ b/.obsidian/plugins/rss-reader/data.json @@ -316,6 +316,28 @@ "image": null, "description": "少数派致力于更好地运用数字产品或科学方法,帮助用户提升工作效率和生活品质", "items": [ + { + "title": "「世界主宰」的掌机之路:岩田聪与任天堂 DS", + "description": "身为掌机市场主宰者的任天堂当然寄希望于通过 GBA SP 缓过一口气,至少撑到自家的次世代主机上市。毕竟索尼 SCE 在这方面一直都是亏钱卖硬件,将 PlayStation 摧枯拉朽的闪击战拖进蓝海玩家的汪洋大海显然对任天堂非常有利——但是「卷王」久多良木健治下的索尼 SCE 攻城掠池的速度比某奥地利落榜美术生速通波兰还快,两家公司在掌机市场上的交锋比预想中的还要更早一些。查看全文", + "content": "身为掌机市场主宰者的任天堂当然寄希望于通过 GBA SP 缓过一口气,至少撑到自家的次世代主机上市。毕竟索尼 SCE 在这方面一直都是亏钱卖硬件,将 PlayStation 摧枯拉朽的闪击战拖进蓝海玩家的汪洋大海显然对任天堂非常有利——但是「卷王」久多良木健治下的索尼 SCE 攻城掠池的速度比某奥地利落榜美术生速通波兰还快,两家公司在掌机市场上的交锋比预想中的还要更早一些。查看全文", + "category": "", + "link": "https://sspai.com/post/71848", + "creator": "Kiririn", + "pubDate": "Tue, 15 Jul 2025 15:53:42 +0800", + "enclosure": "", + "enclosureType": "", + "image": "", + "id": "", + "language": "zh", + "folder": "资讯", + "feed": "少数派", + "read": false, + "favorite": false, + "created": false, + "tags": [], + "hash": "f9e3828119ff015709ad94a0d3fbc93e", + "highlights": [] + }, { "title": "从笃定到踌躇:Quote/0,一个开发者产品的真实旅程", "description": "Quote/0直播预告:7月17日周四19:30,包含产品补货以及重要功能演示与发布小红书:@少数派共创视频号:@少数派sspai距离上一次Quote/0直播已相距近一个月。作为Quote/0的开发者 ...查看全文", @@ -1187,28 +1209,6 @@ "hash": "6629c9637ca00d6edc393608f4b4f9aa", "highlights": [] }, - { - "title": "Save Desktop – 保存你的 Linux 桌面环境配置,支持云同步", - "description": "Save Desktop 可以帮你一键保存 Linux 当前配置的工具,包括主题、图标、壁纸、所有桌面环境设置、扩展等,并且支持通过网络在不同计算机中同步这些配置。@Appinn 这是一个帮你备份 L ", - "content": "\n

Save Desktop 可以帮你一键保存 Linux 当前配置的工具,包括主题、图标、壁纸、所有桌面环境设置、扩展等,并且支持通过网络在不同计算机中同步这些配置。@Appinn

\n\n\n\n
\"Save
\n\n\n\n

这是一个帮你备份 Linux 配置的工具,配合同步功能,可以实现在不同的电脑上使用相同的配置。

\n\n\n\n

支持的环境:

\n\n\n\n
    \n
  • GNOME
  • \n\n\n\n
  • Xfce
  • \n\n\n\n
  • Cinnamon
  • \n\n\n\n
  • Budgie
  • \n\n\n\n
  • COSMIC(Rust 和 GNOME 版本)
  • \n\n\n\n
  • Pantheon
  • \n\n\n\n
  • MATE
  • \n\n\n\n
  • KDE Plasma
  • \n\n\n\n
  • Deepin
  • \n\n\n\n
  • Hyprland(目前系统注销功能尚不可用)
  • \n
\n\n\n
\n
\"Save
\n\n\n

云同步支持 GNOME OA、Rclone 或 Syncthing。

\n\n\n\n

Save Desktop 可以保存以下内容:

\n\n\n\n
    \n
  • 你的图标、字体和主题
  • \n\n\n\n
  • 你的设置
  • \n\n\n\n
  • 你的壁纸(包括动态壁纸,前提是用户名保持不变)
  • \n\n\n\n
  • 你已安装的 Flatpak 应用及其数据
  • \n\n\n\n
  • 你主目录下的 Desktop 文件夹
  • \n\n\n\n
  • 其他与你桌面环境相关的项目(如 Cinnamon 扩展和小程序、KDE Plasma 小部件、GNOME 和 Nautilus 扩展等)
  • \n
\n\n\n\n

获取

\n\n\n\n\n\n\n\n
\n\n\n\n

原文:https://www.appinn.com/save-desktop-for-linux/

\n

[ 点击前往获取链接 ]


", - "category": "linux", - "link": "https://www.appinn.com/save-desktop-for-linux/", - "creator": "青小蛙", - "pubDate": "Sun, 13 Jul 2025 07:36:30 +0000", - "enclosure": "", - "enclosureType": "", - "image": "", - "id": "", - "language": "zh", - "folder": "资讯", - "feed": "小众软件", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "0dee92d692d0043bbc4052c6a2c0634a", - "highlights": [] - }, { "title": "Windows 远程控制的最佳方案,我只服微软。但还有一个问题…", "description": "作为 Windows 操作系统的开发者,微软提供了整套跨平台的远程控制方案,就是 Windows App! 微软在起名这方面,还是非常随意的。 顾名思义,Windows App 是一个叫做 Windo ", @@ -1231,28 +1231,6 @@ "hash": "88576d3b555645965579af10c7b190be", "highlights": [] }, - { - "title": "微软自动奖励 – 一个 Chrome 插件,把 Bing 积分刷到 1000 多分了", - "description": "微软自动奖励(Microsoft Automatic Rewards)是一个 Chrome 插件,通过自动完成每日任务获取 Bing 积分。安装插件之后,只需要登录 Microsoft Rewards ", - "content": "\n

微软自动奖励(Microsoft Automatic Rewards)是一个 Chrome 插件,通过自动完成每日任务获取 Bing 积分。安装插件之后,只需要登录 Microsoft Rewards 和 Bing 即可。@Appinn

\n\n\n\n
\"微软自动奖励
\n\n\n\n

感谢 @陈少举 同学的分享:

\n\n\n\n
\"微软自动奖励
\n\n\n\n

Microsoft Rewards 是什么

\n\n\n\n

Microsoft Rewards(微软奖励)是微软推出的一个积分奖励计划,旨在鼓励用户多使用微软的各类产品和服务,比如 Bing 搜索引擎、Microsoft Edge 浏览器、Xbox 主机、Microsoft Store 购物等。用户只需拥有一个微软账户,完成日常搜索、参与小测试、完成任务或购物等操作,就可以获得积分。

\n\n\n\n

这些积分可用于兑换各类礼品卡,比如国区可兑换:

\n\n\n\n
    \n
  • 原神原石兑换码
  • \n\n\n\n
  • 我的世界钻石(网易)
  • \n\n\n\n
  • 肯德基消费券
  • \n\n\n\n
  • 盒马礼品卡
  • \n\n\n\n
  • 猫眼礼品卡
  • \n\n\n\n
  • 中国石化礼品卡
  • \n\n\n\n
  • 天猫超市礼品卡
  • \n
\n\n\n\n

等等,礼物还是非常多的。

\n\n\n\n

以及,未来的 Windows 10 扩展安全更新(ESU)可能需要 1000 积分来兑换 \"😂\"

\n\n\n\n

(参考:微软大反转!Windows 10 免费安全更新再送一年)

\n\n\n\n

如何注册

\n\n\n\n

可以前往官网(手动去掉下方链接尾巴即可),或通过青小蛙的邀请链接注册:

\n\n\n\n\n\n\n\n

通过青小蛙的邀请链接还可以获得额外积分:

\n\n\n\n
    \n
  • 当好友完成每个任务时,你和你的好友将获得最多 650 个积分
  • \n\n\n\n
  • 好友完成所有欢迎优惠任务后,你和你的好友将另外获得 650 个积分(总共各 1300 个积分)
  • \n
\n\n\n\n

微软自动奖励(Microsoft Automatic Rewards)

\n\n\n\n

在注册之后,就可以启动这个插件了:

\n\n\n\n
    \n
  1. 选择你要进行自动化的任务
  2. \n\n\n\n
  3. 设置参数
  4. \n\n\n\n
  5. 获取积分
  6. \n
\n\n\n\n

注意,开启后,它会找时间自动打开新标签页进行搜索,所以不要被吓到了 \"😂\"

\n\n\n\n
\"微软自动奖励
\n\n\n\n

这是一个逐渐累积的过程,比如青小蛙今天获得了30个积分:

\n\n\n\n
\"微软自动奖励
\n\n\n\n

获取

\n\n\n\n\n\n\n\n

是的,并未上架 edge 商店。

\n\n\n\n

一个搬运:夸克|百度|城通 (访问密码: 5712)

\n\n\n\n

开发者模式下,直接将 .crx 文件拖进浏览器的扩展页面即可。

\n\n\n\n
\n\n\n\n

原文:https://www.appinn.com/microsoft-automatic-rewards/

\n

[ 点击前往获取链接 ]


", - "category": "Chrome", - "link": "https://www.appinn.com/microsoft-automatic-rewards/", - "creator": "青小蛙", - "pubDate": "Wed, 09 Jul 2025 04:39:12 +0000", - "enclosure": "", - "enclosureType": "", - "image": "", - "id": "", - "language": "zh", - "folder": "资讯", - "feed": "小众软件", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "520f9be2227d5727b8de63bbe0f5d6ee", - "highlights": [] - }, { "title": "Backlog.md – 在 git 仓库中用 .md 生成并管理项目看板", "description": "Backlog.md 是一款专为使用 git 的开发者设计的本地项目看板工具,拥有命令行与网页版两种操作模式。 它的思路很直接:每个任务都用一个 .md 文件保存在 git 仓库里,所有进度、分配、归 ", @@ -1275,28 +1253,6 @@ "hash": "a7c49b4081e799784da1d56006161ef7", "highlights": [] }, - { - "title": "DockFlow – 分组切换 Mac 程序坞", - "description": "DockFlow 是一款非常实用的 macOS 工具,支持创建多套程序坞图标组合,并可随场景一键切换。比如在工作时显示多个工作软件,娱乐时切换为游戏或影音应用。@Appinn 数码荔枝上新,买断产品。 ", - "content": "\n

DockFlow 是一款非常实用的 macOS 工具,支持创建多套程序坞图标组合,并可随场景一键切换。比如在工作时显示多个工作软件,娱乐时切换为游戏或影音应用。@Appinn

\n\n\n\n
\"DockFlow
\n\n\n\n

数码荔枝上新,买断产品。

\n\n\n\n

购买地址

\n\n\n\n\n\n\n\n

专为 Mac 用户打造的工作区切换工具,可通过创建和切换 Dock 栏图标方案,轻松适应不同使用场景。

\n\n\n\n
\n\n
\n\n\n\n

快速切换,适应不同场景

\n\n\n\n

无论是开发、办公还是娱乐,只需设置不同图标组合方案,通过快捷键或菜单栏图标一键切换。让你的工作流不再混乱,轻松专注当前任务。

\n\n\n\n
\"DockFlow
\n\n\n\n

自由布局,添加多种项目

\n\n\n\n

支持添加文件夹、文件、网址链接(见视频)到程序坞,甚至还能插入空白间隔,让布局更整洁。常用应用一触即达,效率直线提升!

\n\n\n\n
\"DockFlow
\n\n\n\n

配置导入导出,使用更灵活

\n\n\n\n

个性化设置可以备份导出,方便系统迁移或分享使用。并支持自动关闭非布局内应用,让切换更加智能。

\n\n\n\n

开发者可以在编程工具与设计软件间快速切换,商务人士可以从会议状态流畅进入工作模式,不被无关的图标打扰。

\n\n\n\n

新品上架,通过专属链接下单 DockFlow,到手仅 35 元,买断制永久使用!

\n\n\n\n

购买地址

\n\n\n\n\n\n\n\n
\n\n\n\n

原文:https://www.appinn.com/dockflow/

\n

[ 点击前往获取链接 ]


", - "category": "macOS", - "link": "https://www.appinn.com/dockflow/", - "creator": "青小蛙", - "pubDate": "Mon, 07 Jul 2025 06:56:26 +0000", - "enclosure": "", - "enclosureType": "", - "image": "", - "id": "", - "language": "zh", - "folder": "资讯", - "feed": "小众软件", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "018d59f4838b74ac87ba1d824f35bc41", - "highlights": [] - }, { "title": "扑克牌中,黑桃K上的国王到底是谁?", "description": "


本 文 约 5300 字

阅 读 需 要 16 分 钟 


扑克牌是现代社会最常见的娱乐玩品之一,但你有了解过,牌面上那一张张造型独特的图像是怎么来的吗?比如,黑桃K的牌面上,就集合了王冠、竖琴、利剑等种种纹样,最终组合成了一位威严的君主。他到底是谁?



偶像:英雄与神意


利剑、王冠、竖琴,这是牌面黑桃K上最醒目的元素,最为通行的说法认为,它们共同组成了一位人物原型,就是《圣经》中最受上帝青睐的国王——以色列“大卫王”。牌面上看似互不相关的纹样,反映的正是“大卫王”同时作为英雄、君主和信徒的身份特质。


按照《圣经》的描述,“大卫王”最初是以英雄的姿态现身于以色列王国的。大卫出生于伯利恒,是耶西的第八个儿子。这位以牧羊为业的少年在以色列与非利士的战争中崭露头角。据《撒母耳记》记叙,战场曾出现一位非利士巨人歌利亚,身强力大,无人能敌,少年大卫却主动请缨出战。他拒绝穿戴铠甲,用投石机应战,竟然成功击杀歌利亚,斩下其头颅,帮助犹太人获得了战斗胜利。此战后,大卫成为百姓交口称赞的英雄,也正式开始了从牧童向国王的身份转变。在《圣经》中,这部分故事围绕着他和以色列国王扫罗的争斗展开,大卫蒸蒸日上的声誉让扫罗心生忌惮,在他的驱逐下,大卫开启了自己的流浪生涯还组建了自己的军队。扫罗战死后,大卫在希伯伦受膏(指将膏油——通常是特制的圣油,倒在或涂抹在某人的头上,表明此人是上帝亲自选定来担任某个重要职分的人)为犹大王,最终统一了全以色列,定都耶路撒冷。



如果这一段传奇故事对应着大卫王形象中的利剑与王冠,那其衣袍上的竖琴图案,则提示着贯穿于他命运始末的神性主题。《撒母耳记》开篇便提到,由于扫罗的悖逆,大卫出生不久便被耶稣通过先知撒母耳选中,秘密膏立为以色列日后之王,而在其流亡途中,也是神的保护和看顾使其安然无恙。神意的出现不仅串联着大卫的生命起伏,也将其从牧童蜕变为君主的传奇彻底升华为神祇的显灵与作为,继而成为君王必须忠诚于耶和华的铁证。这样的暗示在《撒母耳记》中处处可见,甚至扫罗也曾因为大卫“被神膏立”的特殊身份而放弃对他的追杀与迫害。在这样的叙说下,大卫王不但有了坎坷离奇的生平,也在事实上成了最虔诚的信徒,甚至是神意福荫世间的符号。



除了迎回约柜,着手修建圣殿外,竖琴也成为大卫祈祷神旨、传递神意最具代表性的媒介。典型的一幕发生在大卫适逢扫罗王的时期,彼时饱受恶魔困扰的扫罗王因大卫的一曲竖琴奏乐而获得救赎,根源在于“耶和华的灵大大感动大卫”,使得手中的竖琴为了神意奏响,以上帝的慈爱悲悯驱散了邪灵。竖琴乐音因而成为连接大卫与神的桥梁,据说大卫王曾多次用竖琴赞美上帝,甚至在迎约柜时(约柜即放置上帝与以色列人所立契约的柜子)还命人奏响竖琴等乐器来虔诚敬拜。


回到现实来看,西方考古的进展曾证实,“大卫王”可能确有其人,活动于公元前10世纪的历史舞台,然而,《圣经》中提到的“定都耶路撒冷”等传奇经历却有着文学杜撰、美化之嫌。尽管如此,“大卫王”身为信徒榜样的地位却从未动摇,频频以救世主领袖、精通律法的学者等理想形象活跃在犹太历史与文化故事中。



受此影响,身世神圣而享誉后世的大卫王成了扑克牌人头牌的取材原型,竖琴、利剑、王冠等鲜明的身份符号也被一同保留。


然而,不管是大卫王还是其角色形象的故事,都没有结束。


圣像:勇气与权欲


世界著名雕塑家米开朗基罗曾留下一件旷世之作。



这件人物雕像是由一块被称为“巨人”的大理石雕刻而成,其成品的人物形象体格健壮,面目俊美,肌肉线条饱满,神态坚毅。人物左手紧握的石块则证明,米开朗基罗意图塑造的,正是经典的以色列英雄“大卫王”。



这件作品有着西方美术史上艺术成就最高的男性裸体塑像的美名。凭借巧夺天工的雕刻技艺,米开朗基罗成功还原了神话中大卫投石击杀歌利亚的场景,然而,人们细究发现,呈现在米开朗基罗手下的“大卫王”却和神话记载有着不小的出入。例如,《旧约》中创造奇迹的青年牧童,却成了雕塑中的俊美成人,故事中提及的石机也被“弱化”为了普通石头。


这番米开朗其罗故意为之的艺术设计,揭开了“大卫王”从神台走向近代历史的演变端倪。


问题的关键在于《大卫》的成型时间。1494年,在宗教人士吉罗拉莫·萨沃纳罗拉的领导下,人们揭竿而起推翻了曾统治佛罗伦萨的美第奇家族,建立了佛罗伦萨第二共和国。这个“新生”的共和国,正是雕塑《大卫》的主要委托方,据说,佛罗伦萨的“共和国政府”认为,《圣经》中大卫击败巨人的事迹同此前爆发的革命相互呼应,大卫身上闪烁的自由、正义光辉,成为佛罗伦萨人民反抗专制统治的最好注脚。


接受委托的米开朗基罗和美第奇家族渊源颇深,不过,尽管正是美第奇家族成员的资助让米开朗基罗有了学习和成长的机会,但他对美第奇家族的专制早有不满,1494年的革命无异于火上浇油,直接点燃了米开朗基罗对共和的青睐与渴望。据说,这位艺术家非常同情人民的反抗,面对此后美第奇家族复辟后的邀约也避之不及,甚至还曾亲身参与1529年的起义。政府委托和内心向往一拍而合,米开朗基罗开始潜心创作《大卫王》,一锤一刻下的也不单单只是神秘的偶像,而是米开朗基罗心中捍卫国家的勇士。因此,米开朗基罗放弃了对大卫割掉敌人头颅的胜利颂扬,转而仔细描刻其甩石的勇气与坚毅。在他的锤凿之下,大卫头部微俯,炯目而视,手中抓着投石带,仿佛下一瞬就将投掷巨石,驱逐强敌。雕像人物的蓄势待发与紧张神情,也被理解为米开朗基罗对世人的警策——不要忘记保护共和国的艰巨,如他自己所说,“领导人应当像大卫一样保卫祖国。”



相较于米开朗基罗的直抒胸臆,大卫王留在另一件艺术作品中的形象则更加隐晦和复杂。


这是一幅以裸体女性为主体的油画作品,由17世纪荷兰画家伦勃朗创作。画面中,身材略显臃肿的女性裸身而坐,手中无力地握着一封信笺,神情低沉而忧郁。画像中并未出现“大卫王”的身影,却影射了《圣经》传说中大卫王的另一段秘辛。



撒母耳记记载,大卫称王之后曾偶见一位年轻貌美的女子沐浴,迷恋其美色,后来得知此女是其部下乌里亚的妻子拔士巴,为“抱得美人归”,他不仅送亲笔信求爱,还设计谋害乌里亚战死沙场,最终将拔士巴召入宫内占为己有。大卫王最终因此举遭遇神罚,落得惨淡结局,拔士巴的故事也传于后世。伦勃朗描画的正是拔士巴手握大卫王来信的情景,但不同于传说中的桃色绯闻,伦勃朗笔下的拔士巴没有倾城之色,反而只是一个平凡女子。刻画的重点也从情节本身变化为了拔士巴的神情面容,画面中她眉眼低垂,似乎身处两难,内心挣扎痛苦。后世研究发现,这些处理与矛盾反映出伦勃朗完全异于米开朗基罗的创作动机,更直白地说,在这位特别的画家笔下,同时身处“暗处”的拔士巴和大卫王,共同构成了伦勃朗的尖锐批判与现实反击。


对抗首先来源于伦勃朗对“圣像”传统的坚守。17世纪的宗教改革深刻影响了荷兰社会,这也使得传统天主教奉行的圣像崇拜受到摒弃与抨击。然而,伦勃朗却不愿意放弃对《圣经》题材的创作,在他的心中,宗教圣像画并非神意外化,而是人性的容器,因此出自其画笔下的形象往往表现着鲜活的世俗与人格,抱负、斗争、梦想等活生生的生活与心声,被他如实展现在画布上。这样的坚守也被延续到了拔士巴的形象创作上,伦勃朗放弃了君主专制时期的社会审美,既延续“大卫王”的故事传统,用犀利笔触抨击了强权与暴君,又回到荷兰的艺术社会,以对真实肉体的描绘批判宫廷的虚伪,畅诉对主流的不屑。



反击也夹杂着伦勃朗的自我言说。根据记录,创作拔士巴画像时的伦勃朗正处于人生的低谷,丧妻、破产、穷困等不幸将其拖入泥潭,雪上加霜的是,与模特情妇的未婚同居使其遭遇了教会的公开谴责,甚至一度被禁止领圣餐。在《圣经》故事中,拔士巴与大卫王的“绯闻”一直被批驳为道德败坏的标志,这也使得这段对拔士巴的“再创”被后世理解为一种无声的控诉,手握信笺无力抗衡命运的美妇照映出现实中情妇遭遇的社会压力与道德指摘,而伦勃朗选择正面一切,欲望、丑闻、道德枷锁,通通被公之于世。


这种坦然和不屑在伦勃朗晚年的画像中被再度证实,画作中,垂垂老矣的伦勃朗完全无意掩盖自我的衰老和丑陋,他以真面目示人,也用真面目入画,只为留下自己作为“真实人物”而存在的容貌与形态,供世人观看、讨论乃至研究。


在这种复杂再现下,《手持大卫王来信的拔士巴》不仅获得了极高的艺术成就,也作为“思想与社会转型”的灯塔伫立于荷兰王国的历史潮涌中,其间充盈着的传统变革、宗教超越和人性回归,一同融入了大卫王的故事序章,成为其身姿历经时代境迁而留下的悠长剪影。

 

牌像:虚幻与现实


扑克牌的出现,是“大卫王”形象演变的另一处里程碑。


1813年,法国政府曾颁布一个官方批准的扑克设计,并对其中每一张人头牌正式命名,此举确定了扑克牌中的“J、Q、K”,使其分别成为侍从、皇后、国王的缩写。


神奇的是,除了名称的对应,十二张人头牌中不少的人物形象也和《圣经》等神话典籍有着密切联系。



其中,作为《圣经》典型代表人物的“大卫王”为何会被扑克选中,虽然已经无法考证,但也有些因头可寻。


首先可以证实的是,西方的扑克牌多少受到了埃及历史文化的影响。虽然根据研究,世界上最早的扑克牌应该出现在中国,以明朝时期的“叶子戏”为雏形,但需要承认的是,中国纸牌传向欧洲的过程,还是经历了地区之间的传递与过渡。伊斯坦布尔托普卡比博物馆的档案室中的一段记录证明了这段时期的存在,据其所言,1400年前的古埃及马穆鲁克王朝的遗物中含有52张牌,其组成和西式牌面组成非常相似,而这副牌的花色有硬币、剑、高脚杯、马球棍等,一些学者据此推测,这些纹样被相继保留、转化,先后被发展、丰富为意大利扑克牌中的马球棒、剑、高脚杯和硬币,最后被法国改造成了方便钢板印刷的简单图案“三叶草”“矛头”“阶砖”和“红心”,这些渊源与设计被悉数整合、杂糅,最后成了现代扑克牌中的四种样式:黑桃、梅花、方块和红心。



相对于笼统的传统延续,更多的观点则倾向于历史的影响,即扑克牌背后存在的矛盾博弈和人头牌人物选择的相互关联。简单来说,扑克牌代表的娱乐与赌博,被基督教等众多教义视为罪孽,而对牌面图像的改进,则成了让扑克牌幸存于世的必然之举。其结果是扑克牌和宗教文化的结合,例如法国国王路易十四年轻时,红衣主教就曾在扑克牌上绘制神话、寓言等故事,从而激起对方的学习兴趣。英国也曾如法炮制,在17世纪将扑克牌改造为教育的工具和媒介,通过牌面图案的修改来弱化其赌博属性。更有传言称,20世纪美国传教士以扑克牌解释教义,一度将这种娱乐卡片硬生生变成了“压缩版”《圣经》。


“存世之道”外,宗教元素对牌面的影响也曾被解释为占卜的常态变形。这需要追溯到扑克牌和塔罗牌的历史纠缠,后者是一种流行于15世纪意大利的贵族纸牌游戏,后被发展出占卜用途。研究显示,扑克牌和塔罗牌在一定程度上具有相似性,这也使得“占卜通灵”成为娱乐之外始终伴随扑克牌发展而存在的功能。19世纪开始,有学者就曾在写作中提及纸牌占卜的现象,人们认为52张牌有着丰富的组合可能,占卜师可以借此做出非常多的预测与关系解读。在扑克牌的主要发明地法国,纸牌占卜更是非常流行的社会活动,很多记录显示,许多人曾以此预测个人的命运和走向。这也使得扑克牌天然被赋予了神秘的宗教性质,成为一种可触碰的灵性符号。例如印在17世纪英国特制扑克牌上的德文诗谶就写着:星辰轨迹昭示危机,逆风而行方得生机。


如同这段诗文所描述的,在很长一段时期内,这种空灵缥缈和鲜活现实的碰撞总是很直白地存在于扑克牌穿梭的生活场景,也许这无法决定“大卫王”成为“黑桃K”的结局,却在冥冥之中勾勒着,岁月变迁中,神秘莫测的宗教形象,从神台典籍走向社会人间,重返人心与烟火的浪漫足迹。

 

参考文献:

[1] 翁启宇著,全球史下看中国 从大河文明到地缘文明  约公元前2000年-前950年,上海社会科学院出版社,2023.

[2] [加]戈登・菲 [美]道格拉斯・斯图尔特,《圣经导读下按卷读经》,李瑞萍译,北京大学出版社,2005年版.

[3] 阮炜作,西方文明三千年 从希腊城邦到工业革命,东方出版中心,2023.

[4] 张宝洲,赵蓓著,外国美术史,西北大学出版社,2008.

[5] 龙迪勇,图像与文字的符号特性及其在叙事活动中的相互模仿,《江西社会科学》2010年第11期.

[6] 顾卫民.荷兰海洋帝国史:1581—1800. 

[7] (英)詹姆斯·M.拉塞尔作;鲁晨鹏译,神奇的发明 古人比我们更超前,北京时代华文书局有限公司,2021.06,第40页.


*本文系“国家人文历史”独家稿件,欢迎读者转发朋友圈。



END
作者 | 念缓‍‍‍‍‍‍‍
编辑 | 胡心雅‍
主编 | 周斌
排版 | 曲晨溪(实习)‍
校对 | 彦文
\"图片\"

国历好物
\"图片\"

进击的士族

旁落的皇权

跨越五百年的权力对抗史

国民历史读物《国家人文历史》专业团队

最新力作

展现士族潮起潮落的史诗级历史画卷

\"图片\"


点击下方封面,即可购买



↓↓↓


\"图片\"
\"图片\"

“在看”的永远18岁~

\n \n\n \n
\n\n \n 文章原文\n
\n \n\n \n\n \"\"
", @@ -1794,28 +1750,6 @@ "hash": "018d59f4838b74ac87ba1d824f35bc41", "highlights": [] }, - { - "title": "侯逸凡:最年轻的“棋后”Conquering the world of chess", - "description": "16岁就成为最年轻的国际象棋世界“棋后”,她是真人版的《后翼弃兵》!", - "content": "16岁就成为最年轻的国际象棋世界“棋后”,她是真人版的《后翼弃兵》!", - "category": "人物", - "link": "http://www.i21st.cn/story/3807.html", - "creator": "www.i21st.cn", - "pubDate": "Thu, 1 Sep 2022 17:00:02", - "enclosure": "", - "enclosureType": "", - "image": "", - "id": "", - "language": "zh", - "folder": "双语", - "feed": "21世纪", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "3372e349617e8cdb5732ee4da3cdcdce", - "highlights": [] - }, { "title": "“大老虎”被拿下!中共中央决定对周永康严重违纪问题立案审查", "description": "", @@ -1838,28 +1772,6 @@ "hash": "9f21f8fa51704a71191c0219c9793433", "highlights": [] }, - { - "title": "索契冬奥会李坚柔夺中国首金", - "description": "Short track speed skating", - "content": "Short track speed skating", - "category": "", - "link": "http://www.i21st.cn/article/translate/4927_1.html", - "creator": "www.i21st.cn", - "pubDate": "Fri, 14 Feb 2014 10:50:53", - "enclosure": "", - "enclosureType": "", - "image": "", - "id": "", - "language": "zh", - "folder": "双语", - "feed": "21世纪", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "7371f6dbcab15edaf9642e52b8d5ac96", - "highlights": [] - }, { "title": "BackWay – 一张照片,帮你找到与过往的距离", "description": "与其说 BackWay 帮你找到回去的路,不如说它只是悄悄为你标记下那些曾经踏足的地方,让你知晓自己与过往的距离。 BackWay 是一款简单、小巧的 iPhone 应用,只有 4 MB 大小,它通过 ", @@ -2159,28 +2071,6 @@ "hash": "70391c560d13fa2ded36785145ef6474", "highlights": [] }, - { - "title": "欧公赛决赛的两副牌", - "description": "这副牌非常关键,这是第二节的最后一副,此前土耳其人一路飙升,打出一波凌厉的攻势,将比分从落后近30IMP变为领先15IMP,在上半场结束前的最后一副,齐老板将比分追回13IMP,总比分才落后2分,至关重要。", - "content": "
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n \n \n 新朋友\n \n \n \n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 标兵桥牌\n \n \n \n

\n

\n \n \n 关注微信\n \n \n

\n
\n
\n \n \n 老朋友\n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 右上角\n \n \n \n

\n

\n \n \n 分享本文\n \n \n

\n
\n
\n \n \n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n 下面两副牌,出现在决赛中,先看叫牌问题:\n \n

\n
\n
\n \n
\n
\n \n 北家叫什么\n \n
\n
\n

\n \n 你是北家,叫牌简单的,开叫1NT,同伴2♣,你2♦没有高花,同伴3♦自然,现在你准备怎么叫?\n \n

\n

\n \n 同伴显示♦的同时,还有4张高花,因为他开始用的是斯台曼,无论是4张哪一门高花,如果你叫3NT,都有可能有危险,但你又不能轻易越过3NT,所以你准备怎么办呢?\n \n

\n

\n \n 实战中,土耳其牌手选择叫3NT,全手牌如下:\n \n

\n
\n
\n \n
\n
\n \n 土耳其北家3NT\n \n
\n
\n

\n \n ♥是2-2,不幸的是东家天然做出♥首攻,3NT打不成。\n \n

\n

\n \n 你又能怎么办呢?\n \n

\n

\n \n 来看齐老板的搭档诺沃萨德茨基是怎么处理的:\n \n

\n
\n
\n \n
\n
\n \n 齐老板的搭档叫3♠\n \n
\n
\n

\n \n 诺沃萨德茨基面临的局面与土耳其人一样,但他没选择叫3NT,而是叫个3♠,3♠的好处是,在表示♦有配合的同时,如果同伴的高花是♥,3♠有抑制首攻的作用,而如果同伴的高花是♠,他巧妙地表示了♥的弱点,可谓一举三得。\n \n

\n

\n \n 齐老板知道同伴是3张♠,而且也知道同伴的♥不好,所以选择4♠。\n \n

\n

\n \n 由于将牌分布友好,4♠是个不错的定约,实战中也做成了。\n \n

\n

\n \n 这副牌非常关键,这是第二节的最后一副,此前土耳其人一路飙升,打出一波凌厉的攻势,将比分从落后近30IMP变为领先15IMP,在上半场结束前的最后一副,齐老板将比分追回13IMP,总比分才落后2分,至关重要。\n \n

\n
\n

\n \n 再看首攻问题:\n \n

\n
\n
\n \n
\n
\n \n 你是东家,首攻哪一张\n \n
\n
\n

\n \n 这里的叫牌也是自然性的,2NT是邀请,同伴加倍3NT,你是东家,准备首攻哪一张?\n \n

\n

\n \n 现在要搞清楚对方是什么类型的牌,然后再研究同伴是为何加倍。\n \n

\n

\n \n 同伴有加倍但不能争叫的实力,第一感觉是♦长套,无法叫牌,所以是不是要首攻♦?\n \n

\n

\n \n 不过,一般来说,开叫1♦的人2NT是邀请,如果是18-19点的平均型,应该直接3NT,而如果是♦套,则可以叫3♦,所以2NT大概是(4-3)-5-1或者4-4-4-1、4-4-5-0等♣短的非均型,点力在15-16点之间。\n \n

\n

\n \n 而同伴是♣长套,有没有可能不争叫呢?从你的♣结构来看,也不是不可能,而同伴是♦长套的可能性不高,因为叫1NT的人有♦张数,开叫人也有♦张数,你还有3张,所以综合考虑,同伴是♣的可能性大。\n \n

\n

\n \n 实战中, 卡利塔首攻♦,全手牌如下:\n \n

\n
\n
\n \n
\n
\n \n 卡利塔首攻♦,轻松超一\n \n
\n
\n

\n \n ♦的位置不利,定约人轻松超一。虽然首攻♣也无法击败定约,但只能说是卡酷司机加倍太激进了,而他所希望的,则是应该攻♣。\n \n

\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n

\n \n 相关链接:\n \n 齐老板首夺欧洲公开桥牌锦标赛冠军\n \n \n

\n

\n \n 相关链接:\n \n 齐老板与土耳其进入决赛\n \n \n

\n

\n \n 相关链接:\n \n 瑞波土法打进欧公赛半决赛\n \n \n

\n

\n \n 相关链接:\n \n 当舒柏林遇见舒柏林\n \n \n

\n

\n \n 相关链接:\n \n 芬奇压哨淘汰罗森塔尔\n \n \n

\n

\n \n 相关链接:\n \n 进攻性叫牌与防守性叫牌——波兹南的牌局\n \n \n

\n

\n \n 相关链接:\n \n 技惊四座的Deep Finesse打法\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛第一阶段结束、弗姆队获得欧洲混团冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲混团赛场上一副牌的两次安全打法\n \n \n

\n

\n \n 相关链接:\n \n 第11届欧洲公开桥牌锦标赛混团预赛结束\n \n \n

\n

\n \n 相关链接:\n \n 第11届欧洲桥牌公开锦标赛\n \n \n

\n

\n \n 相关链接:\n \n 2023年欧洲公开桥牌锦标赛每日一牌——韦尔萨切的失误,还是对手的高明?\n \n \n

\n

\n \n 相关链接:\n \n 第10届欧洲桥牌公开锦标赛\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛结束\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开赛结束、土耳其人获得公开双人赛冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲冬季运动会结束,奥卡队获得冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲冬季桥牌运动会开赛在即\n \n \n

\n

\n \n 相关链接:\n \n 劳伦特当选欧洲桥联主席、期待开启新时代\n \n \n \n
\n
\n

\n

\n \n 相关链接:\n \n 欧洲桥牌新纪元——欧洲桥牌团体锦标赛回顾\n \n \n

\n
\n
\n
\n
\n
\n

\n \n 往期推荐\n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n

\n \n \n 2022年欧洲桥牌团体锦标赛即将开赛\n \n \n

\n
\n
\n
\n
\n

\n \n \n 荷兰加冕欧洲之王\n \n \n

\n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n \n \n \n 点击\n \n \n \n \n 阅读原文\n \n \n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n \n 别忘了点个在看哦!转发那就太好了!\n \n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/y-1dlbaX_Ochsj5wyC8RzA", - "creator": "标兵桥牌", - "pubDate": "Tue, 08 Jul 2025 00:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDQoJpic0ibsIxaF7GAQVwVTT6eGyFJ2u6u8uRPPuRgRemxz9bYpyyQ9DDobFOOPU8ndsqr7ibKoF7EOQ/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDQoJpic0ibsIxaF7GAQVwVTT6eGyFJ2u6u8uRPPuRgRemxz9bYpyyQ9DDobFOOPU8ndsqr7ibKoF7EOQ/0?wx_fmt=jpeg", - "id": "2247512705_1", - "language": "", - "folder": "桥牌", - "feed": "标兵1桥牌", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "6c66baaf54362bf7e0ae12c2ceeae462", - "highlights": [] - }, { "title": "齐老板与土耳其进入决赛", "description": "欧洲公开桥牌锦标赛公团结束1/4淘汰赛,齐老板的瑞士队、波兰的Naukabrydzapl队、土耳其的JEO Hidro队、法国绿队进入半决赛。对于一个有123个队参加的跨国比赛来说,最终的四强恰好均由同一国家团队组成,是很少见的。", @@ -2203,28 +2093,6 @@ "hash": "f8a177f20f224372b56418786352912a", "highlights": [] }, - { - "title": "当舒柏林遇见舒柏林", - "description": "荷兰小将是舒柏林巴司机的晚辈,各种技巧自然也学得滚瓜纯属,德莱昂在单方无局第三家,觉得开叫3♥没什么用,于是祭起了舒柏林的独门法宝,没想到舒柏林遇见舒柏林,马上被舒柏林识破,可惜的是舒柏林没能完成3NT,他还是失败了。", - "content": "
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n \n \n 新朋友\n \n \n \n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 标兵桥牌\n \n \n \n

\n

\n \n \n 关注微信\n \n \n

\n
\n
\n \n \n 老朋友\n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 右上角\n \n \n \n

\n

\n \n \n 分享本文\n \n \n

\n
\n
\n \n \n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n 欧洲公开桥牌锦标赛公团比赛7月2日产生8强,在介绍比赛进程之前,先看下面这副牌:\n \n

\n
\n
\n \n
\n
\n \n 没轮到你,上家先开叫2NT\n \n
\n
\n

\n \n 两家Pass,东家开叫2NT,20-21点,你是南家,如何考虑叫牌?提示一下,东家是瑞士荷兰世界冠军舒柏林。\n \n

\n
\n

\n \n 回到比赛介绍。7月2日的16强战各有惊喜,其中最大的冷门是中国台北/新加坡华人队击败意大利国手组成的芬奇队。昨天我们的文章刚刚介绍过,芬奇队通过艰难的预赛打进32强,淘汰赛击败2号种子罗森塔尔队,正准备大展身手之际,遇到了中国台北/新加坡联军阿曼达队,第一节阿曼达取得优势开局,后面也没给对方机会,一共赢了39IMP,即使不算芬奇队客户所上的两节,阿曼达队与意大利国手对抗的后两节也获得了总分2IMP的优势。看起来现在亚太地区的桥牌水平正在大步提升,非常接近了世界级水平。\n \n

\n

\n \n 其他场次的比赛是齐老板战胜荷兰橙队,保加利亚战胜印度/瑞典联队,两个波兰队,美国的舒里队,法国绿队,土耳其JEO队一起晋级。\n \n

\n

\n \n 女子团体半决赛结束,来自意大利的Tersch队和美国/波兰联军Parker队进入决赛。\n \n

\n

\n \n 根据欧洲桥联的报道,未来5年,即2026-2030年的欧洲冬季跨国桥牌锦标赛将落户捷克布拉格,这是欧洲桥联首次将同一比赛连续在同一举办地举办。\n \n

\n
\n

\n \n 下面回到刚才的牌局:\n \n

\n
\n
\n \n
\n
\n \n 没轮到你,上家开叫2NT\n \n
\n
\n

\n \n 南家是开叫2NT的牌,但东家在前面先开叫了,你会不会认为东家19点进行了升级,也就是你和东家有40点。不过由你首攻,你肯定能够击败2NT,你是否要行动呢?\n \n

\n
\n
\n \n
\n
\n \n 南家舒柏林询问西家,2NT开叫是什么牌?\n \n
\n
\n

\n \n 这是齐老板队与荷兰橙队的比赛,东家并不是舒柏林,南家才是舒柏林,他询问西家,开叫2NT的情况,西家斯普林克许泽对他说,\n \n “你开叫2NT是什么牌,他就是什么牌!”\n \n \n

\n
\n

\n \n 于是舒柏林加倍,叫牌过程如下发展:\n \n

\n
\n
\n \n
\n
\n \n 舒柏林加倍,东家跑3♥,舒柏林3NT\n \n
\n
\n

\n \n 东家是荷兰小将盖伊门德斯德莱昂,这对小将曾经在2022年的罗森布卢姆杯上表现出色,打进了决赛。\n \n

\n
\n
\n \n
\n
\n \n 巴司机微笑问德莱昂,”你学的不错啊“\n \n
\n
\n

\n \n 舒柏林加倍他的2NT,德莱昂跑到3♥,舒柏林自己叫3NT,西家首攻♥6,明手的牌摊下来,舒柏林心说:\n \n “别跟我来这套,这是我玩剩下的!”\n \n \n

\n

\n \n 舒柏林忍让♥,第二墩西家告缺,果然东家是凭♥长套诈叫2NT的,现在舒柏林已有8墩,第9墩如何而来?\n \n

\n

\n \n 舒柏林吃进第三墩♥,西家垫2张♦,明手垫♠,舒柏林兑现4圈♦和1墩♣,东家只有2张♦,垫了2张♣。现在怎么打?\n \n

\n
\n
\n \n
\n
\n \n 是飞♠还是不飞\n \n
\n
\n

\n \n 已知东家有6张♥,跟出2张♦和3张♣,还有2张牌。如果东家有♠K,则拔♣看看他是否还有♣,如果没有了,就是2张♠,他不能垫♠,用♥投入东家即可,但如果东家跟出♣,他就只有1张♠,这张♠是K还是小,就需要猜断了,舒柏林决定打东家单张♠,于是他出小♠到A,准备送给西家:\n \n

\n
\n
\n \n
\n
\n \n 舒柏林没打对\n \n
\n
\n

\n \n 可惜东家有♠K,舒柏林的打法失败了。\n \n

\n

\n \n \n “难道你开叫2NT还有♠K吗?这一点也不正宗啊!”\n \n 舒柏林瞪着眼睛对他的小兄弟说。\n \n

\n

\n \n \n 用2NT开叫做诈叫,是舒柏林的标志性叫牌之一\n \n ,他们这一叫牌,是写进约定卡的。在2024年雷辛格尔杯的决赛中,舒柏林就在我们桌上表演了一次,可惜被睿智的张永革识破,请看:\n \n

\n

\n \n \n \n 博取最佳——雷辛格尔杯之路——齐老板与尼老板(三)\n \n \n \n 之识破舒柏林的诡计\n \n \n

\n

\n \n 荷兰小将是舒柏林巴司机的晚辈,各种技巧自然也学得滚瓜纯属,德莱昂在单方无局第三家,觉得开叫3♥没什么用,于是祭起了舒柏林的独门法宝,没想到\n \n \n \n 舒柏林遇见舒柏林\n \n \n \n ,马上被舒柏林识破,可惜的是舒柏林没能完成3NT,他还是失败了。\n \n

\n

\n \n 从打法上看,舒柏林忽视了德莱昂♣的垫牌顺序,他表示的是3张信号,所以舒柏林如果相信他的小兄弟,应该拔♣,东家没有♣,他只能垫成单张♠K,舒柏林再考虑拔♠A。但他不能这么打的原因,是因为多忍让了一轮♥,送♠就会有1个♠、2个♥、2个♣输墩;如果他第二墩♥就停住,最后就可以安全地拔♠A,最多输1墩♠、1墩♥、2墩♣。\n \n

\n

\n \n 舒柏林还是没把握德莱昂的2NT到底有多少张♥。\n \n

\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n

\n \n 相关链接:\n \n 芬奇压哨淘汰罗森塔尔\n \n \n

\n

\n \n 相关链接:\n \n 进攻性叫牌与防守性叫牌——波兹南的牌局\n \n \n

\n

\n \n 相关链接:\n \n 技惊四座的Deep Finesse打法\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛第一阶段结束、弗姆队获得欧洲混团冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲混团赛场上一副牌的两次安全打法\n \n \n

\n

\n \n 相关链接:\n \n 第11届欧洲公开桥牌锦标赛混团预赛结束\n \n \n

\n

\n \n 相关链接:\n \n 第11届欧洲桥牌公开锦标赛\n \n \n

\n

\n \n 相关链接:\n \n 2023年欧洲公开桥牌锦标赛每日一牌——韦尔萨切的失误,还是对手的高明?\n \n \n

\n

\n \n 相关链接:\n \n 第10届欧洲桥牌公开锦标赛\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛结束\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开赛结束、土耳其人获得公开双人赛冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲冬季运动会结束,奥卡队获得冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲冬季桥牌运动会开赛在即\n \n \n

\n

\n \n 相关链接:\n \n 劳伦特当选欧洲桥联主席、期待开启新时代\n \n \n \n
\n
\n

\n

\n \n 相关链接:\n \n 欧洲桥牌新纪元——欧洲桥牌团体锦标赛回顾\n \n \n

\n
\n
\n
\n
\n
\n

\n \n 往期推荐\n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n

\n \n \n 2022年欧洲桥牌团体锦标赛即将开赛\n \n \n

\n
\n
\n
\n
\n

\n \n \n 荷兰加冕欧洲之王\n \n \n

\n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n \n \n \n 点击\n \n \n \n \n 阅读原文\n \n \n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n \n 别忘了点个在看哦!转发那就太好了!\n \n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/GYUyZ7Na50SVMA9upv-n5A", - "creator": "标兵桥牌", - "pubDate": "Fri, 04 Jul 2025 00:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDQoJpic0ibsIxaF7GAQVwVTT6eGyFJ2u6u8uRPPuRgRemxz9bYpyyQ9DDobFOOPU8ndsqr7ibKoF7EOQ/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDQoJpic0ibsIxaF7GAQVwVTT6eGyFJ2u6u8uRPPuRgRemxz9bYpyyQ9DDobFOOPU8ndsqr7ibKoF7EOQ/0?wx_fmt=jpeg", - "id": "2247512674_1", - "language": "", - "folder": "桥牌", - "feed": "标兵1桥牌", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "5e7f7ba8baf49614c379c8f6b16dd620", - "highlights": [] - }, { "title": "进攻性叫牌与防守性叫牌——波兹南的牌局", "description": "桥牌其实也一样,只不过桥牌的“进攻”和“防守”,有多重的意义,除了简单的在桥牌打法中表现为做庄与防守外,还有在叫牌中也分为自己抢过来打的“进攻性叫牌”和放给对方打或者叫得较低的“防守性叫牌”。", @@ -2247,28 +2115,6 @@ "hash": "8cf9f3254a25c91aade7bc229bc8c13b", "highlights": [] }, - { - "title": "欧洲公开桥牌锦标赛第一阶段结束、弗姆队获得欧洲混团冠军", - "description": "6月27日,正在波兰波兹南进行的第11届欧洲公开桥牌锦标赛第一阶段比赛结束,弗姆队获得混合团体冠军,11Jaszczak队获得老年团体冠军,埃德蒙斯队获得混合一副一比团体冠军,Narkiewicz/Czajka获得混合双人赛冠军。", - "content": "
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n \n \n 新朋友\n \n \n \n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 标兵桥牌\n \n \n \n

\n

\n \n \n 关注微信\n \n \n

\n
\n
\n \n \n 老朋友\n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 右上角\n \n \n \n

\n

\n \n \n 分享本文\n \n \n

\n
\n
\n \n \n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n 6月27日,正在波兰波兹南进行的第11届欧洲公开桥牌锦标赛第一阶段比赛结束,弗姆队(Ferm)获得混合团体冠军,11Jaszczak队获得老年团体冠军,埃德蒙斯队(Edmonds)获得混合一副一比团体冠军,Narkiewicz/Czajka获得混合双人赛冠军。\n \n

\n
\n
\n \n
\n
\n \n Ferm队获得混合团体赛冠军\n \n
\n
\n
\n
\n \n
\n
\n \n 老年团体赛前三名\n \n
\n
\n

\n \n 欧洲公开桥牌锦标赛混合团体赛共有94队参加,10轮瑞士移位赛过后,前32强进入淘汰赛,淘汰赛每天一轮,每轮56副牌分4节进行,经过5天的争夺,2022年世界桥牌综合锦标赛混合团体赛巴厘杯冠军弗姆队决赛战胜阿什队(Ashe)夺得冠军。\n \n

\n

\n \n 弗姆队由美国客户老太太弗姆组建,核心成员是来自瑞士的巴司机和舒柏林,还有丹麦美女马德森,3年前他们在波兰夺得世界冠军,如今再返波兰又夺欧洲冠军,看起来波兰是老太太的福地。与3年前的阵容相比,弗姆队更换了一对牌手,由瑞典美女格隆奎斯特和意大利的曼诺,替换了德国名将阿尼姆与荷兰的魏斯,显然更年轻的情侣表现更好,半决赛他们击败了阿尼姆与魏斯的Yoshka队,整个比赛格隆奎斯特/曼诺的表现都很抢眼,冠军实至名归。\n \n

\n
\n
\n \n
\n
\n \n Ferm队的情侣组合半决赛战胜他们的前任\n \n
\n
\n

\n \n 从6月28日开始,欧公赛进入第二阶段,重头戏公开团体赛开赛,目前共有123个队报名,超过了2017年意大利蒙特卡蒂尼121个队,也超过了2011年在波兹南的第5届120个队,成为此项比赛参赛队数最多的一次。比赛先进行3天15轮的瑞士移位赛,前32队进入淘汰赛,淘汰赛每天一场,7月5日结束。\n \n

\n

\n \n 公开团体赛才是真正的重头戏,欧洲强队以及当打知名牌手均出场亮相,这是百慕大杯赛前一次非常好的热身和预演,对于中国队的备战也很重要。\n \n

\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n

\n \n 相关链接:\n \n 欧洲混团赛场上一副牌的两次安全打法\n \n \n

\n

\n \n 相关链接:\n \n 第11届欧洲公开桥牌锦标赛混团预赛结束\n \n \n

\n

\n \n 相关链接:\n \n 第11届欧洲桥牌公开锦标赛\n \n \n

\n

\n \n 相关链接:\n \n 2023年欧洲公开桥牌锦标赛每日一牌——韦尔萨切的失误,还是对手的高明?\n \n \n

\n

\n \n 相关链接:\n \n 第10届欧洲桥牌公开锦标赛\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛结束\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开赛结束、土耳其人获得公开双人赛冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲冬季运动会结束,奥卡队获得冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲冬季桥牌运动会开赛在即\n \n \n

\n

\n \n 相关链接:\n \n 劳伦特当选欧洲桥联主席、期待开启新时代\n \n \n \n
\n
\n

\n

\n \n 相关链接:\n \n 欧洲桥牌新纪元——欧洲桥牌团体锦标赛回顾\n \n \n

\n
\n
\n
\n
\n
\n

\n \n 往期推荐\n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n

\n \n \n 2022年欧洲桥牌团体锦标赛即将开赛\n \n \n

\n
\n
\n
\n
\n

\n \n \n 荷兰加冕欧洲之王\n \n \n

\n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n \n \n \n 点击\n \n \n \n \n 阅读原文\n \n \n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n \n 别忘了点个在看哦!转发那就太好了!\n \n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/EaSBtOh1aMr3oPsaJMlDHw", - "creator": "标兵桥牌", - "pubDate": "Sat, 28 Jun 2025 12:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDQoJpic0ibsIxaF7GAQVwVTT6eGyFJ2u6u8uRPPuRgRemxz9bYpyyQ9DDobFOOPU8ndsqr7ibKoF7EOQ/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDQoJpic0ibsIxaF7GAQVwVTT6eGyFJ2u6u8uRPPuRgRemxz9bYpyyQ9DDobFOOPU8ndsqr7ibKoF7EOQ/0?wx_fmt=jpeg", - "id": "2247512647_1", - "language": "", - "folder": "桥牌", - "feed": "标兵1桥牌", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "8b7e76707b846119bb7a39057abcdd4f", - "highlights": [] - }, { "title": "第11届欧洲公开桥牌锦标赛混团预赛结束", "description": "欧洲桥牌公开锦标赛每两年举办一次,从2003年到2019年共举办了9届,但由于疫情原因2021年中断,2023年再度恢复,今年是第11届比赛,本次比赛将于6月21日-7月5日在波兰波茨南举办,波茨南曾经于2011年举办过欧洲桥牌公开锦标赛。", @@ -2291,28 +2137,6 @@ "hash": "926d81696677395d95a23772a9ea152c", "highlights": [] }, - { - "title": "世界桥联公布2024年大师分排行榜", - "description": "2024年底,世界桥联公布了世界桥牌运动会结束后世界桥联大师分的排行榜。牌坛奇人齐老板仍然领跑公开组,这已经是他自从2021年登顶之后,连续第四年蝉联世界桥联大师分第一人的宝座了。女子排行榜、老年排行榜、混合排行榜的榜首也各有其主。", - "content": "
\n
\n
\n
\n
\n
\n \n \n 新朋友\n \n \n \n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 标兵桥牌\n \n \n \n

\n

\n \n \n 关注微信\n \n \n

\n
\n
\n \n \n 老朋友\n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 右上角\n \n \n \n

\n

\n \n \n 分享本文\n \n \n

\n
\n
\n \n \n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n 2024年底,世界桥联公布了世界桥牌运动会结束后世界桥联大师分的排行榜。\n \n

\n

\n \n 牌坛奇人齐老板仍然领跑公开组,这已经是他自从2021年登顶之后,连续第四年蝉联世界桥联大师分第一人的宝座了。女子大师分排行榜、老年大师分排行榜、混合大师分排行榜的榜首,分别是瑞典、丹麦、法国牌手。\n \n

\n

\n \n 下面是公开组前40名牌手的排名:\n \n

\n
\n
\n \n
\n
\n \n 2024年世界桥联公开组大师分排行榜\n \n
\n
\n

\n \n 我们能够看到,排在前六名的是齐刷刷的瑞士队牌手,其中齐老板高居第一,超过了第二名卡酷司机近500分,相当于一个世界季军的名次差距。最晚加入瑞士队的诺沃萨德茨基分数最低,但也比第7名的海尔格莫多了100多分。\n \n

\n

\n \n 瑞士大鳄从2021年开始在世界牌坛的兴风作浪,先后拿到了2022年、2023年百慕大杯冠军,2024年世界桥牌运动会季军,年年登上世界锦标赛的领奖台,大师分增值无人能比。\n \n

\n

\n \n 瑞士队之后的排名相对分散,原因是这些牌手无法在世界比赛中集中火力,形成统一团队竞争。\n \n

\n

\n \n 其中小海借助早年在摩纳哥队的成绩,加上近年代表挪威,还有去年世界桥牌运动会跨国团体赛的冠军,得以领跑除瑞士团队的世界牌手。\n \n

\n

\n \n 小海之下,是波兰队的新科世界冠军布雷茨排在第8名,布雷茨在阿根廷拿到了世界桥牌运动会冠军后,加上他2019年百慕大杯冠军、2022年世界桥牌双人赛冠军,他已经成为波兰唯一的大三冠成员,波兰牌手即使强如加夫雷西、卡利塔、卡酷司机,也没有拿到3种世界冠军的牌手。\n \n

\n

\n \n 再往下排名,是第9名和第12名意大利的韦尔萨切与塞门塔。自从劳爷退休后,“小沃”的发小塞门塔成为他的新搭档,两人很快进入状态,成为意大利队的新台柱,先后拿下2023年百慕大杯季军和2024年世界桥牌运动会亚军,也在世界桥联世界桥牌巡回赛的积分榜上连续两年排名第一。\n \n

\n

\n \n 插在两位意大利牌手中间的是荷兰明星穆勒和魏斯,他们虽然从2024年开始退出了荷兰国家队,但凭借2019年、2022年百慕大杯的亚军、2022年欧锦赛冠军等成绩,仍然领跑荷兰牌手。不过老穆勒与魏斯的状态从2023年开始急剧下滑,已有逐步退出世界级牌手的趋势,最近他们参加了中国的A类俱乐部联赛。\n \n

\n

\n \n 美国的莫斯排在12名,超过了14名和15名的尼老板与搭档凯茨,波兰老将加夫雷西退出瑞士队后,仍然依靠此前的积累,排在第13名。\n \n

\n

\n \n 新西兰的威布利(Michael Whibley)占据了第一页的位置,他排在第17名,是亚太地区排名最高的牌手,得益于新西兰最近不错的成绩,以及去年世界桥牌运动会跨国团体赛的亚军。\n \n

\n

\n \n 在第一页位置的,多数是2024年世界桥牌运动会成绩不错的牌手。\n \n

\n

\n \n 中国牌手排名最高的是现役国家队队员连勇,他以971.75分排在第81名,刚好超过劳爷1.05分。\n \n

\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n

\n \n 相关链接:\n \n 世界桥联大师分排行榜齐老板遥遥领先\n \n \n

\n

\n \n 相关链接:\n \n 世界桥联公报最新大师分、齐老板坐稳世界第一\n \n \n

\n

\n \n 相关链接:\n \n 世界桥联最新大师分排行榜、摩纳哥众将排名靠前\n \n \n

\n

\n \n 相关链接:\n \n 世界桥联公布最新大师分、Helgemo登顶世界第一人\n \n \n

\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n 往期推荐\n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n

\n \n \n 世界桥联大师分介绍及排行榜解读\n \n \n

\n
\n
\n
\n
\n

\n \n \n 世界桥联大师分介绍及排行榜解读(二)男子特级大师排行榜解读\n \n \n

\n
\n
\n
\n
\n

\n \n \n 世界桥联大师分介绍及排行榜解读(三)世界桥联女子及中国牌手排行榜\n \n \n

\n
\n
\n
\n
\n

\n \n \n 世界桥联大师分介绍及排行榜解读(四)世界桥联大师分政策和等级制度的变迁对中国牌手的影响\n \n \n

\n
\n
\n
\n
\n

\n \n \n 一览众山小——齐老板登顶世界第一之路\n \n \n

\n
\n
\n
\n
\n

\n \n \n 一览众山小——齐老板登顶世界第一之路(二)\n \n \n

\n
\n
\n
\n
\n

\n \n \n 一览众山小——齐老板登顶世界第一之路 (续完)\n \n \n

\n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n \n \n \n 点击\n \n \n \n \n 阅读原文\n \n \n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n \n 别忘了点个在看哦!转发那就太好了!\n \n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/i8ozGVJzORLNLKLSgY9RYg", - "creator": "标兵桥牌", - "pubDate": "Mon, 16 Jun 2025 00:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDSibjfuNWY4LAB2pN77qbPPaynicibuLW2nEp8bqVrvuouOCgYlbWfciaLXGkM5VzZpZdpb8Z3HAAS28A/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDSibjfuNWY4LAB2pN77qbPPaynicibuLW2nEp8bqVrvuouOCgYlbWfciaLXGkM5VzZpZdpb8Z3HAAS28A/0?wx_fmt=jpeg", - "id": "2247512614_1", - "language": "", - "folder": "桥牌", - "feed": "标兵1桥牌", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "d493b9eff95dc7208df749197028ae29", - "highlights": [] - }, { "title": "美国国家队名单出炉", "description": "5月26日当地时间,美国国家队选拔赛老年团体赛2025年6月1日,美国桥牌协会(USBF)公布了参加2025年世界桥牌团体锦标赛的8支美国队参赛名单。由于美国选拔赛允许一名选手报名多项比赛,因此在选拔赛全部结束后,各个美国国家队进行了调整。", @@ -2335,28 +2159,6 @@ "hash": "904cfda1cd5a396451f15015355b3b1e", "highlights": [] }, - { - "title": "亚太赛每日一牌回顾:一副小牌的攻防与叫牌", - "description": "亚太锦标赛结束了,中国男队夺得冠军,在比赛期间所写的“每日一牌”,比赛期间并没有发出,从今天开始,给桥友们提供迟到的每日一牌。", - "content": "
\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n 新朋友\n \n \n \n \n \n
\n

\n \n \n 点击\n \n \n \n 标兵桥牌\n \n \n \n

\n

\n \n \n 关注微信\n \n \n

\n
\n \n \n 老朋友\n \n \n
\n

\n \n \n 点击\n \n \n \n 右上角\n \n \n \n

\n

\n \n \n 分享本文\n \n \n

\n
\n \n \n \n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n \n 亚太锦标赛结束了,中国男队夺得冠军,在比赛期间所写的“每日一牌”,比赛期间并没有发出,从今天开始,给桥友们提供迟到的每日一牌。\n \n \n

\n
\n

\n \n 5月23日每日一牌:一副小牌的攻防与叫牌\n \n

\n

\n \n 桥友们大多数喜欢大牌,满贯、高阶竞叫、加倍等等,因为大牌来的分数多,让人们看的热血沸腾。相比来说,小牌就很乏味,小牌攻防环节多,也比较复杂,产生的输赢较小,所以通常不被重视。\n \n

\n

\n \n 不过小牌同样重要,小牌产生的输赢,也是非常可观的。\n \n

\n

\n \n 下面这副牌是第二循环第11轮,中国对澳大利亚,此前澳大利亚领先中国,这副小牌在这场双VP的比赛中显得异常重要。\n \n

\n
\n
\n \n
\n
\n \n 东家主打2♣\n \n
\n
\n

\n \n 我是东家,开叫2♣成为最后定约,南家首攻♦A,怎么考虑?\n \n

\n

\n \n 联手有4-4的♠配合,但却难以找到,这是强梅花体系中的2♣开叫所带来不利之处,但好处是对方的8张♥配合也找不到了。从牌面上看,可能的输张有2墩♠、2墩♥、2墩♦、1墩♣,如果♠J、♥A、♣Q的位置有利,也是极有希望做成定约的。\n \n

\n

\n \n 南北方使用反式张数和姿态信号,在有将定约中,他们主要使用张数信号,南家的♦A后,同伴跟3,他改为拔♥A,解决了我的♥问题,北家跟8,然后南家再打♥,我的K吃住。\n \n

\n

\n \n 现在出♣J飞牌,南家有Q,吃住后,他略作思考,又出♣给明手,现在局面如下:\n \n

\n
\n
\n \n
\n
\n \n 现在明手出牌\n \n
\n
\n

\n \n 明手最后一个桥被用掉了,已经输了3墩,还有一个♦要输,看起来就要马上决战♠了,最大的概率就是直接出10飞J,你准备这样打吗?\n \n

\n
\n

\n \n 实战中,我将吃第三张♥,然后清将牌,南家有4张,我把他全清光,明手垫♠,手中送出♦J,南家吃住,全手牌如下:\n \n

\n
\n
\n \n
\n
\n \n 南家被投入,只能再拿一墩♠A了\n \n
\n
\n

\n \n 南家只有♠和♦可出了,无论出哪一门,我都只输一墩♠A,完成定约。我为何要这样打,因为南家的防守清楚地告诉我,所有大牌都在他手里,我不能飞♠J,而他的牌型,我也判断他没有很长的套,因为如果有长套,他很可能在2♣后争叫,他的牌很像现在这个牌型。\n \n

\n

\n \n 如果南家在我的♦J不出K如何?我可以明手的Q超打,然后再出小♦,送给南家,还原为实战中的情况。\n \n

\n
\n
\n \n
\n
\n \n 不用♦K吃也只是延缓被投入\n \n
\n
\n
\n

\n \n 值得一提的是叫牌,现在很多打精确法的牌手都不愿意开叫2♣,但有所不知2♣也并非都是弱点,也有它的长处,这副牌在另一室东家开叫10-13点的1NT,南家加倍,然后南北方抢到♥定约做成140分。假如东家开叫自然法的1♣或者精确法的1♦,南家都是争叫1NT,北家转移,东西方很难再行动抢出♠定约,实战中,有8桌都是南北方打♥定约吃到140分,而东西方打2♣的,只有我这一桌。\n \n

\n

\n \n \n
\n
\n
\n

\n
\n \n \n
\n

\n \n \n
\n
\n
\n

\n

\n \n \n

\n
\n
\n
\n
\n \n \n \n \n \n 点击\n \n \n \n \n 阅读原文\n \n \n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n \n 别忘了点个在看哦!转发那就太好了!\n \n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n \n \n
\n
\n
\n

\n \n
\n
\n

\n

\n \n 相关链接:\n \n 亚太赛每日一牌回顾:满贯要有想象力\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛每日一牌回顾:剥光投入显身手\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛每日一牌回顾:连氏自然轻松满贯\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛每日一牌回顾:心中有挡、无将不慌\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛每日一牌回顾:精彩的四红心\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛每日一牌回顾:有趣的四红心\n \n \n

\n

\n \n 相关链接:\n \n 风雨十四载、再夺亚太冠\n \n \n

\n

\n \n 相关链接:\n \n 荏苒十四载、再战亚太赛\n \n \n

\n

\n \n 相关链接:\n \n 十四年归来仍是追风少年\n \n \n

\n

\n \n 相关链接:\n \n 曼谷重启亚太大赛\n \n \n

\n

\n \n 相关链接:\n \n 第53届亚太桥牌锦标赛落幕,欧公赛决赛法国内战\n \n \n

\n

\n \n 相关链接:\n \n 中国桥牌队亚太锦标赛大四喜,欧公赛女子团体冠军产生\n \n \n

\n

\n \n 相关链接:\n \n 欧公赛公团齐老板被淘汰、亚太赛迎来最后高潮\n \n \n

\n

\n \n 相关链接:\n \n 欧公赛公团结束首轮淘汰赛、亚太赛接近尾声\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛公团预赛结束、亚太赛完成上半程比赛\n \n \n

\n

\n \n 相关链接:\n \n 亚太桥牌锦标赛结束女团、混团第一循环比赛\n \n \n \n
\n
\n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛公团女团开赛,亚太赛中国占据三项第一\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛第一天中国女队占优,欧洲公开桥牌锦标赛混团冠军产生\n \n \n

\n

\n \n 相关链接:\n \n 亚太桥牌锦标赛开幕,欧洲公开桥牌锦标赛产生老年团体冠军\n \n \n

\n

\n \n 相关链接:\n \n 第53届亚太桥牌锦标赛即将开赛\n \n \n

\n

\n \n 相关链接:\n \n 第四届亚洲杯结束、中国香港捧起“宁波杯”\n \n \n

\n

\n \n 相关链接:\n \n 2022年亚洲杯开赛\n \n \n

\n

\n \n 相关链接:\n \n 第三届亚洲杯桥牌赛开幕\n \n \n

\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/mOns8vzMGRhHVuKF9DajUQ", - "creator": "标兵桥牌", - "pubDate": "Wed, 04 Jun 2025 00:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDTH0sTiajz6mMGDUjHnftbibdib7FEvMaiahjxG8C2mKODhpuRaUtBEnLTyDUl3T8iciaqCAYSUSymeB1ZA/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDTH0sTiajz6mMGDUjHnftbibdib7FEvMaiahjxG8C2mKODhpuRaUtBEnLTyDUl3T8iciaqCAYSUSymeB1ZA/0?wx_fmt=jpeg", - "id": "2247512595_1", - "language": "", - "folder": "桥牌", - "feed": "标兵1桥牌", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "30306c42a579ceff24b8e7ac06bc2a90", - "highlights": [] - }, { "title": "亚太赛每日一牌回顾:剥光投入显身手", "description": "亚太锦标赛结束了,中国男队夺得冠军,在比赛期间所写的“每日一牌”,比赛期间并没有发出,从今天开始,给桥友们提供迟到的每日一牌。", @@ -2379,28 +2181,6 @@ "hash": "1477f7b86cceef32ddd423d3016b516f", "highlights": [] }, - { - "title": "亚太赛每日一牌回顾:心中有挡、无将不慌", - "description": "亚太锦标赛结束了,中国男队夺得冠军,在比赛期间所写的“每日一牌”,比赛期间并没有发出,从今天开始,给桥友们提供迟到的每日一牌。", - "content": "
\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n 新朋友\n \n \n \n \n \n
\n

\n \n \n 点击\n \n \n \n 标兵桥牌\n \n \n \n

\n

\n \n \n 关注微信\n \n \n

\n
\n \n \n 老朋友\n \n \n
\n

\n \n \n 点击\n \n \n \n 右上角\n \n \n \n

\n

\n \n \n 分享本文\n \n \n

\n
\n \n \n \n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n \n 亚太锦标赛结束了,中国男队夺得冠军,在比赛期间所写的“每日一牌”,比赛期间并没有发出,从今天开始,给桥友们提供迟到的每日一牌。\n \n \n

\n
\n

\n \n 5月19日每日一牌:心中有挡、无将不慌\n \n

\n

\n \n 桥友们都知道著名的哈曼规则第一条:如果3NT是一个可以选择的定约,就叫3NT。\n \n

\n

\n \n 套用我们的俗话,叫做:走过路过,不要错过3NT。\n \n

\n

\n \n 当我们在叫牌中可以选择时,3NT是优选,这个很容易理解。但是,如果看上去,3NT并不是看上去一个可以选择的定约时,还能选3NT吗?\n \n

\n

\n \n 中国男队的蔡阳,在下面这副牌的处理,就给桥友们做了一个榜样:\n \n

\n
\n
\n \n
\n
\n \n 南家如何选择叫品?\n \n
\n
\n

\n \n 西家开叫3♥,两个Pass转到南家,选择什么叫品?\n \n

\n

\n \n 可供选择的大概有:Pass,加倍,4♦,3NT。\n \n

\n

\n \n 没有一个叫品是舒服的:\n \n

\n

\n \n · Pass太委屈,自己有15点,对方阻击叫,如果同伴有一些点力,就被对方的阻击叫拦住了,绝不情愿。\n \n

\n

\n \n · 加倍的问题是♠太短,同伴如果叫3♠,还是没解决任何问题,如果跳叫4♠,也很不舒服。\n \n

\n

\n \n · 4♦的好处是叫出了自己最长最强的花色,但这牌要打什么呢?5♦太远,4♦都有可能打不成。\n \n

\n

\n \n · 3NT不可选的原因是对方长套花色没有挡张,但除了这一缺点,就没有任何缺陷了。点力足够,赢墩也有,至于挡张嘛…… 管它呢,别忘了你还有个同伴,同伴说不定能帮你挡住,就算同伴挡不住,没准西家间张结构还不攻呢。\n \n

\n
\n

\n \n 蔡阳选择3NT,他成功了吗?\n \n

\n

\n \n 全手牌如下:\n \n

\n
\n
\n \n
\n
\n \n 3NT是最佳定约\n \n
\n
\n

\n \n 这是中国男队第一循环对阵中国香港的一副牌,实战中西家首攻♠Q,蔡阳先取6墩♠,东家放弃了♦,蔡阳再拿6墩♦,加上♥A,全卷13墩。\n \n

\n

\n \n 看,西家♥连接张都没首攻,假如明手的♥是Kx或者什么都没有,3NT都做成啦!\n \n

\n

\n \n \n
\n
\n
\n

\n
\n \n \n
\n

\n \n \n
\n
\n
\n

\n
\n
\n
\n
\n \n \n \n \n \n 点击\n \n \n \n \n 阅读原文\n \n \n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n \n 别忘了点个在看哦!转发那就太好了!\n \n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n \n \n
\n
\n
\n

\n \n
\n
\n

\n

\n \n 相关链接:\n \n 亚太赛每日一牌回顾:精彩的四红心\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛每日一牌回顾:有趣的四红心\n \n \n

\n

\n \n 相关链接:\n \n 风雨十四载、再夺亚太冠\n \n \n

\n

\n \n 相关链接:\n \n 荏苒十四载、再战亚太赛\n \n \n

\n

\n \n 相关链接:\n \n 十四年归来仍是追风少年\n \n \n

\n

\n \n 相关链接:\n \n 曼谷重启亚太大赛\n \n \n

\n

\n \n 相关链接:\n \n 第53届亚太桥牌锦标赛落幕,欧公赛决赛法国内战\n \n \n

\n

\n \n 相关链接:\n \n 中国桥牌队亚太锦标赛大四喜,欧公赛女子团体冠军产生\n \n \n

\n

\n \n 相关链接:\n \n 欧公赛公团齐老板被淘汰、亚太赛迎来最后高潮\n \n \n

\n

\n \n 相关链接:\n \n 欧公赛公团结束首轮淘汰赛、亚太赛接近尾声\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛公团预赛结束、亚太赛完成上半程比赛\n \n \n

\n

\n \n 相关链接:\n \n 亚太桥牌锦标赛结束女团、混团第一循环比赛\n \n \n \n
\n
\n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛公团女团开赛,亚太赛中国占据三项第一\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛第一天中国女队占优,欧洲公开桥牌锦标赛混团冠军产生\n \n \n

\n

\n \n 相关链接:\n \n 亚太桥牌锦标赛开幕,欧洲公开桥牌锦标赛产生老年团体冠军\n \n \n

\n

\n \n 相关链接:\n \n 第53届亚太桥牌锦标赛即将开赛\n \n \n

\n

\n \n 相关链接:\n \n 第四届亚洲杯结束、中国香港捧起“宁波杯”\n \n \n

\n

\n \n 相关链接:\n \n 2022年亚洲杯开赛\n \n \n

\n

\n \n 相关链接:\n \n 第三届亚洲杯桥牌赛开幕\n \n \n

\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/z0GqwfNMdTFUEdkOglYv1A", - "creator": "标兵桥牌", - "pubDate": "Sat, 31 May 2025 00:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDTH0sTiajz6mMGDUjHnftbibdib7FEvMaiahjxG8C2mKODhpuRaUtBEnLTyDUl3T8iciaqCAYSUSymeB1ZA/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDTH0sTiajz6mMGDUjHnftbibdib7FEvMaiahjxG8C2mKODhpuRaUtBEnLTyDUl3T8iciaqCAYSUSymeB1ZA/0?wx_fmt=jpeg", - "id": "2247512576_1", - "language": "", - "folder": "桥牌", - "feed": "标兵1桥牌", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "bf069e2911ea545aad791222bdeddb68", - "highlights": [] - }, { "title": "亚太赛每日一牌回顾:有趣的四红心", "description": "亚太锦标赛结束了,中国男队夺得冠军,在比赛期间所写的“每日一牌”,比赛期间并没有发出,从今天开始,给桥友们提供迟到的每日一牌。", @@ -2423,28 +2203,6 @@ "hash": "b2ba7615ea76a0fee1a3212e0c63f7bd", "highlights": [] }, - { - "title": "孩子只在父母面前「情绪失控」,在外却近乎「完美」,正常吗?该如何引导?", - "description": "
\"头图\"/
\n

弗洛伊德认为:“攻击性如同是一个人的生命力,如果一个人不能活出自己的攻击性,就会出现心理问题。”

\n

即使环境很糟糕,只要一个人能够正常表达攻击性,就能够保护自我不受外界蹂躏。

\n

心理学研究表明:孩子在很小的时候会强烈地依恋父母或其他养育者,当孩子接触外部世界时,失去这种依恋会使他产生一种不安全感,从而使孩子胆小。

\n

但是如果孩子上学以后对外界表现出过分“完美”“懂事”“胆怯”,很大可能就是家庭教育的问题。

\n

我邻居家的孩子就是这样,在家里,脾气大得惊人。和妈妈顶嘴,对弟弟妹妹呼来喝去,翻脸比翻书还快。

\n

爸妈一说他一句,他能当场怼回几句,搞得全家都不安生。

\n

可一旦出了门,到了学校或者外人面前,立马变脸。

\n

不说话,胆小,做事缩手缩脚,老师点个名都吓一跳,像换了个人。

\n

说白了,孩子在家里发横,是因为他知道自己“横得起”;在外面发怂,是因为他不知道能不能“撑得住”。

\n

而这种反差,很多时候不是性格问题,是家庭结构的问题。

\n

家长要反思自己给孩子是否创造了足够安全的生活环境。例如:夫妻之间的吵架、对孩子言语的伤害或者是过度保护孩子,都是有可能让孩子出现这一表现的原因。

\n

如果想要改变,从这五点做起吧!

\n

一、不过分溺爱,做有原则有底线的父母

\n

“爱孩子”和“立规矩”,从来不是一道单选题。

\n

明确规则,不被孩子牵着鼻子走,不能因为孩子哭闹,就主动打破规则的执行,和善而坚定地对孩子说“不”。让孩子明白,边界感的所在。

\n
\"\"
在孩子的成长过程中,要让他知道:除了爱之外,还要有敬畏
\n

二、每天花 15 分钟的时间和孩子聊天,问问他,学校都发生了什么。

\n

三、每周抽时间给孩子拍一段视频,让他听自己的声音,看自己的表现,发现问题及时调整。

\n
\"\"
\n

四、周末邀请孩子的好朋友到家里来玩,给孩子创造多种社交机会和条件,提升孩子的社交能力。

\n

五、在孩子愿意的前提下,给孩子创造一些上台表演的机会,通过正向的实践提升孩子的自信心。

\n
\"\"
提升孩子自信心的 5 句话
\n

亲测,这几个方法对提高孩子自信心,让孩子变得有原则有担当非常有帮助。

\n
\n
\n\n\n\n\n
\n\n\n\n", - "content": "
\"头图\"/
\n

弗洛伊德认为:“攻击性如同是一个人的生命力,如果一个人不能活出自己的攻击性,就会出现心理问题。”

\n

即使环境很糟糕,只要一个人能够正常表达攻击性,就能够保护自我不受外界蹂躏。

\n

心理学研究表明:孩子在很小的时候会强烈地依恋父母或其他养育者,当孩子接触外部世界时,失去这种依恋会使他产生一种不安全感,从而使孩子胆小。

\n

但是如果孩子上学以后对外界表现出过分“完美”“懂事”“胆怯”,很大可能就是家庭教育的问题。

\n

我邻居家的孩子就是这样,在家里,脾气大得惊人。和妈妈顶嘴,对弟弟妹妹呼来喝去,翻脸比翻书还快。

\n

爸妈一说他一句,他能当场怼回几句,搞得全家都不安生。

\n

可一旦出了门,到了学校或者外人面前,立马变脸。

\n

不说话,胆小,做事缩手缩脚,老师点个名都吓一跳,像换了个人。

\n

说白了,孩子在家里发横,是因为他知道自己“横得起”;在外面发怂,是因为他不知道能不能“撑得住”。

\n

而这种反差,很多时候不是性格问题,是家庭结构的问题。

\n

家长要反思自己给孩子是否创造了足够安全的生活环境。例如:夫妻之间的吵架、对孩子言语的伤害或者是过度保护孩子,都是有可能让孩子出现这一表现的原因。

\n

如果想要改变,从这五点做起吧!

\n

一、不过分溺爱,做有原则有底线的父母

\n

“爱孩子”和“立规矩”,从来不是一道单选题。

\n

明确规则,不被孩子牵着鼻子走,不能因为孩子哭闹,就主动打破规则的执行,和善而坚定地对孩子说“不”。让孩子明白,边界感的所在。

\n
\"\"
在孩子的成长过程中,要让他知道:除了爱之外,还要有敬畏
\n

二、每天花 15 分钟的时间和孩子聊天,问问他,学校都发生了什么。

\n

三、每周抽时间给孩子拍一段视频,让他听自己的声音,看自己的表现,发现问题及时调整。

\n
\"\"
\n

四、周末邀请孩子的好朋友到家里来玩,给孩子创造多种社交机会和条件,提升孩子的社交能力。

\n

五、在孩子愿意的前提下,给孩子创造一些上台表演的机会,通过正向的实践提升孩子的自信心。

\n
\"\"
提升孩子自信心的 5 句话
\n

亲测,这几个方法对提高孩子自信心,让孩子变得有原则有担当非常有帮助。

\n
\n
\n\n\n\n\n
\n\n\n\n", - "category": "", - "link": "https://daily.zhihu.com/story/9782552", - "creator": "", - "pubDate": "Mon, 14 Jul 2025 08:27:04 +0800", - "enclosure": "", - "enclosureType": "", - "image": "", - "id": "", - "language": "", - "folder": "资讯", - "feed": "知乎", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "54d622541f4f473a8dd14f6e4d7644c7", - "highlights": [] - }, { "title": "为什么除了人类,哺乳动物拉屎的时间都是 12 秒?", "description": "
\"头图\"/
\n

先说结论:人类也是 12 秒

\n

所有哺乳动物,包括人类,排便时间都是 12 秒左右[1]。

\n

这是乔治亚理工学院的研究团队,花了好几年时间,跑遍动物园,收集了 34 种哺乳动物的粪便数据得出的结论。

\n

从猫咪到大象,23 个物种的排便时间都在 12±7 秒之间[2]。

\n

人类的速度?每秒 2 厘米。

\n

正好在狗(每秒 1 厘米)和大象(每秒 6 厘米)之间。

\n
\"\"
\n

一只小猫,直肠长度 4 厘米。一头大象,直肠长度 40 厘米。体型差距 1000 倍。但排便时间?几乎一样。

\n

大象粪便体积 20 升,狗只有 10 毫升,相差近 1000 倍。

\n

为什么

\n

答案藏在一个你从未注意过的地方——大肠

\n

覆盖在大肠壁上的超薄黏液层,厚度只有头发丝般细微。

\n

科学家们怎么测量这么薄的东西?

\n

等粪便上的黏液蒸发,然后称重。就这么简单粗暴。但别小看这层黏液。它的黏度比粪便低 100 倍。

\n

什么概念?

\n
想象粪便是厚重的蜂蜜。黏液就像水一样滑溜。
粪便在肠道里滑行,就像雪橇在冰道上飞驰。
\n

这是作者的原话。

\n

大动物有更多粪便要排出。

\n

按常理,应该需要更长时间才对。但进化论说不给。

\n

所以体型大的动物有更厚的黏液层。

\n
小动物黏液薄,但粪便也小。
大动物黏液厚,粪便虽大但滑得更快。
\n

结果完成时间一样。

\n

大动物用同样的压力,却能达到更高的排便速度。

\n

这就是为什么大象能以每秒 6 厘米的速度飞速完成任务,而小狗只能慢慢悠悠每秒 1 厘米。

\n
\"\"
\n

我们人类完全符合这个规律。

\n
研究者明确指出,人类和大象、猫咪一样,都排出圆柱形粪便,遵循相同的物理定律。
\n

人类排便速度每秒 2 厘米,恰好处在狗和大象之间。

\n

我们的直肠长度、黏液厚度、排便压力,都完美契合这个 12 秒公式。

\n
\"\"
\n

这项研究不是随便测几个动物就下结论。

\n

研究团队跑到亚特兰大动物园,亲自拍摄大象、大熊猫、疣猪的排便过程。

\n

还在公园里偷偷观察狗狗。

\n

他们还用“流变仪”——一种精密搅拌器,可以测量粪便的密度和黏度。

\n

顺便发现了:食肉动物的粪便是“沉底派”,食草动物的是“漂浮派”。

\n

还给粪便臭味排了个名:老虎和犀牛最臭,熊猫最“香”。

\n
\"\"
\n

研究团队建立了一个数学模型。

\n
排便时间 = 粪便长度 ÷ 排便速度。
\n

粪便长度是直肠长度的两倍,说明不仅直肠,连结肠都在储存粪便。

\n

排便时,粪便像固体栓塞一样整体移动。

\n

大部分哺乳动物的直肠都是水平的,所以重力作用可以忽略。

\n

一切都靠肌肉压力和黏液润滑。

\n
\"\"
\n

腹泻时会怎样?

\n

研究团队计算出,一个 70 公斤的成年人腹泻时只需要 0.5 秒就能完成。

\n

因为此时不需要肌肉用力,重力就够了。

\n

便秘呢?

\n

最严重情况下,可能需要 6 小时才能完成。因为黏液被吸收,失去润滑作用。

\n

但实际上肠壁会变形帮助推进,时间不会这么长。

\n
\"\"
\n

你可能觉得这种研究很无聊。

\n

错了。

\n

这些排便数据帮助设计了宇航员专用尿布,在 NASA 太空排便挑战赛闯到最后一关。。

\n
\"\"
\n

理解黏液的作用机制,还可以开发出治疗便秘的新药物。

\n

如果便秘是黏液缺乏导致的,那么模拟黏液的药物可能就是治疗便秘的良药。。

\n
\"\"
\n

这个团队还有什么发现?

\n

同一个研究团队之前发现了另一个规律。

\n

所有重量超过 3 公斤的哺乳动物,排尿时间都是 21 秒。

\n

这项发现让他们获得了 2015 年搞笑诺贝尔奖[3]。

\n
\n
\n\n\n\n\n
\n\n\n\n", @@ -2920,28 +2678,6 @@ "hash": "5d4fa4d2344f5e35dbe27c1784fde42c", "highlights": [] }, - { - "title": "自学python必看的书籍《Python编程:从入门到实践》PDF版拿走不谢", - "description": "自学python必看的书籍《Python编程:从入门到实践》内容简介本书是一本针对所有层次的Python 读者", - "content": "
\n

\n \n \n 自学python必看的书籍《Python编程:从入门到实践》\n \n \n

\n

\n \n
\n
\n

\n

\n \n \"图片\"\n \n

\n

\n \n \n 内容简介\n \n \n

\n

\n \n 本书是一本针对所有层次的Python 读者而作的Python 入门书。全书分两部分:\n \n

\n

\n \n 第一部分介绍用Python 编程所必须了解的基本概念,包括matplotlib、NumPy 和Pygal 等强大的Python 库和工具介绍,以及列表、字典、if 语句、类、文件与异常、代码测试等内容;\n \n

\n

\n \n 第二部分将理论付诸实践,讲解如何开发三个项目,包括简单的Python 2D 游戏开发如何利用数据生成交互式的信息图,以及创建和定制简单的Web 应用,并帮读者解决常见编程问题和困惑。\n \n

\n

\n \n
\n
\n

\n

\n \n \n 目录\n \n \n

\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n

\n \n \n 书籍的分享格式为PDF电子版,小伙伴们手机也可以阅读学习哦!\n \n \n

\n

\n \n \n 书籍终归只是辅助,想要真的深入学习python,当然还要有系统的学习方法和学习教程才可以让你在学习的路上一帆风顺哦!\n \n \n

\n

\n \n \n 本套视频一共400集,本套视频共分3季:\n \n \n

\n

\n \n 第一季 【基础篇】Python基础 (115集)\n \n

\n

\n \n 第二季 【提高篇】Python深入和扩展(100集)\n \n

\n

\n \n 第三季 【扩展篇】网络编程、多线程、扩展库(85集)\n \n

\n

\n \n 第四季 【高手篇】算法、Python源码、函数式编程、手工实现神经网络(100集)\n \n

\n

\n \n
\n
\n

\n
\n \n \"图片\"\n \n
\n

\n \n \n 以上这些教程我已经为大家打包准备好了,希望对正在学习的你有所帮助!\n \n \n

\n
    \n
  1. \n

    \n \n \n \n 获取方式:公众号私信回复“学习”即可\n \n \n \n

    \n
  2. \n
  3. \n

    \n \n \n \n 需要的小伙伴可以多多点赞转发 让更多的人看到\n \n \n \n

    \n
  4. \n
\n
\n \n
\n
\n
\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/CFfrCyYKJX5rJG0qS4_CqQ", - "creator": "python开发工程师", - "pubDate": "Sat, 12 Jul 2025 06:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZwm6QzeJkQsibiaKQWY9iaEoFlhj1dyuLP0Bqe2vJOHbia2M04j6GCRMERWfZVcZialbuOxhrZKTN0j3mg/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZwm6QzeJkQsibiaKQWY9iaEoFlhj1dyuLP0Bqe2vJOHbia2M04j6GCRMERWfZVcZialbuOxhrZKTN0j3mg/0?wx_fmt=jpeg", - "id": "2247511336_2", - "language": "", - "folder": "IT技术", - "feed": "python开发工程师", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "6ece793eae1a41b9db83f1da32129968", - "highlights": [] - }, { "title": "自学python必看的书籍《Python编程:从入门到实践》PDF版拿走不谢", "description": "自学python必看的书籍《Python编程:从入门到实践》内容简介本书是一本针对所有层次的Python 读者", @@ -2964,28 +2700,6 @@ "hash": "fb4a24facd693493c2b06a9376b12688", "highlights": [] }, - { - "title": "每天五分钟—Python知识点总结13张思维导图(附全套教程)", - "description": "如果初学者接触的第一门语言是Python,学习曲线则会平滑得多,掌握一些基本语法和Python内置的数据结构,", - "content": "
\n

\n \n
\n
\n

\n
\n

\n \n \n 如果初学者接触的第一门语言是Python,学习曲线则会平滑得多\n \n \n \n ,掌握一些基本语法和Python内置的数据结构,已经可以上手写一些小工具或者小型应用。这对初学者来说,非常重要。因为学习的过程是一个突破舒适区的过程,会面临很多痛苦,如果学习过程得不到激励,很容易半途而废,类似我们开玩笑说的「从入门到放弃」。\n \n \n \n Python还有很多优点:\n \n \n \n 上手快,第三方库丰富,资料丰富,很容易做出“可见可得”的应用。\n \n \n \n Python所需要学习的知识基础知识\n \n \n \n Python最基本的语言特性和语法:•变量•数据类型与数据结构•输入和输出•运算符、表达式、语句•控制流(顺序控制,选择控制和循环控制)•面向过程编程•面向对象编程•异常处理•组织代码•标准库、第三方库\n \n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \n
\n
\n
\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n \n 为大家准备了一些python的学习教程和PDF格式的python电子书籍,希望可以帮助到大家。\n \n \n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \n
\n
\n
\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n
\n \n
\n
\n
\n

\n \n \n 以上这些教程我已经为大家打包准备好了,希望对正在学习的你有所帮助!\n \n \n

\n
    \n
  1. \n

    \n \n \n \n 获取方式:公众号内回复“学习”即可\n \n \n \n

    \n
  2. \n
  3. \n

    \n \n \n \n 需要的小伙伴可以多多点赞转发 让更多的人看到\n \n \n \n

    \n
  4. \n
\n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n
\n \n
\n
\n
\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/p1kNytp-ZuUyIa4g6FQemg", - "creator": "python开发工程师", - "pubDate": "Wed, 09 Jul 2025 06:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZzibdZyOl6Eib8HHXcT1nesbEDU5QxVTKl7ibKiaia6p9HI6PiausBDyjib4ibx7TlS7XX8xicFn6hJdXnpuRQ/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZzibdZyOl6Eib8HHXcT1nesbEDU5QxVTKl7ibKiaia6p9HI6PiausBDyjib4ibx7TlS7XX8xicFn6hJdXnpuRQ/0?wx_fmt=jpeg", - "id": "2247511318_2", - "language": "", - "folder": "IT技术", - "feed": "python开发工程师", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "5b731a170332c34c2ac69fa674d01432", - "highlights": [] - }, { "title": "自学python必看的书籍《Python编程:从入门到实践》", "description": "内容简介本书是一本针对所有层次的Python 读者而作的Python 入门书。", @@ -3008,28 +2722,6 @@ "hash": "7dcfc8e3ce8ac73a506470cf4f75b40b", "highlights": [] }, - { - "title": "练完这102个项目,你的Python就牛了", - "description": "Python 初学者在迈过安装编程环境和基本语法的门槛,准备大展身手的时候,可能突然就会进入迷茫期:不知道做些", - "content": "
\n

\n \n
\n
\n

\n

\n \n \n \n \n

\n
\n \"图片\"\n
\n

\n \n Python 初学者在迈过安装编程环境和基本语法的门槛,准备大展身手的时候,可能突然就会进入迷茫期:\n \n

\n

\n \n 不知道做些什么、再学些什么。\n \n

\n

\n \n 然后对编程的兴趣就会慢慢消退,找不到坚持下去的理由,从而慢慢淡忘之前学会的编程知识。\n \n

\n

\n \n 所以找到自己感兴趣、能够跟着动手和学习的 Python 项目是特别重要的❗❗❗\n \n

\n

\n \n 这样才能把学会的Python 知识用起来,不断地提高。最终从新手晋升为高手\n \n

\n

\n \n 不管学习哪门语言都希望能做出实际的东西来,这个实际的东西当然就是项目啦\n \n

\n

\n \n 不用多说大家都知道学编程语言一定要做项目才行。\n \n

\n

\n \n 1、[Python 图片转字符画]\n \n

\n

\n \n 2、【200行Python代码实现2048]\n \n

\n

\n \n 3、[Python3实现火车票查询工具]\n \n

\n

\n \n 4、【高德API+Python解决租房问题]\n \n

\n

\n \n 5、【Python3色情图片识别]\n \n

\n

\n \n 6、【Python破解验证码]\n \n

\n

\n \n 7、【Python实现简单的Web服务器]\n \n

\n

\n \n 8、【pygame开发打飞机游戏]\n \n

\n

\n \n 9、[Django搭建简易博客]\n \n

\n

\n \n 10、[Python基于共现提取《釜山行》人物关系]\n \n

\n

\n \n 11、【基于scrapy爬虫的天气数据采集(python)]\n \n

\n

\n \n 12、【Flask开发轻博客]\n \n

\n

\n \n 13、【Python3图片隐写术]\n \n

\n

\n \n 14、【Python 实现简易Shell)\n \n

\n

\n \n 15、【使用Python解数学方程]\n \n

\n

\n \n 16、【PyQt实现简易浏览器]\n \n

\n

\n \n 17、【神经网络实现手写字符识别系统]\n \n

\n

\n \n 18、【Python实现简单画板]\n \n

\n

\n \n 19、【Python实现3D建模工具]\n \n

\n

\n \n 20、【NBA常规赛结果预测--利用Python进行\n \n \n \n 比赛数据分析]\n \n \n

\n

\n \n 21、\n \n \n \n 【神经网络实现人脸识别任务]\n \n \n

\n

\n \n 22、【\n \n \n \n [Python文本解析器]\n \n \n

\n

\n \n 23、\n \n \n \n [Python3&OpenCV视频转字符动画]\n \n \n

\n

\n \n 24、[Python3实现淘女郎照片爬虫]\n \n

\n

\n \n 25、\n \n \n \n [Python3实现简单的FTP认证服务器]\n \n \n

\n

\n \n 26、\n \n \n \n [基于Flask与MySQL实现番剧推荐系统]\n \n \n

\n

\n \n 27、[Python实现端口扫描器]\n \n

\n

\n \n 28、\n \n \n \n 【使用Python3编写系列实]\n \n \n

\n

\n \n 29、【Python实现康威生命游戏实用脚本]\n \n

\n

\n \n 30、\n \n \n \n 【川普撞脸希拉里(基于OpenCV\n \n \n \n \n 征交换]\n \n \n

\n

\n \n 31、【Python 3实现Markdown解析器]\n \n

\n

\n \n 32、【Python气象数据分析--《Python数据分\n \n \n \n 析实战》]\n \n \n

\n

\n \n 33、\n \n \n \n 【Python实现键值数据库]\n \n \n

\n

\n \n 34、\n \n \n \n [k-近邻算法实现手写数字识别系统]\n \n \n

\n

\n \n 35、\n \n \n \n 【ebay在线拍卖数据分析]\n \n \n

\n

\n \n 36、\n \n \n \n [Python实现英文新闻摘要自动提取]\n \n \n

\n

\n \n 37、\n \n \n \n 【Python实现简易局域网视频聊大工具]\n \n \n

\n

\n \n 38、【基于Flask及爬虫实现微信娱乐机器人]\n \n

\n

\n \n 39、[Python实现Python解释器]\n \n

\n

\n \n 40、【Python3基于Scapy实现DDos]\n \n

\n

\n \n 41、[Python实现密码强度检测器]\n \n

\n

\n \n 42、【使用Python实现深度神经网络]\n \n

\n

\n \n 43、【Python实现从excel读取数据并绘制成精\n \n \n \n 美图像]\n \n \n

\n

\n \n 44、【人机对战初体验Python基于Pygame实现\n \n \n \n 四子棋游戏]\n \n \n

\n

\n \n 45、【Python3实现可控制肉鸡的反向Shell1\n \n

\n

\n \n \n 获取方式:\n \n \n

\n

\n \n \n \n 1.点赞+在看\n \n \n \n

\n

\n \n \n 2.点击下方小卡片发送‘学习’即可获取~\n \n \n

\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n
\n \n
\n
\n
\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/uOAr9KwYg-6HvbJGRBLxvw", - "creator": "python开发工程师", - "pubDate": "Fri, 04 Jul 2025 04:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZwqm1fyIIFpxnqmrUNyQznwjRjhPW9ic9u3d6IlWcphPpBA01tZtwhmSZBXLxAa7YiajapwmwNq1gkw/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZwqm1fyIIFpxnqmrUNyQznwjRjhPW9ic9u3d6IlWcphPpBA01tZtwhmSZBXLxAa7YiajapwmwNq1gkw/0?wx_fmt=jpeg", - "id": "2247511287_1", - "language": "", - "folder": "IT技术", - "feed": "python开发工程师", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "28a2618c1b234cc3ef8658429dea6b8e", - "highlights": [] - }, { "title": "使用Python6年后,发现学Python看完这几本书少走一半弯路,别错过", "description": "在前面6年里,它一直是我热衷使用并不断研究的语言,迄今为止,python都非常友好并且易于学习!", @@ -3052,28 +2744,6 @@ "hash": "25b3ac08e2f72fa4d222dc9703f673c2", "highlights": [] }, - { - "title": "Python是什么?为什么一定要学Python?", - "description": "一,Python是什么?Python是一种计算机编程语言。", - "content": "
\n

\n \n
\n
\n

\n
\n

\n \n 一,Python是什么?\n \n

\n

\n \n Python是一种计算机编程语言。\n \n

\n

\n \n 对于“编程”,你可能或多或少听说过Java/C语言/C++ /PHP等语言,Python作为近几年特别火的一门编程语言,相较前面几种语言,更注重可读性和效率。\n \n

\n

\n \n 因此,Python对于注重开发效率的程序员来说是大受欢迎的,他们给Python的定位是“优雅”、“明确”、“简单”。\n \n

\n

\n \n 除此之外,Python还具有以下优势,获得人们的青睐:\n \n

\n

\n \n Python 易于学习——Python程序简单易懂,初学者只要识字、会简单的算术、看懂英文单词就能入门学习。因此很多小学生从小就开始通过学习python来培养编程思维。\n \n

\n

\n \n 少量的代码就能构建出很多功能—— 在Python中,几乎每个想法都可以通过20-30行代码快速验证。\n \n

\n

\n \n Python 拥有最成熟的程序包资源库之一——Python有满足不同需求的库,无论实现什么功能,都有现成的类库可以使用。\n \n

\n

\n \n Python 是人工智能/现代数据科学中最常使用的语言——Python应用领域广泛(如图),尤其是人工智能/大数据时代不可或缺的技能。\n \n

\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n 二、为什么一定要学Python?\n \n

\n

\n \n 1、时代之驱\n \n

\n

\n \n 这两年,最火的词莫过于“人工智能”。\n \n

\n

\n \n 随着人工智能和机器学习的发展,各行各业职位正在逐渐被智能机器取替,为了应对被取替的风险,越来越多人开始学习编程,学习火热的程度可谓“全民Python编程的时代”。\n \n

\n

\n \n 可以看看几个重要的新闻:\n \n

\n
    \n
  • \n

    \n \n \n Python纳入浙江省高考!浙江省信息技术教材编程语言将会更换为 Python。\n \n \n

    \n
  • \n
\n

\n \n
\n
\n

\n
    \n
  • \n

    \n \n \n Python纳入山东省的小学教材课程,小学生都开始接触 Python 语言了。\n \n \n

    \n
  • \n
\n

\n \n
\n
\n

\n
    \n
  • \n

    \n \n \n Python 已加入全国计算机等级考试!教育部考试中心决定自2018年起,在计算机二级考试加入了“Python语言程序设计”科目。\n \n \n

    \n
  • \n
\n

\n \n
\n
\n

\n

\n \n 现在国外国内很多家长已经给孩子报名学习Python编程课程了。在美国,就连婴幼儿也有专门的编程童书。\n \n

\n

\n \n 所以,如果不想被淘汰,现在就应该抓紧机会学起来!\n \n

\n

\n \n 2、Python最适合0基础学习者\n \n

\n

\n \n 对于编程小白来说,Python是最适合0基础学习的编程语言。\n \n

\n

\n \n Python的语法简单,代码可读性高,容易入门。从Python学起,你很快就能运用Python编程的底层逻辑去学习另外的语言。也就是说,学习Python是学习编程的最好起点。\n \n

\n

\n \n 3、Python极大提升现有岗位的竞争力\n \n

\n

\n \n 有许多在职程序员本身学了c语言、java等其他编程后,仍坚定多学一门Python开发,他们都被Python代码的优美和开发效率所折服。\n \n

\n

\n \n 比如有一些工程师,以前在做很多SEO优化的时候,苦于不会编程,一些程序上面的问题,得不到解决,只能做做简单的页面优化。\n \n

\n

\n \n 现在学会Python之后,可以编写一些查询收录,排名,自动生成网络地图的程序,解决棘手的SEO问题,这样,在原有专业基础上,他们就多了一项高效的技能。\n \n

\n

\n \n 此外,如果你关注新闻和就业情况,可以明显感觉到,随着 Python 语言的持续火热,对于 Python 技术人才的需求也在不断增加。\n \n

\n

\n \n 通过统计,各大招聘网中,Python岗位的日均需求量甚至达到了 15000+,北上广深等一线城市的需求更是数量庞大,可见这个技能的竞争压力之大。\n \n

\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n \n \n 以下通过职友集Python 工资实际查询截图:\n \n \n \n

\n

\n \n
\n
\n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n \n 在北京,Python平均工资20980,并且92.7%的Python从业人员薪资在10K以上。\n \n \n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n \n 在互联网比较发达的二线城市,比如成都平均工资12260,6K以上占比85% ,10K以上占比也到达了70.4%。\n \n \n

\n

\n \n \n 可以说,Python是近两年投入性价比最高的学习技能之一了!\n \n \n

\n

\n \n \n 现在,相信你已经对Python有了初步的认知和了解。\n \n \n

\n

\n \n 最后\n \n

\n

\n \n 如果对Python感兴趣的话,可以试试我的学习方法以及相关的学习资料\n \n

\n

\n \n 对于0基础小白入门:\n \n

\n

\n \n 如果你是零基础小白,想快速入门Python是可以考虑培训的。\n \n

\n

\n \n 一方面是学习时间相对较短,学习内容更全面更集中。\n \n

\n

\n \n Python所有方向的学习路线\n \n

\n

\n \n Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。\n \n

\n

\n \n Python所有方向的学习路线\n \n

\n

\n \n \n Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。\n \n \n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n Python必备开发工具\n \n

\n

\n \n \n 工欲善其事必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。\n \n \n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n Python精品PDF电子书\n \n

\n

\n \n \n 阅读一些书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。\n \n \n

\n
\n \"图片\"\n
\n

\n \n Python学习视频\n \n

\n

\n \n \n 观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。\n \n \n

\n
\n \"图片\"\n
\n

\n \n
\n
\n

\n

\n \n 实战案例\n \n

\n

\n \n \n 光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。\n \n \n

\n
\n \"图片\"\n
\n
\n

\n \n
\n
\n

\n

\n \n 获取方式:\n \n

\n

\n \n 1.点赞+再看\n \n

\n

\n \n 2.公众号内回复:“学习”\n \n

\n

\n \n
\n
\n

\n
\n \n
\n
\n
\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/il87sB37BUOUiln-4bMyzQ", - "creator": "python开发工程师", - "pubDate": "Mon, 30 Jun 2025 09:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZy6NhzQnQnn6fj5giafbrz2bCXCNXVa4kriaKonPngaWuYNXLfSuEOtL6Fu3Ga0XM2RExUB46rDL0eg/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZy6NhzQnQnn6fj5giafbrz2bCXCNXVa4kriaKonPngaWuYNXLfSuEOtL6Fu3Ga0XM2RExUB46rDL0eg/0?wx_fmt=jpeg", - "id": "2247511263_2", - "language": "", - "folder": "IT技术", - "feed": "python开发工程师", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "41aaa40f77c628b164b24e899c10bf40", - "highlights": [] - }, { "title": "自学python必看的书籍《Python编程:从入门到实践》PDF版拿走不谢", "description": "内容简介本书是一本针对所有层次的Python 读者而作的Python 入门书。", @@ -3096,28 +2766,6 @@ "hash": "761a8deb7a6670d138fbe73449a68ef1", "highlights": [] }, - { - "title": "计算机专业18个Java毕设项目(附源码+视频+数据库),拿走即用!期末毕设不用", - "description": "领取方式:点赞+私信留言“111”或主动给我打个招呼,我都会回复的~~~一定要关注我,不然我发不了消息哦~", - "content": "
\n

\n \n \n
\n
\n
\n

\n

\n \n \n \n \n \n

\n

\n \n \n

\n

\n \n \n \n \n \n

\n

\n \n \n

\n

\n \n \n \n \n \n

\n

\n \n \n

\n

\n \n \n \n \n \n

\n

\n \n \n

\n

\n \n \n \n \n \n

\n

\n \n \n 领取方式:点赞+私信留言“111”或主动给我打个招呼,我都会回复的~~~\n \n \n

\n

\n \n \n 一定要关注我,不然我发不了消息哦~\n \n \n

\n

\n \n \n 整理不易,有帮助的话大家一定要 点赞 + 收藏 哦 ❤️!支持作者的同时,便于自己下次更方便地找到叭!\n \n \n

\n
\n \n
\n
\n
\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/jJOoSZywzYalMsmElLJDFw", - "creator": "python开发工程师", - "pubDate": "Wed, 25 Jun 2025 09:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZykJYNAN6ERqWwaXLkUyZR9fvibGb3ZJY6qI6g2LWABXT88R1dhIwRS6icaENdrkXtrtoIuBzJ0Od2w/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZykJYNAN6ERqWwaXLkUyZR9fvibGb3ZJY6qI6g2LWABXT88R1dhIwRS6icaENdrkXtrtoIuBzJ0Od2w/0?wx_fmt=jpeg", - "id": "2247511233_1", - "language": "", - "folder": "IT技术", - "feed": "python开发工程师", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "5f74af3509bb7119b65229e9a44505ee", - "highlights": [] - }, { "title": "python入门教程完整版(PDF版)", "description": "当年3w买的全套python教程用不着了,现在送给有缘人,不要钱,一个月教你从入门到精通本套视频共600集70", @@ -3140,28 +2788,6 @@ "hash": "11ddd29ebda4153b594d8c3b201af156", "highlights": [] }, - { - "title": "学习python的必读书籍《利用Python进行数据分析》附PDF版", - "description": "这是非常经典的一本Python数据分析书,每一章之间有递进关系,适合Python入门()如《Python编程,", - "content": "
\n

\n \n 这是非常经典的一本Python数据分析书,每一章之间有递进关系,适合Python入门()如《Python编程,从入门到实践》)之后阅读,本书专门针对数据分析领域的。我细致地读了一遍,敲了一遍代码,一开始没有头绪,进展缓慢,后来逐渐明朗了。学习编程要多实践,这对没有基础的人来说有困难,因为无从下手。我的体会是,在你基础不牢的时候,就多看,多记,多总结,搭建自己的思路框架。\n \n

\n

\n \n 书籍简介\n \n

\n
\n \"图片\"\n
\n

\n \n 利用Python进行数据分析\n \n

\n

\n \n \n 本书由pandas项目创始人Wes McKinney亲笔撰写,详细介绍利用Python进行操作、处理、清洗和规整数据等方面的具体细节和基本要点。第2版针对Python 3.6进行全面修订和更新,涵盖新版的pandas、NumPy、IPython和Jupyter,并增加大量实际案例,可以帮助你高效解决一系列数据分析问题。\n \n \n

\n

\n \n \n 第2版中的主要更新包括:\n \n \n

\n

\n \n \n • 所有的代码,包括把Python的教程更新到了Python 3.6版本(第1版中使用的是Python 2.7)\n \n \n

\n

\n \n \n • 更新了Python第三方发布版Anaconda和其他所需Python包的安装指引\n \n \n

\n

\n \n \n • 更新pandas库到2017年的新版\n \n \n

\n

\n \n \n • 新增一章,关于更多高级pandas工具和一些使用提示\n \n \n

\n

\n \n \n • 新增statsmodels和scikit-learn的简明使用介绍\n \n \n

\n

\n \n \n \"图片\"\n \n \n

\n

\n \n 目录\n \n

\n

\n \n 第1章 准备工作\n \n
\n \n 第2章 Python语言基础、IPython及Jupyter notebook\n \n
\n \n 第3章 内建数据结构、函数及文件\n \n
\n \n 第4章 NumPy基础:数组与向量化计算\n \n
\n \n 第5章 pandas入门\n \n
\n \n 第6章 数据载入、存储及文件格式\n \n
\n \n 第7章 数据清洗与准备\n \n
\n \n 第8章 数据规整:连接、联合与重塑\n \n
\n \n 第9章 绘图与可视化\n \n
\n \n 第10章 数据聚合与分组操作\n \n
\n \n 第11章 时间序列\n \n
\n \n 第12章 高阶pandas\n \n
\n \n 第13章 Python建模库介绍\n \n
\n \n 第14章 数据分析示例\n \n

\n
\n \"图片\"\n
\n
\n \"图片\"\n
\n

\n \n \n \"图片\"\n \n \n

\n
\n \"图片\"\n
\n

\n \n \n \n 领取方式:\n \n
\n
\n
\n

\n
    \n
  1. \n

    \n \n \n \n 点赞+在看\n \n \n \n

    \n
  2. \n
  3. \n

    \n \n \n \n 私信留下回复:“学习”\n \n \n \n

    \n
  4. \n
\n
\n \n
\n
\n
\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/QzNsOfAyqx2sgOiavcalzQ", - "creator": "python开发工程师", - "pubDate": "Fri, 20 Jun 2025 09:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZzmibvbzGeIPHq0Sn1ibibWA4MtWvsSEdycjHjFSUVspgdgrZ63Tic9M30R85C32DVibmXTlKomqNnDrKw/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZzmibvbzGeIPHq0Sn1ibibWA4MtWvsSEdycjHjFSUVspgdgrZ63Tic9M30R85C32DVibmXTlKomqNnDrKw/0?wx_fmt=jpeg", - "id": "2247511178_1", - "language": "", - "folder": "IT技术", - "feed": "python开发工程师", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "1189f681b3a409d6c3f0d7e40085e750", - "highlights": [] - }, { "title": "瞎扯 · 如何正确地吐槽", "description": "
\"头图\"/
\n\n
\n
@席萝 :阁下可能对鸡的破坏力没什么概念。
\n
\n
\n
\n
\n
\n
\n
\n
\n
\"\"\n
 
\n
 
\n
\n
\n
\n
\n
\n
\n
 
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\"\"\n
 
\n
 
\n
\n
\n
\n
\n
\n
\n
 
\n
\n
\n\n
\n
@科穆宁之怒 :别的不说,青海省只有西宁一个城市……甚至没有其他可以称之为「城市」的地方。
\n
\n\n
\n
@水隼 :领导:小王啊,去把碗洗了。 小王:我可是化学领域的专业人士。 领导:那好,记得刷三遍,直到内壁的水既不聚成水滴也不成股流下。
\n
\n\n
\n
@阿猫lh :车厘子 40 块钱一斤不舍得买, 打开股票账户一看亏了两吨半。
\n
\n\n
\n
@那兰峙月 :捡到一只被气枪打伤的信鸽,送去宠物医院缝合伤口救活了,然后就打算先养一阵子,等它伤好了再放飞…… 然后,赖我,没仔细研究养鸽子的功课,每天一碗玉米粒的喂,喂成了胖子飞不动了……
\n
\n
\n
\n
\n
\n
\n
\n
\n
\"\"\n
 
\n
 
\n
\n
\n
\n
\n
\n
\n
 
\n
\n
\n
\n
补充:我忘记说了,它的名字叫陈小缘,本意是捡到它是缘分,结果变圆球的圆了。 2024-02-07 更新,找到了它做完缝合手术出院到我家的照片……完全看不出来是同一只吧……
\n
\n
\n
\n
\n
\n
\n
\n
\n
\"\"\n
 
\n
 
\n
\n
\n
\n
\n
\n
\n
 
\n
\n
\n
 
\n
\n
\n\n\n
\n\n\n\n", @@ -4046,28 +3672,6 @@ "hash": "99b651d543cad1d20b6bf4c9c9210913", "highlights": [] }, - { - "title": "进攻性叫牌与防守性叫牌——波兹南的牌局", - "description": "桥牌其实也一样,只不过桥牌的“进攻”和“防守”,有多重的意义,除了简单的在桥牌打法中表现为做庄与防守外,还有在叫牌中也分为自己抢过来打的“进攻性叫牌”和放给对方打或者叫得较低的“防守性叫牌”。", - "content": "
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n \n \n 新朋友\n \n \n \n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 标兵桥牌\n \n \n \n

\n

\n \n \n 关注微信\n \n \n

\n
\n
\n \n \n 老朋友\n \n \n
\n
\n

\n \n \n 点击\n \n \n \n 右上角\n \n \n \n

\n

\n \n \n 分享本文\n \n \n

\n
\n
\n \n \n \n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n NBA有句格言,叫做“赢球靠进攻、夺冠靠防守”,其实足球、橄榄球等等基本上都一样,防守虽然观赏性不强,但对于成功是非常有帮助的。\n \n

\n

\n \n 桥牌其实也一样,只不过桥牌的“进攻”和“防守”,有多重的意义,除了简单的在桥牌打法中表现为做庄与防守外,还有在叫牌中也分为自己抢过来打的“\n \n 进攻性叫牌\n \n ”和放给对方打或者叫得较低的\n \n “防守性叫牌”\n \n 。一般来说,实力强的一方,通常会选择进攻性叫牌,因为只有完成满贯和成局定约,才能获得更高的分数,但也有例外的时候,就是当对方持弱牌介入叫牌、意图干扰时,此时强牌一方就同时了有抢过来自己打的“进攻性叫牌”,和加倍对方或者叫的较低的“防守性叫牌”两个选择。\n \n

\n

\n \n 一般来说,牌手们都会“进攻性叫牌”,也就是高叫,但往往忽视“防守性叫牌”的得分能力,特别是在比分落后时,往往“进攻性叫牌”会越输越多,而“防守性叫牌”则是很好的追分办法。\n \n

\n

\n \n 6月30日结束的欧洲公开桥牌锦标赛公开团体赛首轮淘汰赛64进32的比赛中,罗森塔尔队就上演了依靠“防守性叫牌”翻盘的一场好戏。\n \n

\n

\n \n 4节的比赛打完3节,预赛第2的罗森塔尔队面对第44名的哥本哈根队,落后18IMP,14副牌的比赛虽说这个比分不算多,可是要想扳回也不容易。\n \n

\n

\n \n 来看这副牌:\n \n

\n
\n
\n \n
\n
\n \n 6♣还可以\n \n
\n
\n

\n \n 南北方是哥本哈根队的保尔森和尼尔森,东西方是罗森塔尔队的舒尔茨和韦兰肯,这副牌是第26副,整场比赛的倒数第3副,此时罗森塔尔队已慢慢将比分赶上,还有6分的差距。\n \n

\n

\n \n 西家开叫3♠,北家加倍,东家4♠,南家4NT显示两套牌,北家实力超强,5♠扣叫挺进满贯,他们达到6♣定约。\n \n

\n

\n \n 联手牌看起来还不错,只要♦A在西家,就有很大的机会完成定约,即使♦A在东家,也还有双张♦J、♥3-3等机会,综合看起来这个满贯还不错。\n \n

\n

\n \n 北家A吃进首攻,然后拔♣A,噩耗传来:\n \n

\n
\n
\n \n
\n
\n \n 将牌是5-0,完不成定约了\n \n
\n
\n

\n \n 将牌居然是5-0分布,看上去不错的满贯就此垮掉。\n \n

\n

\n \n 全手牌如下:\n \n

\n
\n
\n \n
\n
\n \n 东西可以做成4♠\n \n
\n
\n

\n \n 东西方可以做成4♠,南家第一次的判断非常正确,原本他只有5-4两套,而且在低花上只有2个点,计划防守是个合理的选择,因为这副牌在60桌上很多桌都是加倍东西方的4♠而做成,从这一点来说,南家的“进攻性叫牌”选对了。\n \n

\n

\n \n 不过,北家并不知道南家的牌如此不堪,他也选择了“进攻性叫牌”,如果南北方在对方阻击叫后因为存在分布的不理想而稍微保守一些,就会得到很好的收益。\n \n

\n

\n \n 在另一桌,轮到了东西方采取了“进攻性叫牌”:\n \n

\n
\n
\n \n
\n
\n \n 南北方选择加倍对方5♠\n \n
\n
\n

\n \n 西家在第三家的开叫非常进取,他一定是秉承“12张套一定要在4阶以上叫牌”的原则,北家当然加倍,东家进一步挺进5♠,南家布格兰德这牌无疑是“防守性”,所以他Pass静观其变,当巴克再次加倍时,布格兰德选择罚放,他们吃到了200分的正分。\n \n

\n

\n \n 这副牌就是典型的实力更强一方需要采取“防守性叫牌”的一例。这副牌罗森塔尔队赢9IMP,一举反先,最后2副牌都在加倍中度过,罗森塔尔队最后赢了18IMP,如果哥本哈根队的南北方停在5♣上并且做成,他们这副牌会赢得9IMP,总比分就会战成平手,那时,谁输谁赢就很难讲了。\n \n

\n

\n \n 可谓“会进攻是徒弟、会防守才是师傅”。\n \n

\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n

\n \n 相关链接:\n \n 技惊四座的Deep Finesse打法\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛第一阶段结束、弗姆队获得欧洲混团冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲混团赛场上一副牌的两次安全打法\n \n \n

\n

\n \n 相关链接:\n \n 第11届欧洲公开桥牌锦标赛混团预赛结束\n \n \n

\n

\n \n 相关链接:\n \n 第11届欧洲桥牌公开锦标赛\n \n \n

\n

\n \n 相关链接:\n \n 2023年欧洲公开桥牌锦标赛每日一牌——韦尔萨切的失误,还是对手的高明?\n \n \n

\n

\n \n 相关链接:\n \n 第10届欧洲桥牌公开锦标赛\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛结束\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开赛结束、土耳其人获得公开双人赛冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲冬季运动会结束,奥卡队获得冠军\n \n \n

\n

\n \n 相关链接:\n \n 欧洲冬季桥牌运动会开赛在即\n \n \n

\n

\n \n 相关链接:\n \n 劳伦特当选欧洲桥联主席、期待开启新时代\n \n \n \n
\n
\n

\n

\n \n 相关链接:\n \n 欧洲桥牌新纪元——欧洲桥牌团体锦标赛回顾\n \n \n

\n
\n
\n
\n
\n
\n

\n \n 往期推荐\n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n

\n \n \n 2022年欧洲桥牌团体锦标赛即将开赛\n \n \n

\n
\n
\n
\n
\n

\n \n \n 荷兰加冕欧洲之王\n \n \n

\n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n \n \n \n 点击\n \n \n \n \n 阅读原文\n \n \n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n \n 别忘了点个在看哦!转发那就太好了!\n \n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/MlG5afey3PhW8jkCKRLwNA", - "creator": "标兵桥牌", - "pubDate": "Wed, 02 Jul 2025 00:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDQoJpic0ibsIxaF7GAQVwVTT6eGyFJ2u6u8uRPPuRgRemxz9bYpyyQ9DDobFOOPU8ndsqr7ibKoF7EOQ/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDQoJpic0ibsIxaF7GAQVwVTT6eGyFJ2u6u8uRPPuRgRemxz9bYpyyQ9DDobFOOPU8ndsqr7ibKoF7EOQ/0?wx_fmt=jpeg", - "id": "2247512660_1", - "language": "", - "folder": "桥牌", - "feed": "标兵1桥牌", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "8cf9f3254a25c91aade7bc229bc8c13b", - "highlights": [] - }, { "title": "美国国家队名单出炉", "description": "5月26日当地时间,美国国家队选拔赛老年团体赛2025年6月1日,美国桥牌协会(USBF)公布了参加2025年世界桥牌团体锦标赛的8支美国队参赛名单。由于美国选拔赛允许一名选手报名多项比赛,因此在选拔赛全部结束后,各个美国国家队进行了调整。", @@ -4089,28 +3693,6 @@ "tags": [], "hash": "904cfda1cd5a396451f15015355b3b1e", "highlights": [] - }, - { - "title": "亚太赛每日一牌回顾:有趣的四红心", - "description": "亚太锦标赛结束了,中国男队夺得冠军,在比赛期间所写的“每日一牌”,比赛期间并没有发出,从今天开始,给桥友们提供迟到的每日一牌。", - "content": "
\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n 新朋友\n \n \n \n \n \n
\n

\n \n \n 点击\n \n \n \n 标兵桥牌\n \n \n \n

\n

\n \n \n 关注微信\n \n \n

\n
\n \n \n 老朋友\n \n \n
\n

\n \n \n 点击\n \n \n \n 右上角\n \n \n \n

\n

\n \n \n 分享本文\n \n \n

\n
\n \n \n \n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n

\n \n
\n
\n

\n

\n \n \n 亚太锦标赛结束了,中国男队夺得冠军,在比赛期间所写的“每日一牌”,比赛期间并没有发出,从今天开始,给桥友们提供迟到的每日一牌。\n \n \n

\n
\n

\n \n 5月17日每日一牌:有趣的4♥\n \n

\n

\n \n 在主打时,需要制定计划,找到看似不可能发生但能够完成定约的路线,而找到一条路线时,需要反复确认,是否可行。\n \n

\n

\n \n 下面是亚太锦标赛第一循环中国男队对韩国队的一副牌:\n \n

\n
\n
\n \n
\n
\n \n 南家主打4♥加倍定约\n \n
\n
\n

\n \n 东家开叫1♦,至少4张以上,南家争叫4♥,转到东家加倍,成为最后定约。\n \n

\n

\n \n 南家主打4♥加倍定约,西家首攻♦7,明手的牌看起来没什么用,先摆小,东家A吃, 马上出♦,现在庄家怎么办?\n \n

\n
\n
\n \n
\n
\n \n 首攻♦到A,东家再出♦\n \n
\n
\n

\n \n 将牌足够结实,不怕对方提升将牌,♦和将牌各有一个输墩,♠上面的输墩如何逃脱?如果自己手中出♠,至少要输两墩。\n \n

\n

\n \n 庄家发现,对方只有3张将牌,而且除了A之外,都是小将牌,假如将牌2-1分布,庄家就是铁牌了。他可以大将吃♦,打小将牌,如果西家是Ax两张,无论他出哪一张,明手都可以进手兑现♦赢墩,并且从明手出♠。\n \n

\n

\n \n 再进行下一步行动之前,先考虑一下对方是什么牌,西家是否确定是2张将牌。\n \n

\n

\n \n 西家没有叫4♠,不像有4张以上的♠,假如西家是单张♦,同时也只有2张将牌,那么他就是3-2-1-7牌型,他是否会选择叫5♣?如果西家的高花更短,他的♣相应更长,也更不容易Pass。\n \n

\n

\n \n 如果大将吃♦,打小将牌到明手的路线成功,需要西家是2张将牌、2张♦、不超过3张♠。\n \n

\n

\n \n 实战中,中国男队的庄家决定采取将牌2-1分布的路线,他大将吃♦,打小将牌,西家先跟小,明手将牌9大,发现将牌3-0,只好从明手出♠,东家不知为何抢上♠A,替庄家解决了♠问题,定约做成。\n \n

\n

\n \n 全手牌如下:\n \n

\n
\n
\n \n
\n
\n \n 东家犯错,完成定约\n \n
\n
\n

\n \n 这副牌在另一室的叫牌也是南家直接争叫4♥,但连若旸没有选择Pass准备罚放,而是直接加倍希望同伴叫牌,同伴叫出5♣,还赚了一个加倍。这副牌中国队两边成局,获得16IMP。\n \n

\n

\n \n 顺便谈一下东家的防守,虽然明显的♠A不应该马上出,但在第一墩♦A吃进后,它并不能确定同伴是单张♦,贸然继续♦,一旦庄家单张♦,可能损失惨重,更好的出牌还是试试♣,从同伴Pass 4♥并没有叫4♠来看,庄家的♠有一定的长度,他的♣较短,如果庄家是AQ双张♣,他至少还有4张♠,♦几乎肯定是单张了,所以出♦更不安全。\n \n

\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n \n \n \n \n \n 点击\n \n \n \n \n 阅读原文\n \n \n \n \n \n
\n
\n
\n
\n

\n \n
\n
\n

\n
\n \n \n
\n

\n \n
\n
\n

\n
\n
\n
\n
\n
\n

\n \n \n 别忘了点个在看哦!转发那就太好了!\n \n \n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n \n \n
\n
\n
\n

\n \n
\n
\n

\n

\n \n 相关链接:\n \n 风雨十四载、再夺亚太冠\n \n \n

\n

\n \n 相关链接:\n \n 荏苒十四载、再战亚太赛\n \n \n

\n

\n \n 相关链接:\n \n 十四年归来仍是追风少年\n \n \n

\n

\n \n 相关链接:\n \n 曼谷重启亚太大赛\n \n \n

\n

\n \n 相关链接:\n \n 第53届亚太桥牌锦标赛落幕,欧公赛决赛法国内战\n \n \n

\n

\n \n 相关链接:\n \n 中国桥牌队亚太锦标赛大四喜,欧公赛女子团体冠军产生\n \n \n

\n

\n \n 相关链接:\n \n 欧公赛公团齐老板被淘汰、亚太赛迎来最后高潮\n \n \n

\n

\n \n 相关链接:\n \n 欧公赛公团结束首轮淘汰赛、亚太赛接近尾声\n \n \n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛公团预赛结束、亚太赛完成上半程比赛\n \n \n

\n

\n \n 相关链接:\n \n 亚太桥牌锦标赛结束女团、混团第一循环比赛\n \n \n \n
\n
\n

\n

\n \n 相关链接:\n \n 欧洲公开桥牌锦标赛公团女团开赛,亚太赛中国占据三项第一\n \n \n

\n

\n \n 相关链接:\n \n 亚太赛第一天中国女队占优,欧洲公开桥牌锦标赛混团冠军产生\n \n \n

\n

\n \n 相关链接:\n \n 亚太桥牌锦标赛开幕,欧洲公开桥牌锦标赛产生老年团体冠军\n \n \n

\n

\n \n 相关链接:\n \n 第53届亚太桥牌锦标赛即将开赛\n \n \n

\n

\n \n 相关链接:\n \n 第四届亚洲杯结束、中国香港捧起“宁波杯”\n \n \n

\n

\n \n 相关链接:\n \n 2022年亚洲杯开赛\n \n \n

\n

\n \n 相关链接:\n \n 第三届亚洲杯桥牌赛开幕\n \n \n

\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/fs-IXTIprP-cFsg1E2bafQ", - "creator": "标兵桥牌", - "pubDate": "Thu, 29 May 2025 00:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDTH0sTiajz6mMGDUjHnftbibdib7FEvMaiahjxG8C2mKODhpuRaUtBEnLTyDUl3T8iciaqCAYSUSymeB1ZA/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/iauJ3TcMNYDTH0sTiajz6mMGDUjHnftbibdib7FEvMaiahjxG8C2mKODhpuRaUtBEnLTyDUl3T8iciaqCAYSUSymeB1ZA/0?wx_fmt=jpeg", - "id": "2247512565_1", - "language": "", - "folder": "桥牌", - "feed": "标兵1桥牌", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "b2ba7615ea76a0fee1a3212e0c63f7bd", - "highlights": [] } ], "folder": "双语", @@ -4653,28 +4235,6 @@ "hash": "b2ba7615ea76a0fee1a3212e0c63f7bd", "highlights": [] }, - { - "title": "自学python必看的书籍《Python编程:从入门到实践》", - "description": "内容简介本书是一本针对所有层次的Python 读者而作的Python 入门书。", - "content": "
\n

\n \n
\n
\n

\n

\n \n \"图片\"\n \n

\n

\n \n \n 内容简介\n \n \n

\n

\n \n 本书是一本针对所有层次的Python 读者而作的Python 入门书。全书分两部分:\n \n

\n

\n \n 第一部分介绍用Python 编程所必须了解的基本概念,包括matplotlib、NumPy 和Pygal 等强大的Python 库和工具介绍,以及列表、字典、if 语句、类、文件与异常、代码测试等内容;\n \n

\n

\n \n 第二部分将理论付诸实践,讲解如何开发三个项目,包括简单的Python 2D 游戏开发如何利用数据生成交互式的信息图,以及创建和定制简单的Web 应用,并帮读者解决常见编程问题和困惑。\n \n

\n

\n \n
\n
\n

\n

\n \n \n 目录\n \n \n

\n

\n \n
\n
\n

\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n
\n \n \"图片\"\n \n
\n

\n \n
\n
\n

\n

\n \n \n 书籍的分享格式为PDF电子版,小伙伴们手机也可以阅读学习哦!\n \n \n

\n

\n \n \n 书籍终归只是辅助,想要真的深入学习python,当然还要有系统的学习方法和学习教程才可以让你在学习的路上一帆风顺哦!\n \n \n

\n

\n \n \n 本套视频一共400集,本套视频共分3季:\n \n \n

\n

\n \n 第一季 【基础篇】Python基础 (115集)\n \n

\n

\n \n 第二季 【提高篇】Python深入和扩展(100集)\n \n

\n

\n \n 第三季 【扩展篇】网络编程、多线程、扩展库(85集)\n \n

\n

\n \n 第四季 【高手篇】算法、Python源码、函数式编程、手工实现神经网络(100集)\n \n

\n

\n \n
\n
\n

\n

\n \n
\n
\n

\n
\n \n \"图片\"\n \n
\n

\n \n
\n
\n

\n

\n \n \n 以上这些教程我已经为大家打包准备好了,希望对正在学习的你有所帮助!\n \n \n

\n
    \n
  1. \n

    \n \n \n \n 获取方式:公众号私信回复“学习”即可\n \n \n \n

    \n
  2. \n
  3. \n

    \n \n \n \n 需要的小伙伴可以多多点赞转发 让更多的人看到\n \n \n \n

    \n
  4. \n
\n
\n \n
\n
\n
\n

\n \n \n

\n
\n", - "category": "", - "link": "https://mp.weixin.qq.com/s/c_5LNvw3SAjwBdMs9TRztQ", - "creator": "python开发工程师", - "pubDate": "Sat, 05 Jul 2025 06:00:00 ", - "enclosure": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZxW21P2Wlme8ePMGec1BlUXOFYxYplAyK6CLl2x2bH4d2ZWosUWkQlGPFHSr7MpMNoggFfhVnvzag/0?wx_fmt=jpeg", - "enclosureType": "image/jpeg", - "image": "https://mmbiz.qpic.cn/mmbiz_jpg/Nc53lZXDjZxW21P2Wlme8ePMGec1BlUXOFYxYplAyK6CLl2x2bH4d2ZWosUWkQlGPFHSr7MpMNoggFfhVnvzag/0?wx_fmt=jpeg", - "id": "2247511294_2", - "language": "", - "folder": "IT技术", - "feed": "python开发工程师", - "read": false, - "favorite": false, - "created": false, - "tags": [], - "hash": "7dcfc8e3ce8ac73a506470cf4f75b40b", - "highlights": [] - }, { "title": "自学python必看的书籍《Python编程:从入门到实践》PDF版拿走不谢", "description": "内容简介本书是一本针对所有层次的Python 读者而作的Python 入门书。",