// ==UserScript== // @name YAML 阅读器 & 编辑器 (分屏实时预览 + 快捷缩进) // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 把 .yaml/.yml 拖到任意网页或空白页即可解析成可折叠树;直接打开本地文件时按地址把字节读回来再解析。编辑器支持 Tab/Shift+Tab 缩进、回车自动续进、选区整行缩进,右侧原文改动实时刷新左侧树。支持多文档(---)、锚点/别名、块标量;可搜索定位、另存为 JSON / 规范化 YAML、Ctrl+S 保存。其它格式一律不拦。 // @author healer007 // @match *://*/* // @match file:///* // @include about:blank // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect * // @run-at document-start // @noframes // @license MIT // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI1IiBmaWxsPSIjMjgyYTM2IiBzdHJva2U9IiM1MGZhN2IiIHN0cm9rZS13aWR0aD0iMS40Ii8+PHRleHQgeD0iMTIiIHk9IjE2LjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJDb25zb2xhcyxNZW5sbyxtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTIiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjNTBmYTdiIj5ZPC90ZXh0Pjwvc3ZnPg== // ==/UserScript== /* * 用法 * 1. 浏览器直接打开本地 .yaml / .yml → 自动解析成分屏界面(左侧树形预览,右侧原文编辑)。 * 2. 编辑右侧原文,左侧实时刷新(解析失败会显示行/列和错误摘录,并保留原文)。 * 3. 快捷缩进: * · Tab 光标处插入 2 空格;选中多行时整块加缩进 * · Shift+Tab 当前行/选区行整体减一级缩进(2 空格) * · Enter 自动延续上一行缩进;行尾是 key: / - / | / > 时自动加 2 空格 * · 工具栏 ⇤ / ⇥ 对当前行或选区行减/加缩进 * 4. 保存:Ctrl+S 或「保存」按原格式导出;「另存为」可导出 .json 或规范化 .yml。 * * 范围边界(和 data-reader / markdown-reader / word-reader 协作) * · 脚本注入所有网页只为接住拖放。拖进来的扩展名不是 .yaml/.yml 时不接管 drop, * 浏览器照常打开,其他脚本也能接手。 * · dragenter/dragover 对文件拖放一律 preventDefault(只保证 drop 能触发,不吞默认行为), * 与 data-reader 1.4.0 同一策略,重复取消无副作用。 * · 自动铺满页面只发生在 file:// 且扩展名是 yaml/yml 时;http(s) 上的 yaml 接口页不自动接管。 * · 声明接管用 documentElement.dataset.fileReader = 'yaml', * data-reader 的守护看到非 'data' 会停手,互不抢页面。 * * 与浏览器自带查看器的冲突(Chrome / Edge) * · .yaml/.yml 通常没有内置查看器:要么直接下载,要么空白页。 * · 对策与 data-reader 一致:
有内容先抓快照;为空就按 file:// 地址把字节读回来
* (UTF-8 失败再试 gb18030);用 MutationObserver 在界面被冲掉后挂回去。
*
* 解析能力
* · 内嵌 js-yaml 4.1.0(MIT)浏览器 bundle,完整 YAML 1.1/1.2 子集:
* 锚点 & / 别名 *、块标量 | >、flow 风格 {} []、多文档 ---、!!binary 等标签。
* · 多文档文件在树里按「文档 N」分节;另存为 JSON 时多文档导出为数组。
* · 解析错误带行/列定位 + 出错行摘录(js-yaml YAMLException.mark)。
* · 搜索:匹配的键/值高亮并自动展开,无关子树收起变暗。
* · YAML 文件普遍在百 KB 量级,不做虚拟列表;超过 64MB 拒绝载入。
*/
(function () {
'use strict';
/* ============================================================
* 0. 常量与状态
* ============================================================ */
const VERSION = '1.0.0';
const ROOT_ID = 'yr-reader-root';
const EXT_YAML = ['yaml', 'yml'];
const MAX_AUTO_PARSE = 64 * 1024 * 1024; // 超过 64MB 拒绝载入
const PREVIEW_DEBOUNCE = 250;
const state = {
fileName: 'data',
original: '', // 载入时的原文,用于「还原」和未保存判断
text: '', // 当前原文(= textarea.value)
docs: null, // loadAll 结果(数组,可能是多文档)
error: '', // 解析错误信息(含行列)
errLine: -1, // 出错行(1-based,-1 未知)
parseMs: 0,
query: '',
fold: {}, // 树节点折叠状态 path -> 'open' | 'closed'
debounceTimer: 0
};
const refs = {};
/**
* 浏览器把 file:// 的 yaml 当下载或空白页处理,没有可靠的原生查看器,
* 但 JSON 查看器的经验表明它可能重建 body —— 依然先把正文抓到手再说。
*/
let capturedSource = null;
function captureSource(force) {
if (!force && capturedSource !== null) return capturedSource;
let text = '';
try {
const pre = document.querySelector('body > pre') || document.querySelector('pre');
if (pre) text = pre.textContent || '';
if (!text && location.protocol === 'file:' && document.body && !document.body.firstElementChild) {
text = document.body.textContent || '';
}
} catch (e) {
text = '';
}
// 空读不写入缓存,否则会把后面的 load / 重新读取挡住
if (text) capturedSource = text;
return text || capturedSource || '';
}
function killNativeViewer() {
try {
document.querySelectorAll('.json-formatter-container').forEach(function (el) { el.remove(); });
} catch (e) { /* 忽略 */ }
}
/* ============================================================
* 1. 小工具
* ============================================================ */
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function extOf(name) {
const i = String(name || '').lastIndexOf('.');
return i < 0 ? '' : String(name).slice(i + 1).toLowerCase();
}
function baseName(name) {
return String(name || 'data').replace(/\.[^.]+$/, '') || 'data';
}
function fmtSize(n) {
if (n == null) return '';
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0, v = Number(n);
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
return (i === 0 ? v : v.toFixed(v < 10 ? 2 : 1)) + ' ' + units[i];
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(function () {
try { a.remove(); } catch (e) { /* 忽略 */ }
URL.revokeObjectURL(url);
}, 4000);
}
/** UTF-8 严格解码,失败退 gb18030(中文 Windows 导出的文件常见) */
function decodeText(u8) {
try {
return new TextDecoder('utf-8', { fatal: true }).decode(u8);
} catch (e) {
try { return new TextDecoder('gb18030').decode(u8); } catch (e2) { return new TextDecoder('utf-8').decode(u8); }
}
}
function stripBom(s) {
return s.charCodeAt(0) === 0xFEFF ? s.slice(1) : s;
}
/* ============================================================
* 1b. YAML 解析封装(内嵌 js-yaml,见文件末尾的 bundle)
* ============================================================ */
const YAML_LIB = (function () {
const exports = undefined, module = undefined, define = undefined; // 强制 UMD 走 globalThis 分支
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).jsyaml={})}(this,function(e){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var n,r,i,o,a,l,u,c,s,p,f,d,h,m,g,y,v,b,A,k,w,x,C,S,I,T,O,j,M,F,E,N,L,_,D,U,q,Y,K,R,H,P,B,W,$={},G={},Z={};function V(){if(n)return Z;function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e){return null==e}return n=1,Z.isNothing=t,Z.isObject=function(t){return"object"===e(t)&&null!==t},Z.toArray=function(e){return Array.isArray(e)?e:t(e)?[]:[e]},Z.repeat=function(e,t){for(var n="",r=0;r=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function r(e){return e>=48&&e<=55}function i(e){return e>=48&&e<=57}function o(e){var t=e,n=1,r=t[0];if("-"!==r&&"+"!==r||("-"===r&&(n=-1),r=(t=t.slice(1))[0]),"0"===t)return 0;if("0"===r){if("b"===t[1])return n*parseInt(t.slice(2),2);if("x"===t[1])return n*parseInt(t.slice(2),16);if("o"===t[1])return n*parseInt(t.slice(2),8)}return n*parseInt(t,10)}return x=new t("tag:yaml.org,2002:int",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t=e.length,a=0,l=!1;if(!t)return!1;var u=e[a];if("-"!==u&&"+"!==u||(u=e[++a]),"0"===u){if(a+1===t)return!0;if("b"===(u=e[++a])){for(a++;a=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0o"+e.toString(8):"-0o"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}})}function ae(){if(I)return S;I=1;var e=V(),t=z(),n=new RegExp("^(?:[-+]?(?:[0-9]+)(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),r=new RegExp("^(?:[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");var i=/^[-+]?[0-9]+e/;return S=new t("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(e){return null!==e&&(!!n.test(e)&&(!!isFinite(parseFloat(e,10))||r.test(e)))},construct:function(e){var t=e.toLowerCase(),n="-"===t[0]?-1:1;return"+-".indexOf(t[0])>=0&&(t=t.slice(1)),".inf"===t?1===n?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===t?NaN:n*parseFloat(t,10)},predicate:function(t){return"[object Number]"===Object.prototype.toString.call(t)&&(t%1!=0||e.isNegativeZero(t))},represent:function(t,n){if(isNaN(t))switch(n){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===t)switch(n){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===t)switch(n){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(e.isNegativeZero(t))return"-0.0";var r=t.toString(10);return i.test(r)?r.replace("e",".e"):r},defaultStyle:"lowercase"})}function le(){return O?T:(O=1,T=ne().extend({implicit:[re(),ie(),oe(),ae()]}))}function ue(){return M?j:(M=1,j=le())}function ce(){if(E)return F;E=1;var e=z(),t=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),n=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");return F=new e("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function(e){return null!==e&&(null!==t.exec(e)||null!==n.exec(e))},construct:function(e){var r=0,i=null,o=t.exec(e);if(null===o&&(o=n.exec(e)),null===o)throw new Error("Date resolve error");var a=+o[1],l=+o[2]-1,u=+o[3];if(!o[4])return new Date(Date.UTC(a,l,u));var c=+o[4],s=+o[5],p=+o[6];if(o[7]){for(r=o[7].slice(0,3);r.length<3;)r+="0";r=+r}o[9]&&(i=6e4*(60*+o[10]+ +(o[11]||0)),"-"===o[9]&&(i=-i));var f=new Date(Date.UTC(a,l,u,c,s,p,r));return i&&f.setTime(f.getTime()-i),f},instanceOf:Date,represent:function(e){return e.toISOString()}})}function se(){if(L)return N;L=1;var e=z();return N=new e("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(e){return"<<"===e||null===e}})}function pe(){if(D)return _;D=1;var e=z(),t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r";return _=new e("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(e){if(null===e)return!1;for(var n=0,r=e.length,i=t,o=0;o64)){if(a<0)return!1;n+=6}}return n%8==0},construct:function(e){for(var n=e.replace(/[\r\n=]/g,""),r=n.length,i=t,o=0,a=[],l=0;l>16&255),a.push(o>>8&255),a.push(255&o)),o=o<<6|i.indexOf(n.charAt(l));var u=r%4*6;return 0===u?(a.push(o>>16&255),a.push(o>>8&255),a.push(255&o)):18===u?(a.push(o>>10&255),a.push(o>>2&255)):12===u&&a.push(o>>4&255),new Uint8Array(a)},predicate:function(e){return"[object Uint8Array]"===Object.prototype.toString.call(e)},represent:function(e){for(var n="",r=0,i=e.length,o=t,a=0;a>18&63],n+=o[r>>12&63],n+=o[r>>6&63],n+=o[63&r]),r=(r<<8)+e[a];var l=i%3;return 0===l?(n+=o[r>>18&63],n+=o[r>>12&63],n+=o[r>>6&63],n+=o[63&r]):2===l?(n+=o[r>>10&63],n+=o[r>>4&63],n+=o[r<<2&63],n+=o[64]):1===l&&(n+=o[r>>2&63],n+=o[r<<4&63],n+=o[64],n+=o[64]),n}})}function fe(){if(q)return U;q=1;var e=z(),t=Object.prototype.hasOwnProperty,n=Object.prototype.toString;return U=new e("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(e){if(null===e)return!0;for(var r={},i=e,o=0,a=i.length;ol&&(t=r-l+(o=" ... ").length),n-r>l&&(n=r+l-(a=" ...").length),{str:o+e.slice(t,n).replace(/\t/g,"→")+a,pos:r-t+o.length}}function n(t,n){return e.repeat(" ",n-t.length)+t}return o=function(r,i){if(i=Object.create(i||null),!r.buffer)return null;i.maxLength||(i.maxLength=79),"number"!=typeof i.indent&&(i.indent=1),"number"!=typeof i.linesBefore&&(i.linesBefore=3),"number"!=typeof i.linesAfter&&(i.linesAfter=2);for(var o,a=/\r?\n|\r|\0/g,l=[0],u=[],c=-1;o=a.exec(r.buffer);)u.push(o.index),l.push(o.index+o[0].length),r.position<=o.index&&c<0&&(c=l.length-2);c<0&&(c=l.length-1);for(var s="",p=Math.min(r.line+i.linesAfter,u.length).toString().length,f=i.maxLength-(i.indent+p+3),d=1;d<=i.linesBefore&&!(c-d<0);d++){var h=t(r.buffer,l[c-d],u[c-d],r.position-(l[c]-l[c-d]),f);s=e.repeat(" ",i.indent)+n((r.line-d+1).toString(),p)+" | "+h.str+"\n"+s}var m=t(r.buffer,l[c],u[c],r.position,f);s+=e.repeat(" ",i.indent)+n((r.line+1).toString(),p)+" | "+m.str+"\n",s+=e.repeat("-",i.indent+p+3+m.pos)+"^\n";for(var g=1;g<=i.linesAfter&&!(c+g>=u.length);g++){var y=t(r.buffer,l[c+g],u[c+g],r.position-(l[c]-l[c+g]),f);s+=e.repeat(" ",i.indent)+n((r.line+g+1).toString(),p)+" | "+y.str+"\n"}return s.replace(/\n$/,"")}}(),i=me(),l=Object.prototype.hasOwnProperty,u=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,c=/[\x85\u2028\u2029]/,s=/[,\[\]{}]/,p=/^(?:!|!!|![0-9A-Za-z-]+!)$/,f=/^(?:!|[^,\[\]{}])(?:%[0-9a-f]{2}|[0-9a-z\-#;/?:@&=+$,_.!~*'()\[\]])*$/i;function d(e){return Object.prototype.toString.call(e)}function h(e){return 10===e||13===e}function m(e){return 9===e||32===e}function g(e){return 9===e||32===e||10===e||13===e}function y(e){return 44===e||91===e||93===e||123===e||125===e}function v(e){if(e>=48&&e<=57)return e-48;var t=32|e;return t>=97&&t<=102?t-97+10:-1}function b(e){return 120===e?2:117===e?4:85===e?8:0}function A(e){return e>=48&&e<=57?e-48:-1}function k(e){switch(e){case 48:return"\0";case 97:return"";case 98:return"\b";case 116:case 9:return"\t";case 110:return"\n";case 118:return"\v";case 102:return"\f";case 114:return"\r";case 101:return"";case 32:return" ";case 34:return'"';case 47:return"/";case 92:return"\\";case 78:return"
";case 95:return" ";case 76:return"\u2028";case 80:return"\u2029";default:return""}}function w(e){return e<=65535?String.fromCharCode(e):String.fromCharCode(55296+(e-65536>>10),56320+(e-65536&1023))}function x(e,t,n){"__proto__"===t?Object.defineProperty(e,t,{configurable:!0,enumerable:!0,writable:!0,value:n}):e[t]=n}for(var C=new Array(256),S=new Array(256),I=0;I<256;I++)C[I]=k(I)?1:0,S[I]=k(I);function T(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||i,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.maxDepth="number"==typeof t.maxDepth?t.maxDepth:100,this.maxTotalMergeKeys="number"==typeof t.maxTotalMergeKeys?t.maxTotalMergeKeys:1e4,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.depth=0,this.totalMergeKeys=0,this.firstTabInLine=-1,this.documents=[],this.anchorMapTransactions=[]}function O(e,t){var i={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return i.snippet=r(i),new n(t,i)}function j(e,t){throw O(e,t)}function M(e,t){e.onWarning&&e.onWarning.call(null,O(e,t))}function F(e,t,n){var r=e.anchorMapTransactions;if(0!==r.length){var i=r[r.length-1];l.call(i,t)||(i[t]={existed:l.call(e.anchorMap,t),value:e.anchorMap[t]})}e.anchorMap[t]=n}function E(e){return{position:e.position,line:e.line,lineStart:e.lineStart,lineIndent:e.lineIndent,firstTabInLine:e.firstTabInLine,tag:e.tag,anchor:e.anchor,kind:e.kind,result:e.result}}function N(e,t){e.position=t.position,e.line=t.line,e.lineStart=t.lineStart,e.lineIndent=t.lineIndent,e.firstTabInLine=t.firstTabInLine,e.tag=t.tag,e.anchor=t.anchor,e.kind=t.kind,e.result=t.result}var L={YAML:function(e,t,n){null!==e.version&&j(e,"duplication of %YAML directive"),1!==n.length&&j(e,"YAML directive accepts exactly one argument");var r=/^([0-9]+)\.([0-9]+)$/.exec(n[0]);null===r&&j(e,"ill-formed argument of the YAML directive");var i=parseInt(r[1],10),o=parseInt(r[2],10);1!==i&&j(e,"unacceptable YAML version of the document"),e.version=n[0],e.checkLineBreaks=o<2,1!==o&&2!==o&&M(e,"unsupported YAML version of the document")},TAG:function(e,t,n){var r;2!==n.length&&j(e,"TAG directive accepts exactly two arguments");var i=n[0];r=n[1],p.test(i)||j(e,"ill-formed tag handle (first argument) of the TAG directive"),l.call(e.tagMap,i)&&j(e,'there is a previously declared suffix for "'+i+'" tag handle'),f.test(r)||j(e,"ill-formed tag prefix (second argument) of the TAG directive");try{r=decodeURIComponent(r)}catch(t){j(e,"tag prefix is malformed: "+r)}e.tagMap[i]=r}};function _(e,t,n,r){if(t=32&&l<=1114111||j(e,"expected valid JSON character")}else u.test(i)&&j(e,"the stream contains non-printable characters");e.result+=i}}function D(e){e.totalMergeKeys++,-1!==e.maxTotalMergeKeys&&e.totalMergeKeys>e.maxTotalMergeKeys&&j(e,"merge keys exceeded maxTotalMergeKeys ("+e.maxTotalMergeKeys+")")}function U(e,n,r,i){t.isObject(r)||j(e,"cannot merge mappings; the provided source object is unacceptable"),D(e);for(var o=Object.keys(r),a=0,u=o.length;a100&&j(t,"abnormal merge sequence size");for(var h=0,m=a.length;h1&&(e.result+=t.repeat("\n",n-1))}function P(e,t){var n=e.tag,r=e.anchor,i=[],o=!1;if(-1!==e.firstTabInLine)return!1;null!==e.anchor&&F(e,e.anchor,i);for(var a=e.input.charCodeAt(e.position);0!==a&&(-1!==e.firstTabInLine&&(e.position=e.firstTabInLine,j(e,"tab characters must not be used in indentation")),45===a);){if(!g(e.input.charCodeAt(e.position+1)))break;if(o=!0,e.position++,K(e,!0,-1)&&e.lineIndent<=t)i.push(null),a=e.input.charCodeAt(e.position);else{var l=e.line;if(Q(e,t,3,!1,!0),i.push(e.result),K(e,!0,-1),a=e.input.charCodeAt(e.position),(e.line===l||e.lineIndent>t)&&0!==a)j(e,"bad indentation of a sequence entry");else if(e.lineIndentt)&&(h&&(i=e.line,o=e.lineStart,a=e.position),Q(e,t,4,!0,r)&&(h?f=e.result:d=e.result),h||(q(e,c,s,p,f,d,i,o,a),p=f=d=null),K(e,!0,-1),v=e.input.charCodeAt(e.position)),(e.line===A||e.lineIndent>t)&&0!==v)j(e,"bad indentation of a mapping entry");else if(e.lineIndent=0;r-=1){var i=t[n[r]];i.existed?e.anchorMap[n[r]]=i.value:delete e.anchorMap[n[r]]}}(e),N(e,i),!1)}function Q(e,n,r,i,o){var a,u,c,s,p,f=1,d=!1,k=!1,x=null;e.depth>=e.maxDepth&&j(e,"nesting exceeded maxDepth ("+e.maxDepth+")"),e.depth+=1,null!==e.listener&&e.listener("open",e),e.tag=null,e.anchor=null,e.kind=null,e.result=null;var I=a=u=4===r||3===r;if(i&&K(e,!0,-1)&&(d=!0,e.lineIndent>n?f=1:e.lineIndent===n?f=0:e.lineIndentn?f=1:e.lineIndent===n?f=0:e.lineIndent=0))break;0===i?j(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):l?j(e,"repeat of an indentation width identifier"):(u=n+i-1,l=!0)}if(m(p)){do{p=e.input.charCodeAt(++e.position)}while(m(p));if(35===p)do{p=e.input.charCodeAt(++e.position)}while(!h(p)&&0!==p)}for(;0!==p;){for(Y(e),e.lineIndent=0,p=e.input.charCodeAt(e.position);(!l||e.lineIndentu&&(u=e.lineIndent),h(p))c++;else{if(l||0!==u||j(e,"missing indentation for block scalar"),e.lineIndent0){for(var a=i,l=0;a>0;a--)(i=v(o=e.input.charCodeAt(++e.position)))>=0?l=(l<<4)+i:j(e,"expected hexadecimal character");e.result+=w(l),e.position++}else j(e,"unknown escape sequence");n=r=e.position}else h(o)?(_(e,n,r,!0),H(e,K(e,!1,t)),n=r=e.position):e.position===e.lineStart&&R(e)?j(e,"unexpected end of the document within a double quoted scalar"):(e.position++,m(o)||(r=e.position))}j(e,"unexpected end of the stream within a double quoted scalar")}(e,s)?k=!0:!function(e){var t=e.input.charCodeAt(e.position);if(42!==t)return!1;t=e.input.charCodeAt(++e.position);for(var n=e.position;0!==t&&!g(t)&&!y(t);)t=e.input.charCodeAt(++e.position);e.position===n&&j(e,"name of an alias node must contain at least one character");var r=e.input.slice(n,e.position);return l.call(e.anchorMap,r)||j(e,'unidentified alias "'+r+'"'),e.result=e.anchorMap[r],K(e,!0,-1),!0}(e)?function(e,t,n){var r,i,o,a,l,u,c=e.kind,s=e.result,p=e.input.charCodeAt(e.position);if(g(p)||y(p)||35===p||38===p||42===p||33===p||124===p||62===p||39===p||34===p||37===p||64===p||96===p)return!1;if(63===p||45===p){var f=e.input.charCodeAt(e.position+1);if(g(f)||n&&y(f))return!1}for(e.kind="scalar",e.result="",r=i=e.position,o=!1;0!==p;){if(58===p){var d=e.input.charCodeAt(e.position+1);if(g(d)||n&&y(d))break}else if(35===p){if(g(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&R(e)||n&&y(p))break;if(h(p)){if(a=e.line,l=e.lineStart,u=e.lineIndent,K(e,!1,-1),e.lineIndent>=t){o=!0,p=e.input.charCodeAt(e.position);continue}e.position=i,e.line=a,e.lineStart=l,e.lineIndent=u;break}}o&&(_(e,r,i,!1),H(e,e.line-a),r=i=e.position,o=!1),m(p)||(i=e.position+1),p=e.input.charCodeAt(++e.position)}return _(e,r,i,!1),!!e.result||(e.kind=c,e.result=s,!1)}(e,s,1===r)&&(k=!0,null===e.tag&&(e.tag="?")):(k=!0,null===e.tag&&null===e.anchor||j(e,"alias node should not have any properties")),null!==e.anchor&&F(e,e.anchor,e.result)}else 0===f&&(k=u&&P(e,p));if(null===e.tag)null!==e.anchor&&F(e,e.anchor,e.result);else if("?"===e.tag){null!==e.result&&"scalar"!==e.kind&&j(e,'unacceptable node kind for !> tag; it should be "scalar", not "'+e.kind+'"');for(var N=0,L=e.implicitTypes.length;N"),null!==e.result&&c.kind!==e.kind&&j(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+c.kind+'", not "'+e.kind+'"'),c.resolve(e.result,e.tag)?(e.result=c.construct(e.result,e.tag),null!==e.anchor&&F(e,e.anchor,e.result)):j(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return null!==e.listener&&e.listener("close",e),e.depth-=1,null!==e.tag||null!==e.anchor||k}function X(e){var t,n=e.position,r=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);0!==(t=e.input.charCodeAt(e.position))&&(K(e,!0,-1),t=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==t));){r=!0,t=e.input.charCodeAt(++e.position);for(var i=e.position;0!==t&&!g(t);)t=e.input.charCodeAt(++e.position);var o=e.input.slice(i,e.position),a=[];for(o.length<1&&j(e,"directive name must not be less than one character in length");0!==t;){for(;m(t);)t=e.input.charCodeAt(++e.position);if(35===t){do{t=e.input.charCodeAt(++e.position)}while(0!==t&&!h(t));break}if(h(t))break;for(i=e.position;0!==t&&!g(t);)t=e.input.charCodeAt(++e.position);a.push(e.input.slice(i,e.position))}0!==t&&Y(e),l.call(L,o)?L[o](e,o,a):M(e,'unknown document directive "'+o+'"')}K(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,K(e,!0,-1)):r&&j(e,"directives end mark is expected"),Q(e,e.lineIndent-1,4,!1,!0),K(e,!0,-1),e.checkLineBreaks&&c.test(e.input.slice(n,e.position))&&M(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&R(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,K(e,!0,-1)):e.position=32&&e<=126||e>=161&&e<=55295&&8232!==e&&8233!==e||e>=57344&&e<=65533&&e!==a||e>=65536&&e<=1114111}function g(e){return m(e)&&e!==a&&13!==e&&10!==e}function y(e,t,n){var r=g(e),i=r&&!h(e);return(n?r:r&&44!==e&&91!==e&&93!==e&&123!==e&&125!==e)&&35!==e&&!(58===t&&!i)||g(t)&&!h(t)&&35===e||58===t&&i}function v(e,t){var n,r=e.charCodeAt(t);return r>=55296&&r<=56319&&t+1=56320&&n<=57343?1024*(r-55296)+n-56320+65536:r}function b(e){return/^\n* /.test(e)}function A(e,t,n,r,i,o,l,u){var c,s,p=0,f=null,d=!1,g=!1,A=-1!==r,k=-1,w=m(s=v(e,0))&&s!==a&&!h(s)&&45!==s&&63!==s&&58!==s&&44!==s&&91!==s&&93!==s&&123!==s&&125!==s&&35!==s&&38!==s&&42!==s&&33!==s&&124!==s&&61!==s&&62!==s&&39!==s&&34!==s&&37!==s&&64!==s&&96!==s&&function(e){return!h(e)&&58!==e}(v(e,e.length-1));if(t||l)for(c=0;c=65536?c+=2:c++){if(!m(p=v(e,c)))return 5;w=w&&y(p,f,u),f=p}else{for(c=0;c=65536?c+=2:c++){if(10===(p=v(e,c)))d=!0,A&&(g=g||c-k-1>r&&" "!==e[k+1],k=c);else if(!m(p))return 5;w=w&&y(p,f,u),f=p}g=g||A&&c-k-1>r&&" "!==e[k+1]}return d||g?n>9&&b(e)?5:l?2===o?5:2:g?4:3:!w||l||i(e)?2===o?5:2:1}function k(e,t,r,i,o){e.dump=function(){if(0===t.length)return 2===e.quotingType?'""':"''";if(!e.noCompatMode&&(-1!==u.indexOf(t)||c.test(t)))return 2===e.quotingType?'"'+t+'"':"'"+t+"'";var a=e.indent*Math.max(1,r),p=-1===e.lineWidth?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-a),d=i||e.flowLevel>-1&&r>=e.flowLevel;switch(A(t,d,e.indent,p,function(t){return function(e,t){for(var n=0,r=e.implicitTypes.length;n"+w(t,e.indent)+x(f(function(e,t){var n,r,i=/(\n+)([^\n]*)/g,o=(l=e.indexOf("\n"),l=-1!==l?l:e.length,i.lastIndex=l,C(e.slice(0,l),t)),a="\n"===e[0]||" "===e[0];var l;for(;r=i.exec(e);){var u=r[1],c=r[2];n=" "===c[0],o+=u+(a||n||""===c?"":"\n")+C(c,t),a=n}return o}(t,p),a));case 5:return'"'+function(e){for(var t="",n=0,r=0;r=65536?r+=2:r++){n=v(e,r);var i=l[n];!i&&m(n)?(t+=e[r],n>=65536&&(t+=e[r+1])):t+=i||s(n)}return t}(t)+'"';default:throw new n("impossible error: invalid scalar style")}}()}function w(e,t){var n=b(e)?String(t):"",r="\n"===e[e.length-1];return n+(r&&("\n"===e[e.length-2]||"\n"===e)?"+":r?"":"-")+"\n"}function x(e){return"\n"===e[e.length-1]?e.slice(0,-1):e}function C(e,t){if(""===e||" "===e[0])return e;for(var n,r,i=/ [^ ]/g,o=0,a=0,l=0,u="";n=i.exec(e);)(l=n.index)-o>t&&(r=a>o?a:l,u+="\n"+e.slice(o,r),o=r+1),a=l;return u+="\n",e.length-o>t&&a>o?u+=e.slice(o,a)+"\n"+e.slice(a+1):u+=e.slice(o),u.slice(1)}function S(e,t,n,r){for(var i="",o=e.tag,a=0,l=n.length;a tag resolver accepts not "'+p+'" style');f=s.represent[p](r,p)}t.dump=f}return!0}}return!1}function T(e,t,r,o,a,l,u){e.tag=null,e.dump=r,I(e,r,!1)||I(e,r,!0);var c=i.call(e.dump),s=o;o&&(o=e.flowLevel<0||e.flowLevel>t);var p,f,h="[object Object]"===c||"[object Array]"===c;if(h&&(f=-1!==(p=e.duplicates.indexOf(r))),(null!==e.tag&&"?"!==e.tag||f||2!==e.indent&&t>0)&&(a=!1),f&&e.usedDuplicates[p])e.dump="*ref_"+p;else{if(h&&f&&!e.usedDuplicates[p]&&(e.usedDuplicates[p]=!0),"[object Object]"===c)o&&0!==Object.keys(e.dump).length?(!function(e,t,r,i){var o="",a=e.tag,l=Object.keys(r);if(!0===e.sortKeys)l.sort();else if("function"==typeof e.sortKeys)l.sort(e.sortKeys);else if(e.sortKeys)throw new n("sortKeys must be a boolean or a function");for(var u=0,c=l.length;u1024;h&&(e.dump&&10===e.dump.charCodeAt(0)?s+="?":s+="? "),s+=e.dump,h&&(s+=d(e,t)),T(e,t+1,f,!0,h)&&(e.dump&&10===e.dump.charCodeAt(0)?s+=":":s+=": ",o+=s+=e.dump)}}e.tag=a,e.dump=o||"{}"}(e,t,e.dump,a),f&&(e.dump="&ref_"+p+e.dump)):(!function(e,t,n){for(var r="",i=e.tag,o=Object.keys(n),a=0,l=o.length;a1024&&(u+="? "),u+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),T(e,t,s,!1,!1)&&(r+=u+=e.dump))}e.tag=i,e.dump="{"+r+"}"}(e,t,e.dump),f&&(e.dump="&ref_"+p+" "+e.dump));else if("[object Array]"===c)o&&0!==e.dump.length?(e.noArrayIndent&&!u&&t>0?S(e,t-1,e.dump,a):S(e,t,e.dump,a),f&&(e.dump="&ref_"+p+e.dump)):(!function(e,t,n){for(var r="",i=e.tag,o=0,a=n.length;o",e.dump=m+" "+e.dump}}return!0}function O(e,t){var n=[],r=[];j(e,n,r);for(var i=r.length,o=0;o 0) out += '---\n';
out += YAML_LIB.dump(arr[i] === undefined ? null : arr[i], opts);
}
return out;
}
/** 从 YAMLException 提取行/列(1-based)与原因 */
function parseErrorInfo(e) {
const mark = e && e.mark;
return {
line: mark ? mark.line + 1 : -1,
col: mark ? mark.column + 1 : -1,
reason: (e && e.reason) || (e && e.message) || String(e),
message: (e && e.message) || String(e)
};
}
/* ============================================================
* 2. 树模型 / 搜索匹配
* ============================================================ */
/** 节点自身文本(键名或标量值)是否命中搜索词 */
function nodeSelfMatch(keyLabel, v, q) {
if (!q) return true;
if (keyLabel != null && String(keyLabel).toLowerCase().indexOf(q) >= 0) return true;
if (v === null || typeof v !== 'object') {
return String(v == null ? '' : v).toLowerCase().indexOf(q) >= 0;
}
return false;
}
/** 子树内是否存在任何命中(键名或值) */
function subtreeHasMatch(v, q) {
if (!q) return true;
if (v === null || typeof v !== 'object') {
return String(v == null ? '' : v).toLowerCase().indexOf(q) >= 0;
}
if (Array.isArray(v)) {
for (let i = 0; i < v.length; i++) if (subtreeHasMatch(v[i], q)) return true;
return false;
}
const keys = Object.keys(v);
for (let i = 0; i < keys.length; i++) {
if (keys[i].toLowerCase().indexOf(q) >= 0) return true;
if (subtreeHasMatch(v[keys[i]], q)) return true;
}
return false;
}
/** 纯文本搜索高亮:按原始文本切段,段内 escape,避免 & 实体错位 */
function highlight(text, q) {
const s = String(text == null ? '' : text);
if (!q) return escapeHtml(s);
const low = s.toLowerCase();
const out = [];
let i = 0;
while (i < s.length) {
const hit = low.indexOf(q, i);
if (hit < 0) { out.push(escapeHtml(s.slice(i))); break; }
if (hit > i) out.push(escapeHtml(s.slice(i, hit)));
out.push('' + escapeHtml(s.slice(hit, hit + q.length)) + '');
i = hit + q.length;
}
return out.join('');
}
/* ============================================================
* 3. 样式
* ============================================================ */
const CSS = [
'html,body{height:100%;}',
'body{margin:0;padding:0;overflow:hidden;background:#1e1f29;color:#f8f8f2;',
'font:13px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",Helvetica,Arial,sans-serif;}',
'.yr-root{display:flex;flex-direction:column;height:100vh;width:100vw;}',
/* 顶部工具条 */
'.yr-topbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 12px;flex:0 0 auto;',
'position:relative;background:#282a36;border-bottom:1px solid #44475a;}',
'.yr-badge{display:inline-flex;align-items:center;justify-content:center;height:21px;padding:0 7px;border-radius:5px;',
'font-size:10.5px;font-weight:700;letter-spacing:.5px;background:rgba(80,250,123,.14);border:1px solid rgba(80,250,123,.45);color:#50fa7b;}',
'.yr-name{font-weight:600;font-size:13px;max-width:24vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
'.yr-meta{color:#6272a4;font-size:12px;white-space:nowrap;}',
'.yr-dirty{color:#f1fa8c;font-size:12px;white-space:nowrap;}',
'.yr-grow{flex:1 1 auto;}',
'.yr-btn{appearance:none;border:1px solid #44475a;background:#313244;color:#f8f8f2;border-radius:6px;',
'padding:4px 10px;font-size:12px;line-height:1.5;cursor:pointer;white-space:nowrap;transition:.14s;}',
'.yr-btn:hover{border-color:#50fa7b;color:#50fa7b;}',
'.yr-btn:active{transform:translateY(1px);}',
'.yr-btn.primary{background:rgba(80,250,123,.16);border-color:rgba(80,250,123,.55);color:#50fa7b;font-weight:600;}',
'.yr-sel{appearance:none;background:#313244;color:#f8f8f2;border:1px solid #44475a;border-radius:6px;',
'padding:4px 6px;font-size:12px;cursor:pointer;}',
'.yr-search{background:#313244;border:1px solid #44475a;border-radius:6px;color:#f8f8f2;',
'padding:4px 9px;font-size:12px;width:180px;outline:none;}',
'.yr-search:focus{border-color:#50fa7b;}',
'.yr-search::placeholder{color:#6272a4;}',
/* 分屏 */
'.yr-split{flex:1 1 auto;display:flex;min-height:0;}',
'.yr-pane{overflow:auto;box-sizing:border-box;height:100%;}',
'.yr-pane-left{flex:1 1 0;min-width:0;background:#21222c;}',
'.yr-pane-right{flex:1 1 0;min-width:0;background:#282a36;border-left:0;}',
'.yr-editor{width:100%;height:100%;background:transparent;color:#f8f8f2;border:none;resize:none;outline:none;padding:12px 14px;',
'box-sizing:border-box;white-space:pre;overflow-wrap:normal;tab-size:2;',
'font:12.5px/1.6 "SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;}',
'.yr-splitter{flex:0 0 6px;background:#44475a;cursor:col-resize;position:relative;z-index:5;transition:background .15s;}',
'.yr-splitter:hover,.yr-splitter.dragging{background:#50fa7b;}',
'.yr-splitter::after{content:"⋮";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#888;font-size:16px;line-height:1;}',
/* 树视图 */
'.yr-treewrap{padding:12px 14px;font-family:"SFMono-Regular",Consolas,Menlo,monospace;font-size:12.5px;line-height:1.7;}',
'.yr-doclabel{display:block;color:#8be9fd;font-weight:600;border-bottom:1px dashed #44475a;margin:10px 0 4px;padding-bottom:2px;}',
'.yr-doclabel:first-child{margin-top:0;}',
'.yr-tree details{margin-left:14px;border-left:1px dotted #44475a;padding-left:10px;}',
'.yr-tree summary{cursor:pointer;list-style:none;outline:none;white-space:pre-wrap;word-break:break-all;}',
'.yr-tree summary::-webkit-details-marker{display:none;}',
'.yr-tree summary::before{content:"▸";display:inline-block;width:12px;color:#6272a4;transition:transform .12s;}',
'.yr-tree details[open]>summary::before{transform:rotate(90deg);}',
'.yr-line{white-space:pre-wrap;word-break:break-all;}',
'.yr-k{color:#bd93f9;}',
'.yr-s{color:#50fa7b;}',
'.yr-n{color:#ffb86c;}',
'.yr-b{color:#ff79c6;}',
'.yr-null{color:#6272a4;font-style:italic;}',
'.yr-cnt{color:#6272a4;font-size:11px;margin-left:6px;}',
'.yr-dim{opacity:.32;}',
'.yr-tree mark{background:rgba(241,250,140,.35);color:#f1fa8c;border-radius:2px;padding:0 1px;}',
/* 底栏 */
'.yr-foot{flex:0 0 auto;display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:5px 12px;',
'background:#282a36;border-top:1px solid #44475a;color:#6272a4;font-size:12px;}',
'.yr-foot b{color:#f8f8f2;font-weight:600;}',
/* 空态 / 错误 */
'.yr-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:9px;',
'color:#6272a4;text-align:center;padding:40px;}',
'.yr-empty .yr-big{font-size:40px;line-height:1;}',
'.yr-err{margin:14px;padding:12px 14px;border-radius:9px;background:rgba(255,85,85,.1);',
'border:1px solid rgba(255,85,85,.45);color:#ffb3b3;font-size:12.5px;}',
'.yr-err b{display:block;margin-bottom:6px;color:#ff6b6b;font-size:13px;}',
'.yr-err pre{margin:8px 0 0;padding:8px 10px;background:rgba(0,0,0,.3);border-radius:6px;overflow:auto;',
'font:12px/1.6 Consolas,Menlo,monospace;color:#cfd3e0;}',
'.yr-err pre .yr-errline{color:#ff8f8f;font-weight:600;}',
/* 拖放遮罩 */
'#yr-drop-overlay{position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483646;display:flex;align-items:center;',
'justify-content:center;background:rgba(40,42,54,.95);opacity:0;pointer-events:none;transition:opacity .2s;}',
'#yr-drop-overlay.show{opacity:1;pointer-events:auto;}',
'#yr-drop-overlay .yr-box{text-align:center;border:3px dashed #50fa7b;padding:52px 74px;border-radius:22px;background:rgba(30,31,41,.8);}',
'#yr-drop-overlay .yr-ico{font-size:64px;margin-bottom:16px;}',
'#yr-drop-overlay .yr-tx{font-size:24px;font-weight:600;margin-bottom:8px;}',
'#yr-drop-overlay .yr-hi{font-size:14px;color:#6272a4;}',
/* 滚动条 */
'.yr-pane::-webkit-scrollbar{width:10px;height:10px;}',
'.yr-pane::-webkit-scrollbar-track{background:#1e1f29;}',
'.yr-pane::-webkit-scrollbar-thumb{background:#44475a;border-radius:5px;border:2px solid #1e1f29;}',
'.yr-pane::-webkit-scrollbar-thumb:hover{background:#6272a4;}',
'#yr-drop-overlay,#yr-drop-overlay *{box-sizing:border-box;}'
].join('\n');
let stylesInjected = false;
function injectStyles() {
if (stylesInjected) return;
stylesInjected = true;
try {
if (typeof GM_addStyle === 'function') { GM_addStyle(CSS); return; }
} catch (e) { /* 走兜底 */ }
const st = document.createElement('style');
st.textContent = CSS;
(document.head || document.documentElement).appendChild(st);
}
/** 声明当前页面归本脚本。data-reader / markdown-reader 的守护看到非自家标记会停手。 */
function claimReader() {
try { document.documentElement.dataset.fileReader = 'yaml'; } catch (e) { /* 忽略 */ }
}
function readerStillOurs() {
try { return document.documentElement.dataset.fileReader === 'yaml'; } catch (e) { return false; }
}
/* ============================================================
* 4. 界面
* ============================================================ */
function buildUi() {
const existing = document.getElementById(ROOT_ID);
if (existing && existing.parentNode === document.body) return;
if (existing) existing.remove();
// 先抓原文再动 DOM(同 data-reader 的教训:先清空就读不到 了)
if (autoTarget()) captureSource();
claimReader();
injectStyles();
document.body.innerHTML = '';
document.documentElement.style.height = '100%';
const root = document.createElement('div');
root.className = 'yr-root';
root.id = ROOT_ID;
// ---- 顶部工具条 ----
const top = document.createElement('div');
top.className = 'yr-topbar';
top.innerHTML = [
'YAML',
'data',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
''
].join('');
// ---- 分屏 ----
const split = document.createElement('div');
split.className = 'yr-split';
const left = document.createElement('div');
left.className = 'yr-pane yr-pane-left';
left.setAttribute('data-ref', 'left');
const splitter = document.createElement('div');
splitter.className = 'yr-splitter';
const right = document.createElement('div');
right.className = 'yr-pane yr-pane-right';
const editor = document.createElement('textarea');
editor.className = 'yr-editor';
editor.spellcheck = false;
editor.setAttribute('data-ref', 'editor');
editor.setAttribute('wrap', 'off');
editor.placeholder = '在此粘贴 / 编辑 YAML,左侧实时预览…';
right.appendChild(editor);
split.appendChild(left);
split.appendChild(splitter);
split.appendChild(right);
// ---- 底栏 ----
const foot = document.createElement('div');
foot.className = 'yr-foot';
foot.setAttribute('data-ref', 'foot');
root.appendChild(top);
root.appendChild(split);
root.appendChild(foot);
document.body.appendChild(root);
// ---- 拖放遮罩 ----
const overlay = document.createElement('div');
overlay.id = 'yr-drop-overlay';
overlay.innerHTML = [
'',
' Y',
' 将 YAML 文件拖放到此处',
' 支持 .yaml .yml(多文档 / 锚点别名均可)',
''
].join('');
document.body.appendChild(overlay);
root.querySelectorAll('[data-ref]').forEach(function (el) {
refs[el.getAttribute('data-ref')] = el;
});
refs.root = root;
refs.top = top;
refs.left = left;
refs.right = right;
refs.editor = editor;
refs.splitter = splitter;
refs.foot = foot;
refs.overlay = overlay;
bindUiEl();
bindUi();
}
/* ============================================================
* 5. 渲染
* ============================================================ */
function scalarHtml(v, q) {
if (v === null || v === undefined) return 'null';
if (typeof v === 'boolean') return '' + highlight(v ? 'true' : 'false', q) + '';
if (typeof v === 'number') return '' + highlight(String(v), q) + '';
const s = String(v);
if (s.indexOf('\n') >= 0) {
const first = s.split('\n')[0];
return '' + highlight(first, q) +
'⏎ ' + (s.split('\n').length) + ' 行';
}
return '' + highlight(s, q) + '';
}
/**
* 递归渲染一个节点。
* keyLabel:键名或数组下标 [0];path:折叠状态路径(\u0001 分隔)。
* 搜索时:自身命中 → 高亮;子树有命中 → 展开;都无 → 收起变暗。
*/
function renderNode(keyLabel, v, path, depth, q) {
const selfMatch = nodeSelfMatch(keyLabel, v, q);
const childMatch = q && v !== null && typeof v === 'object' ? subtreeHasMatch(v, q) : false;
const dim = q && !selfMatch && !childMatch;
const keyHtml = keyLabel != null
? '' + highlight(keyLabel, q) + '' + (typeof v === 'object' && v !== null ? '' : ': ')
: '';
if (v === null || typeof v !== 'object') {
return '' + keyHtml + scalarHtml(v, q) + '';
}
const isArray = Array.isArray(v);
const keys = isArray ? v.map(function (x, i) { return i; }) : Object.keys(v);
const countHtml = '' + (isArray ? keys.length + ' 项' : keys.length + ' 键') + '';
let open;
if (q) open = childMatch || selfMatch; // 搜索:有命中的展开
else if (state.fold[path] === 'closed') open = false;
else if (state.fold[path] === 'open') open = true;
else open = depth < 2; // 默认展开前两层
let children = '';
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const label = isArray ? '[' + k + ']' : String(k);
children += renderNode(label, v[k], path + '\u0001' + k, depth + 1, q);
}
return '' +
'' + keyHtml + countHtml + '
' + children + '';
}
function renderTree() {
const left = refs.left;
if (state.error) {
left.innerHTML = buildErrorHtml();
return;
}
if (!state.docs) {
left.innerHTML = [
'',
' Y',
' 拖入 .yaml / .yml 文件,或在右侧直接粘贴内容',
''
].join('');
return;
}
if (!state.docs.length) {
left.innerHTML = [
'',
' ∅',
' 这是一个空 YAML(没有文档内容)',
''
].join('');
return;
}
const q = state.query.trim().toLowerCase();
let html = '';
for (let d = 0; d < state.docs.length; d++) {
if (state.docs.length > 1) html += '📄 文档 ' + (d + 1) + '';
html += renderNode(null, state.docs[d], String(d), 0, q);
}
html += '';
left.innerHTML = html;
}
/** 错误面板:行列定位 + 出错行摘录(前后各一行) */
function buildErrorHtml() {
const lines = String(state.text || '').split('\n');
let snippet = '';
if (state.errLine > 0) {
const from = Math.max(0, state.errLine - 2);
const to = Math.min(lines.length, state.errLine + 1);
for (let i = from; i < to; i++) {
const mark = (i + 1) === state.errLine;
snippet += (i + 1) + ' | ' + (mark ? '' + escapeHtml(lines[i]) + '' : escapeHtml(lines[i])) + '\n';
}
}
return [
'解析失败',
escapeHtml(state.error),
snippet ? '' + snippet + '
' : '',
'左侧预览已暂停,右侧原文可继续编辑;改好后点「重新解析」。',
''
].join('');
}
function renderHead() {
const docs = state.docs;
refs.name.textContent = state.fileName;
refs.dirty.textContent = state.text === state.original ? '' : '● 未保存';
if (state.error) {
refs.meta.textContent = state.text.length.toLocaleString('en-US') + ' 字符 · 解析失败';
} else if (!docs) {
refs.meta.textContent = '';
} else {
const docN = docs.length;
const topKeys = docN === 1 && docs[0] !== null && typeof docs[0] === 'object' && !Array.isArray(docs[0])
? Object.keys(docs[0]).length : null;
refs.meta.textContent = fmtSize(state.text.length) + ' · ' +
(docN === 1 ? '1 个文档' : docN + ' 个文档') +
(topKeys != null ? ' · 顶层 ' + topKeys + ' 键' : '') +
(state.parseMs ? ' · ' + state.parseMs.toFixed(1) + ' ms' : '');
}
}
function renderFoot() {
const docs = state.docs;
if (state.error || !docs || !docs.length) { refs.foot.innerHTML = ''; return; }
refs.foot.innerHTML = [
'树节点实时预览 · ' + state.text.split('\n').length.toLocaleString('en-US') + ' 行原文',
'',
''
].join('');
}
function renderAll() {
renderHead();
renderTree();
renderFoot();
}
/* ============================================================
* 6. 解析调度 / 加载 / 保存
* ============================================================ */
function reparse() {
const text = refs.editor ? refs.editor.value : state.text;
state.text = text;
const t0 = performance.now();
try {
state.docs = yamlParseAll(text, state.fileName);
state.error = '';
state.errLine = -1;
} catch (e) {
const info = parseErrorInfo(e);
state.docs = null;
state.errLine = info.line;
state.error = (info.line > 0 ? '第 ' + info.line + ' 行 第 ' + info.col + ' 列 · ' : '') + info.reason;
}
state.parseMs = performance.now() - t0;
renderAll();
}
function scheduleReparse() {
clearTimeout(state.debounceTimer);
state.debounceTimer = setTimeout(function () { reparse(); }, PREVIEW_DEBOUNCE);
}
function loadText(text, fileName) {
if (!refs.editor) ensureUi();
state.fileName = fileName || 'data';
state.original = stripBom(String(text == null ? '' : text));
state.text = state.original;
state.query = '';
state.fold = {};
state.error = '';
state.errLine = -1;
state.parseMs = 0;
showOverlay(false);
const ta = refs.editor;
const pos = { top: ta.scrollTop, left: ta.scrollLeft };
ta.value = state.original;
ta.scrollTop = pos.top;
ta.scrollLeft = pos.left;
if (refs.search) refs.search.value = '';
reparse();
}
async function loadFile(file) {
if (!file) return;
const ext = extOf(file.name);
if (EXT_YAML.indexOf(ext) < 0) {
alert('只支持 .yaml / .yml 文件!');
return;
}
if (file.size > MAX_AUTO_PARSE) {
alert('文件超过 64 MB,暂不支持载入。请拆分文件。');
return;
}
if (state.text !== state.original && !confirm('当前内容有未保存的修改,确定丢弃并打开 ' + file.name + ' 吗?')) return;
ensureUi();
try {
const buf = await file.arrayBuffer();
loadText(decodeText(new Uint8Array(buf)), file.name);
} catch (e) {
alert('读取文件失败:' + (e && e.message ? e.message : e));
}
}
function pickFiles() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.yaml,.yml';
input.style.display = 'none';
input.addEventListener('change', function () {
const f = input.files && input.files[0];
try { input.remove(); } catch (e) { /* 忽略 */ }
if (f) loadFile(f);
});
document.body.appendChild(input);
input.click();
}
/** 按目标格式拿到最终要写出的文本;解析失败时原样写原文,绝不吞内容 */
function buildOutput(fmt) {
const target = fmt || 'origin';
if (target === 'json') {
if (state.docs && state.docs.length) {
const value = state.docs.length === 1 ? state.docs[0] : state.docs;
return { text: JSON.stringify(value, null, 2) + '\n', ext: 'json' };
}
return { text: state.text, ext: 'yaml' };
}
if (target === 'yml') {
if (state.docs && state.docs.length) {
return { text: yamlDumpAll(state.docs), ext: 'yaml' };
}
return { text: state.text, ext: 'yaml' };
}
return { text: state.text, ext: 'yaml' };
}
function save(fmt) {
const out = buildOutput(fmt);
if (!out.text) return;
const name = baseName(state.fileName) + '.' + out.ext;
const mime = out.ext === 'json' ? 'application/json;charset=utf-8' : 'application/yaml;charset=utf-8';
downloadBlob(new Blob([out.text], { type: mime }), name);
// 只有「按原格式保存」才认为已保存
if (!fmt || fmt === 'origin') {
state.original = out.text;
state.text = out.text;
if (refs.editor) refs.editor.value = out.text;
renderHead();
}
}
/* ============================================================
* 6b. 编辑器快捷缩进
* ============================================================ */
function afterEdit(ta) {
state.text = ta.value;
if (!refs.editor) return; // 无界面(控制台 / 自动化驱动)时只更新状态不渲染
renderHead();
scheduleReparse();
}
/** 光标处插入文本并收起选区 */
function insertAtCursor(ta, str) {
const s = ta.selectionStart, e = ta.selectionEnd;
ta.value = ta.value.slice(0, s) + str + ta.value.slice(e);
ta.selectionStart = ta.selectionEnd = s + str.length;
}
/**
* 选区覆盖的行(无选区 = 当前行)整体加/减一级缩进(2 空格)。
* 空行不动;减缩进最多去掉行首 2 空格。
*/
function indentSelection(ta, delta) {
const v = ta.value;
const s = ta.selectionStart, e = ta.selectionEnd;
const lineStart = v.lastIndexOf('\n', s - 1) + 1;
let lineEnd = v.indexOf('\n', e);
if (lineEnd < 0) lineEnd = v.length;
const lines = v.slice(lineStart, lineEnd).split('\n');
for (let i = 0; i < lines.length; i++) {
if (!lines[i].trim()) continue;
if (delta > 0) lines[i] = ' ' + lines[i];
else lines[i] = lines[i].replace(/^ {1,2}/, '');
}
const out = lines.join('\n');
ta.value = v.slice(0, lineStart) + out + v.slice(lineEnd);
ta.selectionStart = lineStart;
ta.selectionEnd = lineStart + out.length;
afterEdit(ta);
}
/**
* 回车自动续进:
* · 继承当前行缩进
* · 行尾是 key: / - / | / > 时额外 +2 空格(进入子级 / 块标量正文)
* · 当前行只有 "- " 时也 +2(列表项内容对齐)
*/
function enterAutoIndent(ta) {
const v = ta.value;
const s = ta.selectionStart, e = ta.selectionEnd;
const lineStart = v.lastIndexOf('\n', s - 1) + 1;
const line = v.slice(lineStart, s);
const indent = (line.match(/^[ \t]*/) || [''])[0];
// 先剥掉行尾空白再判断(Tab 插入的空格 / 手敲空格都不能影响判定)
const trimmed = line.replace(/\s+$/, '');
const extra = /[:\-|>]$/.test(trimmed) ? ' ' : '';
const ins = '\n' + indent + extra;
ta.value = v.slice(0, s) + ins + v.slice(e);
ta.selectionStart = ta.selectionEnd = s + ins.length;
afterEdit(ta);
}
/* ============================================================
* 7. 事件
* ============================================================ */
function onAction(act) {
switch (act) {
case 'save': save(undefined); break;
case 'reparse': reparse(); break;
case 'revert':
if (state.text !== state.original && !confirm('确定丢弃所有修改,回到载入时的内容吗?')) return;
if (refs.editor) refs.editor.value = state.original;
reparse();
break;
case 'indent': if (refs.editor) indentSelection(refs.editor, 1); break;
case 'outdent': if (refs.editor) indentSelection(refs.editor, -1); break;
case 'pick': pickFiles(); break;
default: break;
}
}
function bindUi() {
// document/window 级监听只挂一次(remount 后不重复)
if (bindUi._done) return;
bindUi._done = true;
// 顶部按钮统一委托
document.addEventListener('click', function (e) {
const el = e.target && e.target.closest ? e.target.closest('[data-act]') : null;
if (!el) return;
const act = el.dataset.act;
if (act === 'save' || act === 'reparse' || act === 'revert' || act === 'indent' || act === 'outdent' || act === 'pick') {
e.preventDefault();
onAction(act);
}
});
// 树折叠状态:details 的 toggle 不冒泡,用捕获监听接住
document.addEventListener('toggle', function (e) {
const t = e.target;
if (!t || !t.dataset || t.dataset.path === undefined) return;
state.fold[t.dataset.path] = t.open ? 'open' : 'closed';
}, true);
// 点击面板外什么都不做(没有浮动面板),占位保持结构对称
// 未保存离开提醒
window.addEventListener('beforeunload', function (e) {
if (state.text === state.original) return;
e.preventDefault();
e.returnValue = '';
return '';
});
}
/** 元素级绑定,随 buildUi 重挂(remount 后节点是新的) */
function bindUiEl() {
// 搜索(作用于左侧树)
let searchTimer = 0;
refs.search.addEventListener('input', function () {
const v = this.value;
clearTimeout(searchTimer);
searchTimer = setTimeout(function () {
state.query = v;
renderTree();
renderHead();
}, 120);
});
// 保存格式选择
refs.top.querySelector('[data-ref="savefmt"]').addEventListener('change', function () {
save(this.value === 'origin' ? undefined : this.value);
this.value = 'origin';
});
// 编辑区
refs.editor.addEventListener('input', function () {
state.text = this.value;
renderHead();
scheduleReparse();
});
refs.editor.addEventListener('keydown', function (e) {
const ta = e.target;
// Tab:插入 2 空格;多行选区整块缩进;Shift+Tab 减缩进
if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
const multiLine = ta.selectionStart !== ta.selectionEnd &&
ta.value.slice(ta.selectionStart, ta.selectionEnd).indexOf('\n') >= 0;
if (e.shiftKey) indentSelection(ta, -1);
else if (multiLine) indentSelection(ta, 1);
else insertAtCursor(ta, ' ');
return;
}
// Enter:自动续进
if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
enterAutoIndent(ta);
return;
}
// Ctrl+S 保存 / Ctrl+Enter 强制重新解析
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
e.preventDefault();
save(undefined);
return;
}
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
clearTimeout(state.debounceTimer);
reparse();
}
});
// 分割条拖动
let dragging = false, startX = 0, startW = 0;
refs.splitter.addEventListener('mousedown', function (e) {
dragging = true;
startX = e.clientX;
startW = refs.left.getBoundingClientRect().width;
refs.splitter.classList.add('dragging');
document.body.style.cursor = 'col-resize';
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (!dragging) return;
const total = refs.root.querySelector('.yr-split').getBoundingClientRect().width;
const spW = refs.splitter.getBoundingClientRect().width;
const w = Math.min(Math.max(startW + (e.clientX - startX), total * 0.1), total * 0.9 - spW);
refs.left.style.flex = '0 0 ' + w + 'px';
refs.right.style.flex = '0 0 ' + (total - w - spW) + 'px';
});
document.addEventListener('mouseup', function () {
if (!dragging) return;
dragging = false;
refs.splitter.classList.remove('dragging');
document.body.style.cursor = '';
});
}
/* ---------------- 拖放:只接管 .yaml / .yml ---------------- */
function isFileDrag(e) {
const dt = e.dataTransfer;
if (!dt) return false;
const types = dt.types || [];
for (let i = 0; i < types.length; i++) if (types[i] === 'Files') return true;
return false;
}
/** 拖动中能不能确认是 YAML 文件。dragover 阶段拿不到文件名,只用于显示遮罩。 */
function dragHasYamlFile(e) {
const dt = e.dataTransfer;
if (!dt) return false;
const items = dt.items ? Array.prototype.slice.call(dt.items) : [];
for (let i = 0; i < items.length; i++) {
if (items[i].kind && items[i].kind !== 'file') continue;
let f = null;
try { f = items[i].getAsFile(); } catch (err) { f = null; }
if (f && f.name) {
if (EXT_YAML.indexOf(extOf(f.name)) >= 0) return true;
continue;
}
const type = String(items[i].type || '').toLowerCase().split(';')[0].trim();
if (type === 'text/yaml' || type === 'application/yaml' || type === 'text/x-yaml' ||
type === 'application/x-yaml') return true;
}
return false;
}
/** 拖放提示层。不走 buildUi,在别人的页面上拖一下不会把整页清掉。 */
function ensureOverlay() {
if (refs.overlay && refs.overlay.parentNode) return refs.overlay;
if (!document.body) return null;
injectStyles();
const overlay = document.createElement('div');
overlay.id = 'yr-drop-overlay';
overlay.innerHTML = [
'',
' Y',
' 将 YAML 文件拖放到此处',
' 支持 .yaml .yml(多文档 / 锚点别名均可)',
''
].join('');
document.body.appendChild(overlay);
refs.overlay = overlay;
return overlay;
}
function showOverlay(on) {
if (on) ensureOverlay();
if (!refs.overlay) return;
refs.overlay.classList.toggle('show', !!on);
}
function bindDrop() {
// 与 data-reader 1.4.0 同一策略:dragenter/dragover 对所有文件拖放 preventDefault,
// 保证 drop 能触发且不吞默认行为;drop 阶段按真实文件名决定是否接管。
document.addEventListener('dragenter', function (e) {
if (!isFileDrag(e)) return;
e.preventDefault();
if (dragHasYamlFile(e)) showOverlay(true);
}, true);
document.addEventListener('dragover', function (e) {
if (!isFileDrag(e)) return;
e.preventDefault();
try { e.dataTransfer.dropEffect = 'copy'; } catch (err) { /* 忽略 */ }
}, true);
document.addEventListener('dragleave', function (e) {
if (e.relatedTarget) return;
showOverlay(false);
}, true);
document.addEventListener('drop', function (e) {
if (!isFileDrag(e)) return;
showOverlay(false);
const files = Array.prototype.slice.call((e.dataTransfer && e.dataTransfer.files) || []);
const hit = files.filter(function (f) { return EXT_YAML.indexOf(extOf(f.name)) >= 0; });
if (!hit.length) return; // 不是 yaml → 不取消默认动作
e.preventDefault();
e.stopPropagation();
loadFile(hit[0]);
}, true);
}
/* ============================================================
* 8. 启动
* ============================================================ */
/** 当前页面是不是「直接打开的本地 yaml 文件」 */
function autoTarget() {
if (location.protocol !== 'file:') return '';
const pathname = decodeURIComponent(location.pathname || '');
const name = pathname.split('/').pop() || '';
if (!name || EXT_YAML.indexOf(extOf(name)) < 0) return '';
return name;
}
function registerMenus() {
if (typeof GM_registerMenuCommand !== 'function') return;
try {
GM_registerMenuCommand('📂 打开 YAML 文件…', pickFiles);
GM_registerMenuCommand('💾 保存当前内容', function () { save(undefined); });
GM_registerMenuCommand('🔄 重新解析', function () { reparse(); });
} catch (e) { /* 忽略 */ }
}
/** 对外暴露口子,方便控制台 / 自动化驱动(与 data-reader 的 DataReader 同款) */
function exposeApi() {
const api = {
version: VERSION,
loadText: loadText,
open: pickFiles,
save: save,
reparse: reparse,
state: state
};
try { window.YamlReader = api; } catch (e) { /* 忽略 */ }
try { (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window).YamlReader = api; } catch (e) { /* 忽略 */ }
}
function guardNativeViewer() {
let scheduled = false;
let observer = null;
const check = function () {
scheduled = false;
if (!readerStillOurs()) {
try { if (observer) observer.disconnect(); } catch (e) { /* 忽略 */ }
return;
}
const root = document.getElementById(ROOT_ID);
if (!root || root.parentNode !== document.body) remount();
else killNativeViewer();
};
const schedule = function () {
if (scheduled) return;
scheduled = true;
const raf = window.requestAnimationFrame || function (fn) { setTimeout(fn, 16); };
raf(check);
};
try {
observer = new MutationObserver(schedule);
observer.observe(document.documentElement, { childList: true, subtree: true });
} catch (e) { /* 忽略 */ }
}
/** 界面被冲掉后重建:用已抓在手里的正文快照恢复 */
function remount() {
const fileName = state.fileName;
buildUi();
killNativeViewer();
if (state.text) {
loadText(state.text, fileName);
} else if (capturedSource && capturedSource.trim() && fileName && fileName !== 'data') {
loadText(capturedSource, fileName);
} else {
renderAll();
showOverlay(true);
}
}
/** 取不到原文也给出界面(错误面板本身就是指引),绝不留白让人以为脚本没装 */
function showEmptyState(auto, msg) {
state.fileName = auto;
state.error = msg;
state.text = '';
state.docs = null;
refs.editor.value = '';
renderAll();
}
let viewerGuarding = false;
function ensureUi() {
if (!document.body) return;
buildUi();
if (viewerGuarding) return;
viewerGuarding = true;
guardNativeViewer();
}
function bufferToText(buf) {
if (buf == null || buf === '') return '';
if (typeof buf === 'string') return buf;
try {
if (buf instanceof ArrayBuffer) return decodeText(new Uint8Array(buf));
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView && ArrayBuffer.isView(buf)) {
return decodeText(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
}
} catch (e) { /* 忽略 */ }
return '';
}
/** 页面自己的 XHR。油猴沙箱里的 XHR 经常读不到 file://,必须借页面的。 */
function pageXhr(url, done, binary) {
const XHR = (typeof unsafeWindow !== 'undefined' && unsafeWindow && unsafeWindow.XMLHttpRequest)
? unsafeWindow.XMLHttpRequest
: XMLHttpRequest;
let xhr;
try { xhr = new XHR(); } catch (e) { done(''); return; }
try { xhr.open('GET', url, true); } catch (e) { done(''); return; }
if (binary) { try { xhr.responseType = 'arraybuffer'; } catch (e) { /* 退回文本 */ } }
xhr.onload = function () {
if (xhr.status && xhr.status !== 200 && xhr.status !== 0) { done(''); return; }
if (binary) {
const text = bufferToText(xhr.response);
if (text && text.trim()) { done(text); return; }
pageXhr(url, done, false);
return;
}
done(xhr.responseText || '');
};
xhr.onerror = function () {
if (binary) pageXhr(url, done, false);
else done('');
};
try { xhr.send(); } catch (e) {
if (binary) pageXhr(url, done, false);
else done('');
}
}
/** 按文件地址把字节读回来(file:// 的 yaml 常常没有 ) */
function readFileUrl(url, done) {
let settled = false;
const finish = function (text) {
if (settled) return;
settled = true;
try { done(text || ''); } catch (e) { /* 忽略 */ }
};
pageXhr(url, function (text) {
if (text && String(text).trim()) { finish(text); return; }
if (typeof GM_xmlhttpRequest !== 'function') { finish(''); return; }
try {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'arraybuffer',
timeout: 20000,
onload: function (res) {
const bufText = bufferToText(res && res.response);
finish(bufText || (res && res.responseText) || '');
},
onerror: function () { finish(''); },
ontimeout: function () { finish(''); }
});
} catch (e) { finish(''); }
}, true);
}
function init() {
bindDrop();
registerMenus();
exposeApi();
const auto = autoTarget();
if (!auto) return;
const bootFromText = function (text) {
if (!text || !text.trim()) return false;
capturedSource = text;
ensureUi();
if (text.length > MAX_AUTO_PARSE) {
showEmptyState(auto, '文件超过 64 MB,已跳过自动解析。可以把内容粘贴到右侧再点「重新解析」。');
showOverlay(true);
return true;
}
loadText(text, auto);
return true;
};
const fail = function () {
ensureUi();
showEmptyState(auto, '没能从这个页面读到文件原文(浏览器可能把它当成下载处理了)。'
+ '点「📂 打开」选择文件,或直接把 ' + auto + ' 拖进这个页面。');
};
if (bootFromText(captureSource(true))) return;
readFileUrl(location.href, function (text) {
if (bootFromText(text) || bootFromText(captureSource(true))) return;
fail();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();