feat(browse): shared schema completion + hover docs; bring it to the .zddc editor
Generalise the front-matter completion into a reusable, provider-based helper
(browse/js/yaml-complete.js) and wire BOTH YAML editors through it. Still fully
deterministic — every candidate and doc string comes from a schema, no AI.
- yaml-complete.js: shared CodeMirror plumbing (indent→key-path, sibling scan,
show-hint, debounced hover tooltip) + two providers:
· flatProvider — a fixed field list (front matter), with an exclude set.
· schemaProvider — a JSON Schema walker that resolves nested key-paths
through properties / additionalProperties / patternProperties and the
recursive $ref:"#" .zddc uses for paths:; keys from object properties,
values from enum / boolean, hover docs from `description`.
- .zddc editor (preview-yaml.js): fetch /.api/zddc-schema once and attach the
schemaProvider on .zddc files — nested-key completion at every level, enum
values (default_tool, dir_tool, views.*.tool), booleans, and hover docs.
Plain .yaml stays lint+highlight only.
- Front-matter editor (preview-markdown.js): refactored to delegate to the
shared helper via flatProvider (excluding the filename-driven identity keys);
the bespoke frontMatterHints is gone — one implementation now.
- Hover-doc tooltip styling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2c877bd5b7
commit
a13ce12a75
5 changed files with 329 additions and 83 deletions
|
|
@ -68,6 +68,7 @@ concat_files \
|
||||||
"../shared/zddc-source.js" \
|
"../shared/zddc-source.js" \
|
||||||
"js/init.js" \
|
"js/init.js" \
|
||||||
"js/util.js" \
|
"js/util.js" \
|
||||||
|
"js/yaml-complete.js" \
|
||||||
"js/conflict.js" \
|
"js/conflict.js" \
|
||||||
"js/menu-model.js" \
|
"js/menu-model.js" \
|
||||||
"js/loader.js" \
|
"js/loader.js" \
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,24 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hover-doc tooltip (yaml-complete.js) — appended to document.body, so it's
|
||||||
|
styled globally. Carries a key's schema description on hover. */
|
||||||
|
.cm-doc-tip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9700;
|
||||||
|
max-width: 360px;
|
||||||
|
padding: 6px 9px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: var(--bg-elevated, var(--bg, #fff));
|
||||||
|
color: var(--text, #222);
|
||||||
|
border: 1px solid var(--border, #ccc);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28);
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/* CodeMirror has to fill the grid cell. The vendored CSS sets
|
/* CodeMirror has to fill the grid cell. The vendored CSS sets
|
||||||
`height: 300px` by default — we override to 100% so it grows with
|
`height: 300px` by default — we override to 100% so it grows with
|
||||||
the preview pane. */
|
the preview pane. */
|
||||||
|
|
|
||||||
|
|
@ -209,73 +209,6 @@
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Schema completion for the front-matter editor ──────────────────────
|
|
||||||
// Deterministic, schema-driven — NO heuristics, no AI. Keys + enum values
|
|
||||||
// come straight from /.api/frontmatter (fmFields), the converter's own
|
|
||||||
// field list. Returns a CodeMirror show-hint result ({list, from, to}) or
|
|
||||||
// null. The front matter is flat (top-level keys), so no nesting to walk.
|
|
||||||
function frontMatterHints(cm) {
|
|
||||||
if (!fmFields || !fmFields.length || !window.CodeMirror) return null;
|
|
||||||
var Pos = window.CodeMirror.Pos;
|
|
||||||
var cur = cm.getCursor();
|
|
||||||
var before = cm.getLine(cur.line).slice(0, cur.ch);
|
|
||||||
var colon = before.indexOf(':');
|
|
||||||
|
|
||||||
if (colon === -1) {
|
|
||||||
// KEY context — complete recognised keys, minus the filename-driven
|
|
||||||
// identity keys (we don't want those typed into front matter) and
|
|
||||||
// keys already present in the document.
|
|
||||||
var m = before.match(/^(\s*)([\w-]*)$/);
|
|
||||||
if (!m) return null;
|
|
||||||
var indent = m[1], typed = m[2];
|
|
||||||
var identity = {};
|
|
||||||
IDENTITY_FIELDS.forEach(function (f) { identity[f.fm] = true; });
|
|
||||||
var present = {};
|
|
||||||
for (var ln = 0; ln < cm.lineCount(); ln++) {
|
|
||||||
var km = cm.getLine(ln).match(/^\s*([\w-]+)\s*:/);
|
|
||||||
if (km) present[km[1]] = true;
|
|
||||||
}
|
|
||||||
var list = [];
|
|
||||||
fmFields.forEach(function (f) {
|
|
||||||
if (identity[f.name] || present[f.name]) return;
|
|
||||||
if (typed && f.name.indexOf(typed) !== 0) return;
|
|
||||||
var item = {
|
|
||||||
text: f.name + ': ',
|
|
||||||
displayText: f.name + (f.hint ? ' — ' + f.hint : '')
|
|
||||||
};
|
|
||||||
// If the key is an enum, insert "key: " then immediately offer
|
|
||||||
// its values — one fluid keystroke path for doctype/numbering.
|
|
||||||
if (f.values && f.values.length) {
|
|
||||||
item.hint = function (cmi, data, comp) {
|
|
||||||
cmi.replaceRange(comp.text, data.from, data.to);
|
|
||||||
setTimeout(function () {
|
|
||||||
cmi.showHint({ hint: frontMatterHints, completeSingle: false });
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
list.push(item);
|
|
||||||
});
|
|
||||||
if (!list.length) return null;
|
|
||||||
return { list: list, from: Pos(cur.line, indent.length), to: cur };
|
|
||||||
}
|
|
||||||
|
|
||||||
// VALUE context — complete the enum values for this line's key.
|
|
||||||
var key = before.slice(0, colon).trim();
|
|
||||||
var field = null;
|
|
||||||
for (var i = 0; i < fmFields.length; i++) {
|
|
||||||
if (fmFields[i].name === key) { field = fmFields[i]; break; }
|
|
||||||
}
|
|
||||||
if (!field || !field.values || !field.values.length) return null;
|
|
||||||
var rest = before.slice(colon + 1);
|
|
||||||
var valTyped = rest.replace(/^\s*/, '');
|
|
||||||
var valStart = colon + 1 + (rest.length - valTyped.length);
|
|
||||||
var vlist = field.values.filter(function (v) {
|
|
||||||
return v.indexOf(valTyped) === 0;
|
|
||||||
}).map(function (v) { return { text: v, displayText: v }; });
|
|
||||||
if (!vlist.length) return null;
|
|
||||||
return { list: vlist, from: Pos(cur.line, valStart), to: cur };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── TOC (table of contents) ────────────────────────────────────────────
|
// ── TOC (table of contents) ────────────────────────────────────────────
|
||||||
// ATX headings only; the body markdown drives the outline. Clicking
|
// ATX headings only; the body markdown drives the outline. Clicking
|
||||||
// a heading routes to whichever Toast UI pane is currently active
|
// a heading routes to whichever Toast UI pane is currently active
|
||||||
|
|
@ -712,26 +645,19 @@
|
||||||
gutters: ['CodeMirror-lint-markers', 'CodeMirror-linenumbers'],
|
gutters: ['CodeMirror-lint-markers', 'CodeMirror-linenumbers'],
|
||||||
lint: { hasGutters: true },
|
lint: { hasGutters: true },
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
readOnly: !writableMode,
|
readOnly: !writableMode
|
||||||
// Ctrl-Space (and Tab as a convenience) opens schema completion.
|
|
||||||
extraKeys: {
|
|
||||||
'Ctrl-Space': function (cm) {
|
|
||||||
cm.showHint({ hint: frontMatterHints, completeSingle: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// The yaml lint helper (registered by the .zddc previewer) checks this
|
// The yaml lint helper (registered by the .zddc previewer) checks this
|
||||||
// to decide the schema layer; a .md node → plain js-yaml parse lint.
|
// to decide the schema layer; a .md node → plain js-yaml parse lint.
|
||||||
fmCM._zddcNode = node;
|
fmCM._zddcNode = node;
|
||||||
// Auto-open completion as the author types a key/value character (never
|
// Schema completion + hover docs via the shared helper. The front
|
||||||
// auto-inserts — completeSingle:false — so it's a menu, not a guess).
|
// matter is flat; the identity keys are excluded from suggestions
|
||||||
// No-op when frontMatterHints finds nothing to offer.
|
// (filename-driven — see renderIdentityCue).
|
||||||
if (writableMode) {
|
var yc = window.app.modules.yamlComplete;
|
||||||
fmCM.on('inputRead', function (cm, change) {
|
if (yc) {
|
||||||
if (!change.text || change.text.length !== 1) return; // skip paste/delete
|
yc.attach(fmCM, yc.flatProvider(function () { return fmFields; }, {
|
||||||
if (!/[\w-]/.test(change.text[0])) return;
|
exclude: IDENTITY_FIELDS.map(function (f) { return f.fm; })
|
||||||
cm.showHint({ hint: frontMatterHints, completeSingle: false });
|
}), { readOnly: !writableMode });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// CodeMirror mis-measures when mounted before its pane is laid out;
|
// CodeMirror mis-measures when mounted before its pane is laid out;
|
||||||
// refresh on the next frame so the gutters + scroll size correctly.
|
// refresh on the next frame so the gutters + scroll size correctly.
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,23 @@
|
||||||
var util = window.app.modules.util;
|
var util = window.app.modules.util;
|
||||||
var escapeHtml = util.escapeHtml;
|
var escapeHtml = util.escapeHtml;
|
||||||
|
|
||||||
|
// Cached .zddc JSON Schema (the machine grammar) — fetched once from
|
||||||
|
// /.api/zddc-schema and used to drive completion + hover on .zddc files.
|
||||||
|
// null = not loaded; {} on fetch failure. The promise dedupes fetches.
|
||||||
|
var zddcSchema = null;
|
||||||
|
var zddcSchemaPromise = null;
|
||||||
|
function loadZddcSchema() {
|
||||||
|
if (zddcSchema !== null) return;
|
||||||
|
if (!zddcSchemaPromise) {
|
||||||
|
zddcSchemaPromise = fetch('/.api/zddc-schema', {
|
||||||
|
headers: { 'Accept': 'application/schema+json, application/json' },
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(function (r) { return r.ok ? r.json() : null; })
|
||||||
|
.then(function (j) { zddcSchema = j || {}; })
|
||||||
|
.catch(function () { zddcSchema = {}; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Filename routing ────────────────────────────────────────────────────
|
// ── Filename routing ────────────────────────────────────────────────────
|
||||||
|
|
||||||
// True for .zddc cascade files — `.zddc` (literal name, no ext)
|
// True for .zddc cascade files — `.zddc` (literal name, no ext)
|
||||||
|
|
@ -535,6 +552,15 @@
|
||||||
editor._zddcNode = node;
|
editor._zddcNode = node;
|
||||||
// Force an initial lint pass now that _zddcNode is set.
|
// Force an initial lint pass now that _zddcNode is set.
|
||||||
editor.performLint();
|
editor.performLint();
|
||||||
|
// Schema completion + hover docs for .zddc files (the machine grammar
|
||||||
|
// drives keys, enum/boolean values, and nested paths via $ref:"#").
|
||||||
|
// Plain .yaml gets no schema (lint + highlighting only).
|
||||||
|
var yc = window.app.modules.yamlComplete;
|
||||||
|
if (yc && isZddcFile(node.name)) {
|
||||||
|
loadZddcSchema();
|
||||||
|
yc.attach(editor, yc.schemaProvider(function () { return zddcSchema || {}; }),
|
||||||
|
{ readOnly: !writable });
|
||||||
|
}
|
||||||
currentEditor = editor;
|
currentEditor = editor;
|
||||||
currentNodeRef = node;
|
currentNodeRef = node;
|
||||||
currentDirty = false;
|
currentDirty = false;
|
||||||
|
|
|
||||||
275
browse/js/yaml-complete.js
Normal file
275
browse/js/yaml-complete.js
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
// yaml-complete.js — deterministic, schema-driven completion + hover docs for
|
||||||
|
// the browse YAML editors (markdown front matter + .zddc). NO heuristics, no
|
||||||
|
// AI: every candidate and doc string comes from a PROVIDER backed by the
|
||||||
|
// converter's field list or the .zddc JSON Schema.
|
||||||
|
//
|
||||||
|
// A provider answers three questions about a position, identified by its key
|
||||||
|
// PATH (the array of parent keys):
|
||||||
|
// keysAt(path) → [{name, hint, values}] valid child keys here
|
||||||
|
// valuesFor(path, key) → [string] | null enum/boolean values
|
||||||
|
// describe(path, key) → string | null doc text (for hover)
|
||||||
|
// The CodeMirror plumbing (indent→path, sibling scan, show-hint, hover) is
|
||||||
|
// shared; only the provider differs between the flat front matter and the
|
||||||
|
// nested .zddc schema. Requires CodeMirror 5 + the show-hint add-on.
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
if (!window.app) window.app = {};
|
||||||
|
if (!window.app.modules) window.app.modules = {};
|
||||||
|
|
||||||
|
function indentOf(line) { var m = line.match(/^(\s*)/); return m ? m[1].length : 0; }
|
||||||
|
function isBlankOrComment(line) { return /^\s*$/.test(line) || /^\s*#/.test(line); }
|
||||||
|
function truncate(s, n) { s = String(s); return s.length > n ? s.slice(0, n - 1) + '…' : s; }
|
||||||
|
|
||||||
|
// Parent key-path for a line, derived from YAML indentation: walk upward
|
||||||
|
// collecting each "key:" line at a strictly smaller indent.
|
||||||
|
function pathAt(cm, lineNo) {
|
||||||
|
var path = [];
|
||||||
|
var target = indentOf(cm.getLine(lineNo));
|
||||||
|
for (var ln = lineNo - 1; ln >= 0 && target > 0; ln--) {
|
||||||
|
var line = cm.getLine(ln);
|
||||||
|
if (isBlankOrComment(line)) continue;
|
||||||
|
var ind = indentOf(line);
|
||||||
|
if (ind < target) {
|
||||||
|
var m = line.match(/^\s*([\w.\-]+)\s*:/);
|
||||||
|
if (m) { path.unshift(m[1]); target = ind; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sibling keys already present at the same indent within this block, so we
|
||||||
|
// don't re-suggest a key the author already wrote.
|
||||||
|
function presentSiblings(cm, lineNo, indent) {
|
||||||
|
var present = {};
|
||||||
|
[-1, 1].forEach(function (dir) {
|
||||||
|
for (var ln = lineNo + dir; ln >= 0 && ln < cm.lineCount(); ln += dir) {
|
||||||
|
var line = cm.getLine(ln);
|
||||||
|
if (isBlankOrComment(line)) continue;
|
||||||
|
var ind = indentOf(line);
|
||||||
|
if (ind < indent) break; // left the block
|
||||||
|
if (ind === indent) {
|
||||||
|
var m = line.match(/^\s*([\w.\-]+)\s*:/);
|
||||||
|
if (m) present[m[1]] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyItem(k, hinter) {
|
||||||
|
var item = {
|
||||||
|
text: k.name + ': ',
|
||||||
|
displayText: k.name + (k.hint ? ' — ' + truncate(k.hint, 64) : '')
|
||||||
|
};
|
||||||
|
// An enum key inserts "key: " then immediately opens its value menu.
|
||||||
|
if (k.values && k.values.length) {
|
||||||
|
item.hint = function (cmi, data, comp) {
|
||||||
|
cmi.replaceRange(comp.text, data.from, data.to);
|
||||||
|
setTimeout(function () { cmi.showHint({ hint: hinter, completeSingle: false }); }, 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeHinter(provider) {
|
||||||
|
function hinter(cm) {
|
||||||
|
var CM = window.CodeMirror;
|
||||||
|
if (!CM) return null;
|
||||||
|
var cur = cm.getCursor();
|
||||||
|
var before = cm.getLine(cur.line).slice(0, cur.ch);
|
||||||
|
var colon = before.indexOf(':');
|
||||||
|
var path = pathAt(cm, cur.line);
|
||||||
|
|
||||||
|
if (colon === -1) {
|
||||||
|
// KEY context.
|
||||||
|
var m = before.match(/^(\s*)([\w.\-]*)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
var indent = m[1], typed = m[2];
|
||||||
|
var keys = provider.keysAt(path) || [];
|
||||||
|
if (!keys.length) return null;
|
||||||
|
var present = presentSiblings(cm, cur.line, indent.length);
|
||||||
|
var list = [];
|
||||||
|
keys.forEach(function (k) {
|
||||||
|
if (present[k.name]) return;
|
||||||
|
if (typed && k.name.indexOf(typed) !== 0) return;
|
||||||
|
list.push(keyItem(k, hinter));
|
||||||
|
});
|
||||||
|
if (!list.length) return null;
|
||||||
|
return { list: list, from: CM.Pos(cur.line, indent.length), to: cur };
|
||||||
|
}
|
||||||
|
|
||||||
|
// VALUE context.
|
||||||
|
var key = before.slice(0, colon).trim();
|
||||||
|
var values = provider.valuesFor(path, key) || [];
|
||||||
|
if (!values.length) return null;
|
||||||
|
var rest = before.slice(colon + 1);
|
||||||
|
var valTyped = rest.replace(/^\s*/, '');
|
||||||
|
var valStart = colon + 1 + (rest.length - valTyped.length);
|
||||||
|
var vlist = [];
|
||||||
|
values.forEach(function (v) {
|
||||||
|
if (valTyped && v.indexOf(valTyped) !== 0) return;
|
||||||
|
vlist.push({ text: v, displayText: v });
|
||||||
|
});
|
||||||
|
if (!vlist.length) return null;
|
||||||
|
return { list: vlist, from: CM.Pos(cur.line, valStart), to: cur };
|
||||||
|
}
|
||||||
|
return hinter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lightweight hover docs: hover a "key:" → its schema description. No
|
||||||
|
// add-on — a debounced mousemove over the editor + a fixed-position tip.
|
||||||
|
function attachHover(cm, provider) {
|
||||||
|
var tip = null, timer = null;
|
||||||
|
function hide() { if (tip && tip.parentNode) tip.parentNode.removeChild(tip); tip = null; }
|
||||||
|
function show(text, x, y) {
|
||||||
|
hide();
|
||||||
|
tip = document.createElement('div');
|
||||||
|
tip.className = 'cm-doc-tip';
|
||||||
|
tip.textContent = text;
|
||||||
|
document.body.appendChild(tip);
|
||||||
|
tip.style.left = x + 'px';
|
||||||
|
tip.style.top = (y + 16) + 'px';
|
||||||
|
}
|
||||||
|
var wrap = cm.getWrapperElement();
|
||||||
|
wrap.addEventListener('mousemove', function (e) {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
var ex = e.clientX, ey = e.clientY;
|
||||||
|
timer = setTimeout(function () {
|
||||||
|
if (!wrap.isConnected) { hide(); return; }
|
||||||
|
try {
|
||||||
|
var pos = cm.coordsChar({ left: ex, top: ey }, 'window');
|
||||||
|
var line = cm.getLine(pos.line) || '';
|
||||||
|
var m = line.match(/^\s*([\w.\-]+)\s*:/);
|
||||||
|
if (!m) { hide(); return; }
|
||||||
|
var keyStart = line.indexOf(m[1]);
|
||||||
|
if (pos.ch < keyStart || pos.ch > keyStart + m[1].length) { hide(); return; }
|
||||||
|
var doc = provider.describe(pathAt(cm, pos.line), m[1]);
|
||||||
|
if (doc) show(doc, ex, ey); else hide();
|
||||||
|
} catch (_e) { hide(); }
|
||||||
|
}, 350);
|
||||||
|
});
|
||||||
|
wrap.addEventListener('mouseleave', function () { if (timer) clearTimeout(timer); hide(); });
|
||||||
|
cm.on('cursorActivity', hide);
|
||||||
|
cm.on('changes', hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire completion (Ctrl-Space + auto-trigger as you type) and hover docs
|
||||||
|
// onto a CodeMirror instance. opts.readOnly skips the typing trigger;
|
||||||
|
// opts.hover:false skips hover.
|
||||||
|
function attach(cm, provider, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var hinter = makeHinter(provider);
|
||||||
|
var keys = Object.assign({}, cm.getOption('extraKeys') || {}, {
|
||||||
|
'Ctrl-Space': function (c) { c.showHint({ hint: hinter, completeSingle: false }); }
|
||||||
|
});
|
||||||
|
cm.setOption('extraKeys', keys);
|
||||||
|
if (!opts.readOnly) {
|
||||||
|
cm.on('inputRead', function (c, change) {
|
||||||
|
if (!change.text || change.text.length !== 1) return; // skip paste/delete
|
||||||
|
if (!/[\w.\-]/.test(change.text[0])) return;
|
||||||
|
c.showHint({ hint: hinter, completeSingle: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (opts.hover !== false) attachHover(cm, provider);
|
||||||
|
return hinter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Providers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Flat: a fixed field list [{name, hint, values}] at the root, nothing
|
||||||
|
// nested (front matter). opts.exclude = names never suggested.
|
||||||
|
function flatProvider(getFields, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var exclude = {};
|
||||||
|
(opts.exclude || []).forEach(function (n) { exclude[n] = true; });
|
||||||
|
function fields() { return getFields() || []; }
|
||||||
|
function find(name) {
|
||||||
|
var fs = fields();
|
||||||
|
for (var i = 0; i < fs.length; i++) if (fs[i].name === name) return fs[i];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
keysAt: function (path) {
|
||||||
|
if (path.length) return [];
|
||||||
|
return fields().filter(function (f) { return !exclude[f.name]; })
|
||||||
|
.map(function (f) { return { name: f.name, hint: f.hint, values: f.values }; });
|
||||||
|
},
|
||||||
|
valuesFor: function (path, key) {
|
||||||
|
if (path.length) return null;
|
||||||
|
var f = find(key); return f ? f.values : null;
|
||||||
|
},
|
||||||
|
describe: function (path, key) {
|
||||||
|
if (path.length) return null;
|
||||||
|
var f = find(key); return f ? f.hint : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema: a JSON Schema (draft-2020-12 subset). Resolves nested key-paths
|
||||||
|
// through properties / additionalProperties / patternProperties and the
|
||||||
|
// recursive $ref:"#" .zddc uses for paths:. Keys = object property names;
|
||||||
|
// values = enum / boolean.
|
||||||
|
function schemaProvider(getSchema) {
|
||||||
|
function root() { return getSchema(); }
|
||||||
|
function deref(node) { return (node && node.$ref === '#') ? root() : node; }
|
||||||
|
function stepInto(node, seg) {
|
||||||
|
node = deref(node);
|
||||||
|
if (!node || node.type !== 'object') return null;
|
||||||
|
if (node.properties && node.properties[seg]) return node.properties[seg];
|
||||||
|
if (node.additionalProperties && typeof node.additionalProperties === 'object') {
|
||||||
|
return node.additionalProperties;
|
||||||
|
}
|
||||||
|
if (node.patternProperties) {
|
||||||
|
for (var p in node.patternProperties) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(node.patternProperties, p)) {
|
||||||
|
return node.patternProperties[p];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function containerAt(path) {
|
||||||
|
var node = deref(root());
|
||||||
|
for (var i = 0; i < path.length; i++) {
|
||||||
|
node = stepInto(node, path[i]);
|
||||||
|
if (!node) return null;
|
||||||
|
node = deref(node);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
function valuesOf(node) {
|
||||||
|
node = deref(node);
|
||||||
|
if (!node) return null;
|
||||||
|
if (Array.isArray(node.enum)) return node.enum.map(String);
|
||||||
|
if (node.type === 'boolean') return ['true', 'false'];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function keyNodeAt(path, key) {
|
||||||
|
var c = containerAt(path);
|
||||||
|
if (!c || !c.properties) return null;
|
||||||
|
return c.properties[key] || null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
keysAt: function (path) {
|
||||||
|
var c = containerAt(path);
|
||||||
|
if (!c || c.type !== 'object' || !c.properties) return [];
|
||||||
|
return Object.keys(c.properties).map(function (name) {
|
||||||
|
var n = deref(c.properties[name]) || {};
|
||||||
|
return { name: name, hint: n.description, values: valuesOf(n) };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
valuesFor: function (path, key) { return valuesOf(keyNodeAt(path, key)); },
|
||||||
|
describe: function (path, key) {
|
||||||
|
var n = deref(keyNodeAt(path, key));
|
||||||
|
return n ? n.description : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.app.modules.yamlComplete = {
|
||||||
|
attach: attach,
|
||||||
|
makeHinter: makeHinter,
|
||||||
|
flatProvider: flatProvider,
|
||||||
|
schemaProvider: schemaProvider
|
||||||
|
};
|
||||||
|
})();
|
||||||
Loading…
Reference in a new issue