feat: source_format auto-detect with expandable detector registry
The create-form selector forced authors to remember to switch from
markdown to rst before typing. Auto-detect runs on every preview
keystroke + at form submit time, picking the right format from
content shape so authors just write.
SOURCE_FORMAT_DETECTORS in custom.js carries the registry — first
match wins, ordered most-specific first. Today: html, latex,
mediawiki, rst, with markdown as the catch-all default.
Adding a new format (e.g. asciidoc, org, ipynb):
1. Push one entry to SOURCE_FORMAT_DETECTORS in custom.js
2. Add one <option> to source_format_select in create.j2
3. Confirm pandoc accepts the format name
No Python changes needed for new formats — the existing
preview_post + new_thread paths pass source_format through to
pandoc untouched.
UX:
- Default selector value: 'auto' (was 'markdown')
- "detected: rst" label appears next to selector when auto picks
- Manual override options stay for ambiguous content
- Form onsubmit resolves 'auto' to a concrete format before POST
so node.source_format never persists as the literal 'auto'
Defense in depth: new_thread.py + preview_post both treat 'auto'
as a no-op (falls to markdown default) for no-JS clients or racy
submits.
This commit is contained in:
parent
d1145510e2
commit
50b9d1ee8e
4 changed files with 111 additions and 5 deletions
|
|
@ -78,15 +78,108 @@ function previewAjax(textarea, div, show_raw, mathjax) {
|
|||
);
|
||||
}
|
||||
|
||||
// ===== source_format detection — expandable registry ========================
|
||||
// First match wins. To add a new format:
|
||||
// 1. Push a new entry into SOURCE_FORMAT_DETECTORS below.
|
||||
// 2. Add a matching <option value="..."> to source_format_select in any
|
||||
// template that exposes the selector (create.j2 today).
|
||||
// 3. Confirm remarkbox/lib/pandoc.py + the preview_post view accept the
|
||||
// format name (pandoc subprocess uses whatever string you pass).
|
||||
// Detection runs on every preview keystroke + on form submit when the
|
||||
// selector is set to "auto". For "auto" cases we resolve to a concrete
|
||||
// format before saving so the persisted node.source_format is honest.
|
||||
// Order detectors most-specific → least-specific; markdown is the catch-all
|
||||
// fallback when nothing matches.
|
||||
var SOURCE_FORMAT_DETECTORS = [
|
||||
{
|
||||
format: 'html',
|
||||
// Unambiguous root markers — full HTML documents.
|
||||
test: function(text) {
|
||||
return /<!doctype\s+html|<html\b/i.test(text);
|
||||
}
|
||||
},
|
||||
{
|
||||
format: 'latex',
|
||||
test: function(text) {
|
||||
return /\\documentclass\b|\\begin\{document\}|\\section\{|\\subsection\{/.test(text);
|
||||
}
|
||||
},
|
||||
{
|
||||
format: 'mediawiki',
|
||||
// Need both heading + bold pattern to distinguish from rst/md.
|
||||
test: function(text) {
|
||||
return /^==+\s+.+\s+==+\s*$/m.test(text) && /'''[^']/.test(text);
|
||||
}
|
||||
},
|
||||
{
|
||||
format: 'rst',
|
||||
test: function(text) {
|
||||
if (/^\.\.\s+[a-z][\w-]*::/m.test(text)) return true; // directives
|
||||
if (/^\.\.\s+_\S+:/m.test(text)) return true; // link targets
|
||||
// Section underline: line made of one repeated char of length
|
||||
// >= the previous non-empty line's length.
|
||||
var lines = text.split('\n');
|
||||
for (var i = 1; i < lines.length; i++) {
|
||||
var prev = lines[i-1].trim();
|
||||
var curr = lines[i].trim();
|
||||
if (prev.length >= 3 &&
|
||||
/^([=\-~^"'`*+#])\1+$/.test(curr) &&
|
||||
curr.length >= prev.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function detectSourceFormat(text) {
|
||||
if (!text || text.length < 3) return null;
|
||||
for (var i = 0; i < SOURCE_FORMAT_DETECTORS.length; i++) {
|
||||
try {
|
||||
if (SOURCE_FORMAT_DETECTORS[i].test(text)) {
|
||||
return SOURCE_FORMAT_DETECTORS[i].format;
|
||||
}
|
||||
} catch (e) { /* malformed regex / bad text — skip this detector */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hook called from create.j2 form onsubmit. When source_format_select is on
|
||||
// 'auto' we replace its value with the detected format so the POST carries
|
||||
// a concrete name. Falls to 'markdown' when detection finds nothing.
|
||||
function resolveAutoSourceFormat(form) {
|
||||
var sel = form.querySelector('#source_format_select');
|
||||
var ta = form.querySelector('#thread_data_textarea');
|
||||
if (sel && ta && sel.value === 'auto') {
|
||||
sel.value = detectSourceFormat(ta.value) || 'markdown';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show "detected: rst" next to the selector so authors see what auto picked.
|
||||
function updateDetectionLabel(ta, detected) {
|
||||
var span = document.querySelector('#detected_format_label');
|
||||
if (!span) return;
|
||||
span.textContent = detected ? ('detected: ' + detected) : '';
|
||||
}
|
||||
|
||||
function sendPreview(textarea, div, mathjax) {
|
||||
var url = '/preview-post';
|
||||
var ta = document.getElementById(textarea);
|
||||
var data = new FormData();
|
||||
data.append('data', ta.value);
|
||||
data.append('csrf_token', csrf_token);
|
||||
// textarea declares its source_format via data-source-format; the
|
||||
// endpoint dispatches to pandoc for rst/html/mediawiki/latex when set.
|
||||
// textarea declares its source_format via data-source-format. When the
|
||||
// value is 'auto' (default on create form), run client-side detection
|
||||
// and substitute the concrete format before posting. The endpoint
|
||||
// dispatches to pandoc for rst/html/mediawiki/latex/etc.
|
||||
var fmt = ta.dataset.sourceFormat;
|
||||
if (fmt === 'auto') {
|
||||
var detected = detectSourceFormat(ta.value);
|
||||
updateDetectionLabel(ta, detected);
|
||||
fmt = detected || 'markdown';
|
||||
}
|
||||
if (fmt) data.append('source_format', fmt);
|
||||
|
||||
fetch(url, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<form method="post" action="" onsubmit="submit.disabled = true; return true;">
|
||||
<form method="post" action="" onsubmit="resolveAutoSourceFormat(this); submit.disabled = true; return true;">
|
||||
<input
|
||||
name = "thread_title"
|
||||
id = "thread_title_input"
|
||||
|
|
@ -11,15 +11,19 @@
|
|||
name = "thread_data"
|
||||
id = "thread_data_textarea"
|
||||
class = "common-textarea"
|
||||
data-source-format = "markdown"
|
||||
data-source-format = "auto"
|
||||
onkeyup = "previewAjax( 'thread_data_textarea', 'preview', show_raw=true, mathjax={{ request.mathjax }} )"
|
||||
placeholder = "What do you want to say? markdown"
|
||||
required></textarea>
|
||||
|
||||
<label for="source_format_select" style="display:inline-block;margin-right:0.5em">format:</label>
|
||||
{# Source-format selector. 'auto' defers to detectSourceFormat() in custom.js,
|
||||
which runs an expandable detector registry. To add a new format here, push
|
||||
an entry to SOURCE_FORMAT_DETECTORS in custom.js + add a <option> below. #}
|
||||
<select id="source_format_select" name="source_format"
|
||||
onchange="document.getElementById('thread_data_textarea').dataset.sourceFormat = this.value; previewAjax('thread_data_textarea', 'preview', show_raw=true, mathjax={{ request.mathjax }})">
|
||||
<option value="markdown" selected>markdown</option>
|
||||
<option value="auto" selected>auto-detect</option>
|
||||
<option value="markdown">markdown</option>
|
||||
<option value="rst">reStructuredText</option>
|
||||
<option value="html">html</option>
|
||||
<option value="mediawiki">mediawiki</option>
|
||||
|
|
@ -28,6 +32,7 @@
|
|||
<option value="asciidoc">asciidoc</option>
|
||||
<option value="textile">textile</option>
|
||||
</select>
|
||||
<span id="detected_format_label" style="margin-left:0.5em;font-size:0.85em;color:#888"></span>
|
||||
|
||||
<div id='preview' name='preview' class="preview"></div>
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ def preview_post(request):
|
|||
"""
|
||||
data = request.params.get("data", "")
|
||||
source_format = (request.params.get("source_format") or "markdown").strip().lower()
|
||||
# JS auto-resolves to a concrete format, but defend against stragglers.
|
||||
if source_format == "auto":
|
||||
source_format = "markdown"
|
||||
try:
|
||||
if source_format in ("", "markdown"):
|
||||
return markdown_to_html(data, request.namespace, dbsession=request.dbsession)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ def new_thread(request):
|
|||
thread_data = request.params.get("thread_data", "")
|
||||
anonymous_name = request.params.get("anonymous_name", "").strip()
|
||||
source_format = request.params.get("source_format", "").strip() or None
|
||||
# JS resolves 'auto' to a concrete format before submit, but defend against
|
||||
# no-JS clients / racy submits — never persist the literal string 'auto'
|
||||
# as a node.source_format. None falls back to markdown via set_data.
|
||||
if source_format == "auto":
|
||||
source_format = None
|
||||
|
||||
if request.spam:
|
||||
return request.spam
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue