// ==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') + ' 行原文', '', 'Tab / Shift+Tab 缩进 · Enter 自动续进 · Ctrl+S 保存' ].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();
    }
})();