Single-source the scheduler help from markdown the AI index can read
This commit is contained in:
+136
-12
@@ -83,17 +83,26 @@ function vv_docs_render(string $rel, array $vars): string {
|
||||
if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'md') return '<p>File not found.</p>';
|
||||
if (!is_file($path)) return '<p>File not found.</p>';
|
||||
|
||||
$md = file_get_contents($path);
|
||||
return vv_docs_markdown(file_get_contents($path), $vars);
|
||||
}
|
||||
|
||||
// Substitute `$VAR_NAME` markers with live conf values
|
||||
$md = preg_replace_callback('/`\$([A-Z0-9_]+)`/', function($m) use ($vars) {
|
||||
$key = $m[1];
|
||||
return isset($vars[$key])
|
||||
? '<code class="vv-live-var">' . htmlspecialchars($vars[$key]) . '</code>'
|
||||
: '<code class="vv-unknown-var">$' . htmlspecialchars($key) . '</code>';
|
||||
}, $md);
|
||||
|
||||
// Render markdown
|
||||
// Markdown → HTML for the subset these docs actually use: headings, paragraphs, bullet and
|
||||
// ordered lists, tables, blockquotes, fenced code, horizontal rules, and inline bold / italic /
|
||||
// code / links.
|
||||
//
|
||||
// Hand-rolled rather than vendored. Parsedown was the original plan and PARSEDOWN_PATH is still
|
||||
// honoured below if the file ever appears, but it has never been present on this system, so the
|
||||
// only path this function ever took was a <pre> dump of raw markdown — which is not a document,
|
||||
// it is the source of one. A renderer for a subset we control is a few dozen lines and adds
|
||||
// nothing to a repo that gets pushed.
|
||||
//
|
||||
// Escape first, then format. Every line is passed through htmlspecialchars() before any tag is
|
||||
// introduced, so the only HTML in the output is HTML this function put there. That is what makes
|
||||
// safe mode unnecessary rather than merely configured — and it is why the $VAR substitution runs
|
||||
// here, after escaping. Injecting <code> into the markdown before rendering, as this file used to
|
||||
// do, meant both Parsedown's safe mode and the <pre> fallback escaped the tags and printed them
|
||||
// as literal text. The feature never worked; nothing called it, so nothing reported it.
|
||||
function vv_docs_markdown(string $md, array $vars): string {
|
||||
if (file_exists(PARSEDOWN_PATH)) {
|
||||
require_once PARSEDOWN_PATH;
|
||||
$pd = new Parsedown();
|
||||
@@ -101,6 +110,121 @@ function vv_docs_render(string $rel, array $vars): string {
|
||||
return $pd->text($md);
|
||||
}
|
||||
|
||||
// Fallback: plain preformatted text
|
||||
return '<pre>' . htmlspecialchars($md) . '</pre>';
|
||||
$inline = function (string $s) use ($vars): string {
|
||||
$s = htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
||||
// `$VAR` → the live conf value. Unresolved names render in their own class rather than
|
||||
// silently reading as prose, so a stale reference in a doc is visible as a defect.
|
||||
$s = preg_replace_callback('/`\$([A-Z0-9_]+)`/', function ($m) use ($vars) {
|
||||
return isset($vars[$m[1]])
|
||||
? '<code class="vv-live-var">' . htmlspecialchars($vars[$m[1]]) . '</code>'
|
||||
: '<code class="vv-unknown-var">$' . $m[1] . '</code>';
|
||||
}, $s);
|
||||
$s = preg_replace('/`([^`]+)`/', '<code>$1</code>', $s);
|
||||
$s = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $s);
|
||||
$s = preg_replace('/(?<![\w*])\*([^*]+)\*(?![\w*])/', '<em>$1</em>', $s);
|
||||
// Links are restricted to http/https and relative paths — a doc is trusted, but these
|
||||
// files sync between hosts, so javascript: must not be reachable through one.
|
||||
$s = preg_replace('/\[([^\]]+)\]\((https?:\/\/[^\s)]+|[^\s):]+)\)/', '<a href="$2">$1</a>', $s);
|
||||
return $s;
|
||||
};
|
||||
|
||||
$out = '';
|
||||
$list = null; // 'ul' | 'ol' | null
|
||||
$inTable = false;
|
||||
$inCode = false;
|
||||
$para = [];
|
||||
$quote = [];
|
||||
|
||||
$flushPara = function () use (&$para, &$out, $inline) {
|
||||
if (!$para) return;
|
||||
$out .= '<p>' . $inline(implode(' ', $para)) . "</p>\n";
|
||||
$para = [];
|
||||
};
|
||||
$flushQuote = function () use (&$quote, &$out, $inline) {
|
||||
if (!$quote) return;
|
||||
$out .= '<blockquote>' . $inline(implode(' ', $quote)) . "</blockquote>\n";
|
||||
$quote = [];
|
||||
};
|
||||
$closeList = function () use (&$list, &$out) {
|
||||
if ($list) { $out .= "</$list>\n"; $list = null; }
|
||||
};
|
||||
$closeTable = function () use (&$inTable, &$out) {
|
||||
if ($inTable) { $out .= "</tbody></table>\n"; $inTable = false; }
|
||||
};
|
||||
|
||||
foreach (explode("\n", str_replace("\r\n", "\n", $md)) as $line) {
|
||||
if (preg_match('/^```/', $line)) {
|
||||
$flushPara(); $closeList(); $closeTable();
|
||||
$out .= $inCode ? "</code></pre>\n" : '<pre class="vv-doc-code"><code>';
|
||||
$inCode = !$inCode;
|
||||
continue;
|
||||
}
|
||||
if ($inCode) { $out .= htmlspecialchars($line, ENT_QUOTES, 'UTF-8') . "\n"; continue; }
|
||||
|
||||
$t = trim($line);
|
||||
|
||||
// One guard rather than a flush in every branch: the quote ends the moment a line is
|
||||
// not a quote line, whatever that next line turns out to be.
|
||||
if ($quote && !str_starts_with($t, '>')) $flushQuote();
|
||||
|
||||
if ($t === '') { $flushPara(); $closeList(); $closeTable(); continue; }
|
||||
if (preg_match('/^(---+|\*\*\*+)$/', $t)) { $flushPara(); $closeList(); $closeTable(); $out .= "<hr>\n"; continue; }
|
||||
|
||||
if (preg_match('/^(#{1,6})\s+(.*)$/', $t, $m)) {
|
||||
$flushPara(); $closeList(); $closeTable();
|
||||
$n = strlen($m[1]);
|
||||
$out .= "<h$n>" . $inline($m[2]) . "</h$n>\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Consecutive > lines are one quote, not one per line — same wrapping convention as
|
||||
// paragraphs, which is how they are written.
|
||||
if (preg_match('/^>\s?(.*)$/', $t, $m)) {
|
||||
$closeList(); $closeTable();
|
||||
if (!$quote) $flushPara();
|
||||
$quote[] = $m[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tables: a header row, a separator of dashes, then body rows. The separator is what
|
||||
// identifies the block — a lone pipe in prose is not a table.
|
||||
if (strpos($t, '|') !== false && preg_match('/^\|?[\s:-]*-[\s|:-]*\|/', $t)) {
|
||||
continue; // separator consumed by the header below
|
||||
}
|
||||
if (strpos($t, '|') !== false && substr_count($t, '|') >= 2) {
|
||||
$cells = array_map('trim', explode('|', trim($t, '| ')));
|
||||
if (!$inTable) {
|
||||
$flushPara(); $closeList();
|
||||
$out .= '<table class="vv-doc-table"><thead><tr>';
|
||||
foreach ($cells as $c) $out .= '<th>' . $inline($c) . '</th>';
|
||||
$out .= "</tr></thead><tbody>\n";
|
||||
$inTable = true;
|
||||
} else {
|
||||
$out .= '<tr>';
|
||||
foreach ($cells as $c) $out .= '<td>' . $inline($c) . '</td>';
|
||||
$out .= "</tr>\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$closeTable();
|
||||
|
||||
if (preg_match('/^[-*]\s+(.*)$/', $t, $m)) {
|
||||
$flushPara();
|
||||
if ($list !== 'ul') { $closeList(); $out .= "<ul>\n"; $list = 'ul'; }
|
||||
$out .= '<li>' . $inline($m[1]) . "</li>\n";
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^\d+\.\s+(.*)$/', $t, $m)) {
|
||||
$flushPara();
|
||||
if ($list !== 'ol') { $closeList(); $out .= "<ol>\n"; $list = 'ol'; }
|
||||
$out .= '<li>' . $inline($m[1]) . "</li>\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$para[] = $t;
|
||||
}
|
||||
|
||||
$flushQuote(); $flushPara(); $closeList(); $closeTable();
|
||||
if ($inCode) $out .= "</code></pre>\n";
|
||||
return $out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user