The markdown editor's save handlers (markDirty, save(), convertBtns intercept) referenced a bare identifier `writable` that never existed in their scope — the captured variable was named `writableMode`. JS silently evaluates `!undefined` to true, so saveBtn.disabled stayed true forever and Ctrl-S was a no-op. The download-as-* intercept treated every dirty file as read-only and offered the "save a copy elsewhere" toast. YAML editor had the matching-name pattern (`writable` defined and referenced) so the symptom was hidden, but the same stale-closure shape: capture once at mount, never re-read when the underlying tree node's writable bit changed. Fix both: gating logic reads canSave(node) fresh at every click, not from a closure. Mount-time captures stay for initial UI shape (read-only banner, CodeMirror readOnly:'nocursor') where the decision is correct at the moment it's applied. Codify the pattern in AGENTS.md § "JS module pattern": no bundler + no reactivity layer ⇒ closures don't refresh ⇒ read fresh in handlers, never cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
559 lines
24 KiB
JavaScript
559 lines
24 KiB
JavaScript
// preview-yaml.js — YAML editor plugin for the browse preview pane.
|
|
//
|
|
// Routes any .yaml / .yml file, plus the .zddc cascade files
|
|
// (`.zddc` and `*.zddc.yaml`), through a CodeMirror 5 editor with
|
|
// syntax highlighting and live linting. js-yaml.loadAll feeds parse
|
|
// errors into CM's lint gutter; for .zddc files an additional
|
|
// schema-aware pass flags unknown keys, bad enum values, and wrong
|
|
// types.
|
|
//
|
|
// Layout (single column):
|
|
// ┌─────────────────────────────────────────────────────────────┐
|
|
// │ name | dirty | status | source | [Save] │
|
|
// ├─────────────────────────────────────────────────────────────┤
|
|
// │ CodeMirror editor (line numbers + lint gutter) │
|
|
// └─────────────────────────────────────────────────────────────┘
|
|
//
|
|
// Save (Ctrl+S) writes back via PUT (server mode) or
|
|
// FileSystemWritableFileStream (FS-API). Zip members and
|
|
// virtual nodes are read-only — Save stays disabled.
|
|
(function () {
|
|
'use strict';
|
|
|
|
if (!window.app || !window.app.modules) return;
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/&/g, '&').replace(/</g, '<')
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// ── Filename routing ────────────────────────────────────────────────────
|
|
|
|
// True for .zddc cascade files — `.zddc` (literal name, no ext)
|
|
// and `<anything>.zddc.yaml` (e.g. `defaults.zddc.yaml`). These
|
|
// get the schema-aware lint layer.
|
|
function isZddcFile(name) {
|
|
if (!name) return false;
|
|
if (name === '.zddc') return true;
|
|
return /\.zddc\.ya?ml$/i.test(name);
|
|
}
|
|
|
|
function isYamlFile(node) {
|
|
if (!node || !node.name) return false;
|
|
if (isZddcFile(node.name)) return true;
|
|
var ext = (node.ext || '').toLowerCase();
|
|
return ext === 'yaml' || ext === 'yml';
|
|
}
|
|
|
|
// ── Save (mirrors preview-markdown.js) ─────────────────────────────────
|
|
|
|
async function saveContent(node, content) {
|
|
if (node.handle && typeof node.handle.createWritable === 'function') {
|
|
var writable = await node.handle.createWritable();
|
|
await writable.write(content);
|
|
await writable.close();
|
|
return;
|
|
}
|
|
if (node.url && window.app.state.source === 'server') {
|
|
var resp = await fetch(node.url, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/x-yaml; charset=utf-8' },
|
|
body: content,
|
|
credentials: 'same-origin'
|
|
});
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
return;
|
|
}
|
|
throw new Error('No write target for this file (read-only source).');
|
|
}
|
|
|
|
function isZipMemberNode(node) {
|
|
if (node.handle && node.handle.isZipEntry) return true;
|
|
if (node.url && window.app.state.source === 'server'
|
|
&& /\.zip\//i.test(node.url)) return true;
|
|
return false;
|
|
}
|
|
|
|
function canSave(node) {
|
|
if (isZipMemberNode(node)) return false;
|
|
// Virtual .zddc placeholders are designed to be saved — a PUT
|
|
// materializes the file from the synthetic body and the next
|
|
// listing serves a real entry. Every other virtual node (per-
|
|
// user home, canonical-folder virtuals) is just a tree
|
|
// affordance, not a writable file.
|
|
if (node.virtual && node.name !== '.zddc') return false;
|
|
// Server-computed authority gate. Mirrors the markdown editor's
|
|
// check — listing's `writable` bit is the same decision the
|
|
// file API would reach on PUT.
|
|
if (node.url && window.app.state.source === 'server' && !node.writable) return false;
|
|
if (node.handle && typeof node.handle.createWritable === 'function') return true;
|
|
if (node.url && window.app.state.source === 'server') return true;
|
|
return false;
|
|
}
|
|
|
|
async function hashContent(text) {
|
|
if (!window.crypto || !window.crypto.subtle) return null;
|
|
var enc = new TextEncoder().encode(text);
|
|
var buf = await window.crypto.subtle.digest('SHA-256', enc);
|
|
var bytes = new Uint8Array(buf);
|
|
var hex = '';
|
|
for (var i = 0; i < bytes.length; i++) {
|
|
hex += bytes[i].toString(16).padStart(2, '0');
|
|
}
|
|
return hex;
|
|
}
|
|
|
|
// ── .zddc schema ────────────────────────────────────────────────────────
|
|
//
|
|
// Mirrors the Go-side decoder in zddc/internal/zddc/*. Allowed
|
|
// tool names are the embedded set (always available) plus the
|
|
// composable ones served when declared in apps:. Unknown keys at
|
|
// any level surface as warnings — typos like `defaul_tool` are
|
|
// common and the cascade silently ignores them.
|
|
|
|
var ALLOWED_TOOLS = {
|
|
archive: 1, browse: 1, landing: 1, transmittal: 1, classifier: 1,
|
|
tables: 1, form: 1
|
|
};
|
|
|
|
var TOP_KEYS = {
|
|
title: 'string',
|
|
acl: 'acl',
|
|
admins: 'string[]',
|
|
roles: 'rolemap',
|
|
available_tools: 'tools[]',
|
|
default_tool: 'tool',
|
|
dir_tool: 'tool',
|
|
auto_own: 'bool',
|
|
auto_own_fenced: 'bool',
|
|
virtual: 'bool',
|
|
drop_target: 'bool',
|
|
worm: 'string[]',
|
|
paths: 'pathmap',
|
|
display: 'stringmap',
|
|
apps: 'appsmap',
|
|
apps_pubkey: 'string',
|
|
tables: 'stringmap',
|
|
convert: 'convert',
|
|
created_by: 'string',
|
|
inherit: 'bool'
|
|
};
|
|
|
|
var ACL_KEYS = { inherit: 'bool', permissions: 'stringmap',
|
|
allow: 'string[]', deny: 'string[]' };
|
|
var ROLE_KEYS = { members: 'string[]', reset: 'bool' };
|
|
var CONVERT_KEYS = { client: 'string', project: 'string',
|
|
contractor: 'string', project_number: 'string' };
|
|
|
|
function typeOf(v) {
|
|
if (v === null || v === undefined) return 'null';
|
|
if (Array.isArray(v)) return 'array';
|
|
return typeof v; // 'string' | 'number' | 'boolean' | 'object'
|
|
}
|
|
|
|
// Collect schema issues for a parsed .zddc document. Each issue is
|
|
// { keyPath: string[], message: string, severity: 'error' | 'warning' }.
|
|
// keyPath is used by findLine() to locate the offending source line.
|
|
function validateZddc(doc) {
|
|
var issues = [];
|
|
if (typeOf(doc) === 'null') return issues;
|
|
if (typeOf(doc) !== 'object') {
|
|
issues.push({ keyPath: [], severity: 'error',
|
|
message: 'Root must be a map (got ' + typeOf(doc) + ').' });
|
|
return issues;
|
|
}
|
|
walkObject(doc, TOP_KEYS, [], issues);
|
|
return issues;
|
|
}
|
|
|
|
function walkObject(obj, schema, path, issues) {
|
|
for (var key in obj) {
|
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
var here = path.concat([key]);
|
|
var kind = schema[key];
|
|
if (!kind) {
|
|
issues.push({ keyPath: here, severity: 'warning',
|
|
message: 'Unknown key "' + key + '" — typo? It will be silently ignored.' });
|
|
continue;
|
|
}
|
|
checkValue(obj[key], kind, here, issues);
|
|
}
|
|
}
|
|
|
|
function checkValue(val, kind, path, issues) {
|
|
var t = typeOf(val);
|
|
switch (kind) {
|
|
case 'string':
|
|
if (t !== 'string' && t !== 'null') addTypeErr(path, kind, t, issues);
|
|
return;
|
|
case 'bool':
|
|
if (t !== 'boolean' && t !== 'null') addTypeErr(path, kind, t, issues);
|
|
return;
|
|
case 'string[]':
|
|
if (t !== 'array' && t !== 'null') addTypeErr(path, kind, t, issues);
|
|
return;
|
|
case 'tools[]':
|
|
if (t !== 'array' && t !== 'null') {
|
|
addTypeErr(path, kind, t, issues); return;
|
|
}
|
|
if (t === 'array') {
|
|
for (var i = 0; i < val.length; i++) {
|
|
if (typeOf(val[i]) !== 'string') {
|
|
issues.push({ keyPath: path, severity: 'error',
|
|
message: 'available_tools[' + i + '] must be a string.' });
|
|
} else if (!ALLOWED_TOOLS[val[i]]) {
|
|
issues.push({ keyPath: path, severity: 'warning',
|
|
message: 'Unknown tool "' + val[i]
|
|
+ '". Known: ' + Object.keys(ALLOWED_TOOLS).join(', ') + '.' });
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
case 'tool':
|
|
if (t === 'null') return;
|
|
if (t !== 'string') { addTypeErr(path, kind, t, issues); return; }
|
|
if (!ALLOWED_TOOLS[val]) {
|
|
issues.push({ keyPath: path, severity: 'warning',
|
|
message: 'Unknown tool "' + val + '". Known: '
|
|
+ Object.keys(ALLOWED_TOOLS).join(', ') + '.' });
|
|
}
|
|
return;
|
|
case 'stringmap':
|
|
if (t === 'null') return;
|
|
if (t !== 'object') { addTypeErr(path, kind, t, issues); return; }
|
|
for (var k in val) {
|
|
if (!Object.prototype.hasOwnProperty.call(val, k)) continue;
|
|
if (typeOf(val[k]) !== 'string') {
|
|
issues.push({ keyPath: path.concat([k]), severity: 'error',
|
|
message: 'Value must be a string (got '
|
|
+ typeOf(val[k]) + ').' });
|
|
}
|
|
}
|
|
return;
|
|
case 'pathmap':
|
|
if (t === 'null') return;
|
|
if (t !== 'object') { addTypeErr(path, kind, t, issues); return; }
|
|
for (var seg in val) {
|
|
if (!Object.prototype.hasOwnProperty.call(val, seg)) continue;
|
|
if (seg.indexOf('/') !== -1) {
|
|
issues.push({ keyPath: path.concat([seg]), severity: 'error',
|
|
message: 'Path keys must be a single segment — '
|
|
+ 'nest blocks instead of using "' + seg + '".' });
|
|
}
|
|
var v = val[seg];
|
|
if (typeOf(v) === 'null') continue;
|
|
if (typeOf(v) !== 'object') {
|
|
issues.push({ keyPath: path.concat([seg]), severity: 'error',
|
|
message: 'paths.' + seg + ' must be a map of cascade rules.' });
|
|
continue;
|
|
}
|
|
walkObject(v, TOP_KEYS, path.concat([seg]), issues);
|
|
}
|
|
return;
|
|
case 'appsmap':
|
|
if (t === 'null') return;
|
|
if (t !== 'object') { addTypeErr(path, kind, t, issues); return; }
|
|
for (var app in val) {
|
|
if (!Object.prototype.hasOwnProperty.call(val, app)) continue;
|
|
if (!ALLOWED_TOOLS[app]) {
|
|
issues.push({ keyPath: path.concat([app]), severity: 'warning',
|
|
message: 'Unknown tool "' + app + '" in apps:.' });
|
|
}
|
|
if (typeOf(val[app]) !== 'string') {
|
|
issues.push({ keyPath: path.concat([app]), severity: 'error',
|
|
message: 'apps.' + app + ' must be a spec string '
|
|
+ '(channel | v<semver> | URL | path).' });
|
|
}
|
|
}
|
|
return;
|
|
case 'rolemap':
|
|
if (t === 'null') return;
|
|
if (t !== 'object') { addTypeErr(path, kind, t, issues); return; }
|
|
for (var rn in val) {
|
|
if (!Object.prototype.hasOwnProperty.call(val, rn)) continue;
|
|
var rv = val[rn];
|
|
if (typeOf(rv) === 'null') continue;
|
|
if (typeOf(rv) !== 'object') {
|
|
issues.push({ keyPath: path.concat([rn]), severity: 'error',
|
|
message: 'roles.' + rn + ' must be a map ({members, reset}).' });
|
|
continue;
|
|
}
|
|
walkObject(rv, ROLE_KEYS, path.concat([rn]), issues);
|
|
}
|
|
return;
|
|
case 'acl':
|
|
if (t === 'null') return;
|
|
if (t !== 'object') { addTypeErr(path, kind, t, issues); return; }
|
|
walkObject(val, ACL_KEYS, path, issues);
|
|
return;
|
|
case 'convert':
|
|
if (t === 'null') return;
|
|
if (t !== 'object') { addTypeErr(path, kind, t, issues); return; }
|
|
walkObject(val, CONVERT_KEYS, path, issues);
|
|
return;
|
|
}
|
|
}
|
|
|
|
function addTypeErr(path, expected, got, issues) {
|
|
issues.push({ keyPath: path, severity: 'error',
|
|
message: 'Expected ' + expected + ', got ' + got + '.' });
|
|
}
|
|
|
|
// Locate the source line for a key path. .zddc files are
|
|
// block-style YAML in practice (no flow style, no anchors), so a
|
|
// simple indent-aware scan works: for each segment, find a line
|
|
// matching "<indent><key>:" whose indent is deeper than the
|
|
// previously-matched line. Falls back to line 0 if no match.
|
|
function findLine(source, keyPath) {
|
|
if (!keyPath || keyPath.length === 0) return 0;
|
|
var lines = source.split('\n');
|
|
var prevIndent = -1;
|
|
var prevLine = 0;
|
|
for (var i = 0; i < keyPath.length; i++) {
|
|
var key = keyPath[i];
|
|
var found = -1;
|
|
// Escape regex metachars in the key.
|
|
var keyRe = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
var re = new RegExp('^(\\s*)"?' + keyRe + '"?\\s*:');
|
|
for (var j = prevLine; j < lines.length; j++) {
|
|
var m = lines[j].match(re);
|
|
if (m && m[1].length > prevIndent) {
|
|
found = j;
|
|
prevIndent = m[1].length;
|
|
prevLine = j + 1;
|
|
break;
|
|
}
|
|
}
|
|
if (found === -1) return prevLine > 0 ? prevLine - 1 : 0;
|
|
}
|
|
return prevLine > 0 ? prevLine - 1 : 0;
|
|
}
|
|
|
|
// ── CodeMirror lint helper ──────────────────────────────────────────────
|
|
|
|
function registerLinter(CM) {
|
|
// The lint helper signature: function(text, options, editor) → annotations[]
|
|
// Each annotation: { from, to, message, severity }.
|
|
CM.registerHelper('lint', 'yaml', function (text, _opts, editor) {
|
|
var out = [];
|
|
if (!window.jsyaml) return out;
|
|
var parsed;
|
|
try {
|
|
// loadAll handles multi-doc YAML; we only validate the
|
|
// first doc against the schema (the .zddc cascade reads
|
|
// only the first document).
|
|
var docs = [];
|
|
window.jsyaml.loadAll(text, function (d) { docs.push(d); });
|
|
parsed = docs[0];
|
|
} catch (e) {
|
|
var mark = e.mark;
|
|
var pos = mark ? CM.Pos(mark.line, mark.column) : CM.Pos(0, 0);
|
|
out.push({ from: pos, to: pos, severity: 'error',
|
|
message: e.message || String(e) });
|
|
return out;
|
|
}
|
|
// Schema layer — only for .zddc cascade files.
|
|
var node = editor._zddcNode;
|
|
if (node && isZddcFile(node.name)) {
|
|
var issues = validateZddc(parsed);
|
|
for (var i = 0; i < issues.length; i++) {
|
|
var ln = findLine(text, issues[i].keyPath);
|
|
out.push({
|
|
from: CM.Pos(ln, 0),
|
|
to: CM.Pos(ln, (editor.getLine(ln) || '').length),
|
|
severity: issues[i].severity,
|
|
message: issues[i].message
|
|
});
|
|
}
|
|
}
|
|
return out;
|
|
});
|
|
}
|
|
|
|
// ── Mount ───────────────────────────────────────────────────────────────
|
|
|
|
var currentEditor = null;
|
|
|
|
function dispose() {
|
|
// CM doesn't have an explicit destroy(); GC handles it once
|
|
// the host element is removed. Clear our reference so a stale
|
|
// editor doesn't keep handlers alive.
|
|
currentEditor = null;
|
|
}
|
|
|
|
async function render(node, container, ctx) {
|
|
if (typeof window.CodeMirror === 'undefined') {
|
|
container.innerHTML =
|
|
'<div class="preview-empty" style="color:var(--danger)">'
|
|
+ 'CodeMirror isn\'t bundled in this build.</div>';
|
|
return;
|
|
}
|
|
dispose();
|
|
|
|
var text;
|
|
try {
|
|
var buf = await ctx.getArrayBuffer(node);
|
|
text = new TextDecoder('utf-8', { fatal: false }).decode(buf);
|
|
} catch (e) {
|
|
container.innerHTML =
|
|
'<div class="preview-empty" style="color:var(--danger)">'
|
|
+ 'Could not read ' + escapeHtml(node.name) + ': '
|
|
+ escapeHtml(e.message || String(e)) + '</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
var shell = document.createElement('div');
|
|
shell.className = 'yaml-shell';
|
|
container.appendChild(shell);
|
|
|
|
// Info header — same look as the markdown plugin's info-header
|
|
// so the two editors feel like one family.
|
|
var infohdr = document.createElement('div');
|
|
infohdr.className = 'md-shell__infohdr yaml-shell__infohdr';
|
|
|
|
var titleEl = document.createElement('span');
|
|
titleEl.className = 'md-shell__title';
|
|
titleEl.textContent = node.name;
|
|
titleEl.title = node.name;
|
|
|
|
var schemaTag = document.createElement('span');
|
|
schemaTag.className = 'md-shell__source yaml-shell__schema';
|
|
if (isZddcFile(node.name)) {
|
|
schemaTag.textContent = '.zddc schema';
|
|
schemaTag.title = 'Linted against the .zddc cascade schema '
|
|
+ '(unknown keys, bad enums, and wrong types are flagged).';
|
|
} else {
|
|
schemaTag.textContent = 'YAML';
|
|
}
|
|
|
|
var dirtyEl = document.createElement('span');
|
|
dirtyEl.className = 'md-shell__dirty';
|
|
|
|
var statusEl = document.createElement('span');
|
|
statusEl.className = 'md-shell__status';
|
|
|
|
var sourceEl = document.createElement('span');
|
|
sourceEl.className = 'md-shell__source';
|
|
if (isZipMemberNode(node)) sourceEl.textContent = 'read-only (zip)';
|
|
else if (node.handle) sourceEl.textContent = 'local';
|
|
else if (node.url) sourceEl.textContent = 'server';
|
|
|
|
var saveBtn = document.createElement('button');
|
|
saveBtn.className = 'btn btn-sm btn-primary md-shell__save';
|
|
saveBtn.type = 'button';
|
|
saveBtn.textContent = 'Save';
|
|
saveBtn.disabled = true;
|
|
|
|
infohdr.appendChild(titleEl);
|
|
infohdr.appendChild(schemaTag);
|
|
infohdr.appendChild(dirtyEl);
|
|
infohdr.appendChild(statusEl);
|
|
infohdr.appendChild(sourceEl);
|
|
infohdr.appendChild(saveBtn);
|
|
shell.appendChild(infohdr);
|
|
|
|
var editorHost = document.createElement('div');
|
|
editorHost.className = 'yaml-shell__editor';
|
|
shell.appendChild(editorHost);
|
|
|
|
// Register the lint helper once per page lifetime.
|
|
if (!window.CodeMirror.__zddcYamlLinterReady) {
|
|
registerLinter(window.CodeMirror);
|
|
window.CodeMirror.__zddcYamlLinterReady = true;
|
|
}
|
|
|
|
var writable = canSave(node);
|
|
var editor = window.CodeMirror(editorHost, {
|
|
value: text,
|
|
mode: 'yaml',
|
|
lineNumbers: true,
|
|
tabSize: 2,
|
|
indentUnit: 2,
|
|
indentWithTabs: false,
|
|
lineWrapping: false,
|
|
gutters: ['CodeMirror-lint-markers', 'CodeMirror-linenumbers'],
|
|
lint: { hasGutters: true },
|
|
// autofocus:false keeps the keyboard caret in the browse
|
|
// tree pane so arrow-key nav can continue through yaml /
|
|
// .zddc files without diverting into the editor. User
|
|
// clicks (or tabs) into the editor when they want to type.
|
|
autofocus: false,
|
|
// CodeMirror's "nocursor" mode is the truest read-only:
|
|
// selection allowed for copy, no caret, no edit affordances.
|
|
readOnly: !writable ? 'nocursor' : false,
|
|
});
|
|
// Stash the node on the editor so the lint helper can decide
|
|
// whether to apply the .zddc schema layer.
|
|
editor._zddcNode = node;
|
|
// Force an initial lint pass now that _zddcNode is set.
|
|
editor.performLint();
|
|
currentEditor = editor;
|
|
|
|
if (!writable) {
|
|
saveBtn.disabled = true;
|
|
saveBtn.title = 'Save not available — read-only source.';
|
|
// Read-only banner above the editor explains why.
|
|
var roBanner = document.createElement('div');
|
|
roBanner.className = 'yaml-readonly-banner';
|
|
roBanner.innerHTML = '<span aria-hidden="true">🔒</span>'
|
|
+ ' Read-only — you don\'t have write access to this file.';
|
|
editorHost.insertBefore(roBanner, editorHost.firstChild);
|
|
}
|
|
|
|
var initialHash = await hashContent(text);
|
|
|
|
function markDirty(isDirty) {
|
|
saveBtn.disabled = !isDirty || !canSave(node);
|
|
dirtyEl.textContent = isDirty ? '● modified' : '';
|
|
}
|
|
|
|
editor.on('change', async function () {
|
|
var h = await hashContent(editor.getValue());
|
|
markDirty(h !== initialHash);
|
|
});
|
|
|
|
async function save() {
|
|
if (saveBtn.disabled) return;
|
|
// Re-check authority at click time, not via the mount-time
|
|
// `writable` capture — the listing may have re-evaluated
|
|
// (e.g. user toggled admin mode without a hard reload).
|
|
if (!canSave(node)) return;
|
|
var content = editor.getValue();
|
|
try {
|
|
statusEl.textContent = 'Saving…';
|
|
await saveContent(node, content);
|
|
initialHash = await hashContent(content);
|
|
markDirty(false);
|
|
statusEl.textContent = 'Saved ' + new Date().toLocaleTimeString();
|
|
if (window.zddc && window.zddc.toast) {
|
|
window.zddc.toast('Saved ' + node.name, 'success');
|
|
}
|
|
} catch (e) {
|
|
statusEl.textContent = 'Save failed: ' + (e.message || e);
|
|
if (window.zddc && window.zddc.toast) {
|
|
window.zddc.toast('Save failed: ' + (e.message || e), 'error');
|
|
}
|
|
}
|
|
}
|
|
saveBtn.addEventListener('click', save);
|
|
editor.setOption('extraKeys', {
|
|
'Ctrl-S': save,
|
|
'Cmd-S': save
|
|
});
|
|
|
|
// CM defers layout until its host has a size — refresh after
|
|
// mount so the gutters and viewport sync to the grid cell.
|
|
setTimeout(function () { try { editor.refresh(); } catch (_e) {} }, 0);
|
|
}
|
|
|
|
function handles(node) {
|
|
if (!node || node.isDir || node.isZip) return false;
|
|
return isYamlFile(node);
|
|
}
|
|
|
|
window.app.modules.yamledit = {
|
|
handles: handles,
|
|
render: render
|
|
};
|
|
})();
|