Skip to content

Commit aae546d

Browse files
committed
iss1757 - Interleave extractors and filters
1 parent b9e5e33 commit aae546d

9 files changed

Lines changed: 142 additions & 78 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Extractor: lastcalc
2+
// Sets answerEl to the raw content of the last calculation block.
3+
export default function lastcalc(raw, answerEl, blocks) {
4+
if (blocks) {
5+
for (let i = blocks.length - 1; i >= 0; i--) {
6+
if (blocks[i].type === 'calculation') {
7+
answerEl.value = blocks[i].raw;
8+
answerEl.dispatchEvent(new Event('change'));
9+
return;
10+
}
11+
}
12+
}
13+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Filter: calculation
2+
// Finds text enclosed in @ characters on a single line and wraps it in double stars.
3+
// e.g. "The answer is @x^2 + 1@ here" → "The answer is **x^2 + 1** here"
4+
5+
export default function calculation(text, blockCollector) {
6+
if (blockCollector) {
7+
blockCollector.blocks = [];
8+
}
9+
10+
return text.replace(/@([^@\n]+)@/g, (match, raw) => {
11+
const rendered = `**${raw}**`;
12+
if (blockCollector) {
13+
blockCollector.blocks.push({ type: 'calculation', raw, rendered });
14+
}
15+
return rendered;
16+
});
17+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Markdown filter — creates a single markdownit instance with the full transformLib.
2+
// Call markdown(text, transforms, blockCollector) to render text with a chosen
3+
// subset of transforms. The transformLib is the authoritative registry; add new
4+
// transforms here after creating their file in markdownittransforms/.
5+
6+
import markdownit from '../markdownit.js';
7+
import markdownitSub from '../markdownitextensions/sub.js';
8+
import asciimathBlock from '../markdownitextensions/asciimathblock.js';
9+
import markdownitrules from './markdownitrules.js';
10+
11+
// tex.js uses named CJS exports (exports.tex = ...) — import the whole namespace.
12+
import * as mdItPluginTex from '../markdownitextensions/tex.js';
13+
14+
import boldfilter from '../markdownittransforms/020_boldfilter.js';
15+
import latexwrap from '../markdownittransforms/010_latexwrap.js';
16+
17+
// Registry maps the string names used in the transforms option to the actual functions.
18+
// Add new transforms here after creating their file in markdownittransforms/.
19+
const transformLib = {
20+
boldfilter,
21+
latexwrap,
22+
};
23+
24+
// Shared mutable state updated before each render so the single converter instance
25+
// can serve calls with different transforms / collectors.
26+
const state = { transforms: [], transformLib, collector: null };
27+
28+
// mdItPluginTex.tex must come before markdownitrules.
29+
const converter = markdownit({ html: true })
30+
.use(markdownitSub)
31+
.use(mdItPluginTex.tex, { render: (content) => content, delimiters: 'brackets' })
32+
.use(asciimathBlock)
33+
.use(markdownitrules, { state });
34+
35+
export default function markdown(text, blockCollector, op) {
36+
state.transforms = (op.transforms || '')
37+
.split(',')
38+
.map(s => s.trim())
39+
.filter(Boolean);
40+
state.collector = blockCollector || null;
41+
return converter.render(text);
42+
}

corsscripts/ascii/markdownitrules.js renamed to corsscripts/ascii/filters/markdownitrules.js

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,33 @@
11
// Markdown-it plugin — bundled into stackascii.bundle.js via stackascii.src.js.
22
// Edit transform behaviour in markdownittransforms/*.js, then run: npm run build:bundle
3-
4-
import boldfilter from './markdownittransforms/020_boldfilter.js';
5-
import latexwrap from './markdownittransforms/010_latexwrap.js';
6-
7-
// Registry maps the string names used in options.transforms to the actual functions.
8-
// Add new transforms here after creating their file in markdownittransforms/.
9-
const transformRegistry = {
10-
boldfilter,
11-
latexwrap,
12-
};
3+
// The transformLib is defined in markdown.js and passed via options.state.
134

145
export default function markdownitrules(mdit, options) {
156
"use strict";
16-
options = options || {};
17-
const transforms = (options.transforms || '')
18-
.split(',')
19-
.map(s => s.trim())
20-
.filter(Boolean);
21-
const collector = options.collector || null;
7+
const state = options.state;
228

239
// Reset the collector at the start of each render pass.
2410
mdit.core.ruler.push('reset_collector', () => {
25-
if (collector) {
26-
collector.blocks = [];
11+
if (state.collector) {
12+
state.collector.blocks = [];
2713
}
2814
});
2915

3016
mdit.renderer.rules.code_inline = function(tokens, idx) {
3117
const code = tokens[idx].content;
3218
const inlineWrap = (s) => `\\(${s}\\)`;
3319
const rendered = inlineWrap(window.AMparseMath(code, true));
34-
if (collector) {
35-
collector.blocks.push({ type: 'code_inline', raw: code, rendered });
20+
if (state.collector) {
21+
state.collector.blocks.push({ type: 'code_inline', raw: code, rendered });
3622
}
3723
return rendered;
3824
};
3925

4026
mdit.renderer.rules.asciimath_block = function(tokens, idx) {
4127
const code = tokens[idx].content;
4228
const rendered = applyTransforms(code, true);
43-
if (collector) {
44-
collector.blocks.push({ type: 'asciimath_block', raw: code, rendered });
29+
if (state.collector) {
30+
state.collector.blocks.push({ type: 'asciimath_block', raw: code, rendered });
4531
}
4632
return rendered;
4733
};
@@ -50,17 +36,17 @@ export default function markdownitrules(mdit, options) {
5036
const code = tokens[idx].content;
5137
const inlineWrap = (s) => `\\(${s}\\)`;
5238
const rendered = inlineWrap(code);
53-
if (collector) {
54-
collector.blocks.push({ type: 'math_inline', raw: code, rendered });
39+
if (state.collector) {
40+
state.collector.blocks.push({ type: 'math_inline', raw: code, rendered });
5541
}
5642
return rendered;
5743
};
5844

5945
mdit.renderer.rules.math_block = function(tokens, idx) {
6046
const code = tokens[idx].content;
6147
const rendered = applyTransforms(code, false);
62-
if (collector) {
63-
collector.blocks.push({ type: 'math_block', raw: code, rendered });
48+
if (state.collector) {
49+
state.collector.blocks.push({ type: 'math_block', raw: code, rendered });
6450
}
6551
return rendered;
6652
};
@@ -74,11 +60,11 @@ export default function markdownitrules(mdit, options) {
7460
if (isASCIIMaths) {
7561
lines = lines.map(line => window.AMparseMath(line, true));
7662
}
77-
for (const transform of transforms) {
78-
if (!transformRegistry[transform]) {
63+
for (const transform of state.transforms) {
64+
if (!state.transformLib[transform]) {
7965
throw new Error(`markdownitrules: unknown transform "${transform}"`);
8066
}
81-
lines = transformRegistry[transform](lines);
67+
lines = state.transformLib[transform](lines);
8268
}
8369
return lines.join('\n') + '\n';
8470
}

corsscripts/ascii/stackascii.bundle.js

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

corsscripts/ascii/stackascii.bundle.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

corsscripts/ascii/stackascii.js

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,28 @@
66
// ASCIIMathTeXImg.js is loaded as a plain <script> tag by the PHP (sloppy mode).
77
// It sets window.AMparseMath before init() is called.
88

9-
import markdownit from './markdownit.js';
10-
import markdownitSub from './markdownitextensions/sub.js';
11-
import asciimathBlock from './markdownitextensions/asciimathblock.js';
12-
import markdownitrules from './markdownitrules.js';
9+
import markdown from './filters/markdown.js';
10+
import calculation from './filters/calculation.js';
1311

14-
// tex.js uses named CJS exports (exports.tex = ...) — import the whole namespace.
15-
import * as mdItPluginTex from './markdownitextensions/tex.js';
12+
const filterlib = { markdown, calculation };
1613

1714
import finalfunction from './extractors/finalfunction.js';
1815
import lastexpr from './extractors/lastexpr.js';
1916
import lastblock from './extractors/lastblock.js';
17+
import lastcalc from './extractors/lastcalc.js';
2018
import regexmatch from './extractors/regexmatch.js';
2119
import regexall from './extractors/regexall.js';
2220

23-
const extractorlib = { finalfunction, lastexpr, lastblock, regexmatch, regexall };
21+
const extractorlib = { finalfunction, lastexpr, lastblock, lastcalc, regexmatch, regexall };
2422

2523
export default function init(inputIds, operations) {
2624
const markdownContainerId = inputIds[0];
2725
// inputIds[1..N] correspond to each parsed answer entry in order.
2826
const alloperations = operations;
2927
const filters = operations.filter(operator => operator.operation === 'filter');
3028
const extractors = operations.filter(operator => operator.operation === 'extractor');
31-
const markdownitinfo = filters.find(operator => operator.type === 'markdownit');
32-
const inputTransforms = markdownitinfo ? markdownitinfo.transforms : 'latexwrap,boldfilter';
33-
3429
const blockCollector = { blocks: [] };
3530

36-
// mdItPluginTex.tex must come before markdownitrules.
37-
const previewMarkdownConverter = markdownit({ html: true })
38-
.use(markdownitSub)
39-
.use(mdItPluginTex.tex, { render: (content) => content, delimiters: 'brackets' })
40-
.use(asciimathBlock)
41-
.use(markdownitrules, { transforms: inputTransforms, collector: blockCollector });
42-
43-
function convertMarkdown(markdown) {
44-
const html = previewMarkdownConverter.render(markdown);
45-
document.getElementById('asciiContainerRow').innerHTML = html;
46-
}
47-
4831
function renderMath() {
4932
const raw = document.getElementById(markdownContainerId).value.trim();
5033
const output = document.getElementById('asciiContainerRow');
@@ -53,24 +36,46 @@ export default function init(inputIds, operations) {
5336
return;
5437
}
5538

56-
convertMarkdown(raw);
39+
let processedOutput = raw;
40+
let displayfixed = false;
41+
let answerIndex = 1;
42+
43+
if (alloperations) {
44+
alloperations.forEach((currentop, i) => {
45+
if (currentop.operation === 'filter') {
46+
const filter = filterlib[currentop.type];
47+
if (filter) {
48+
let filterInput = processedOutput;
49+
if (currentop.reset === 'true') {
50+
filterInput = raw;
51+
}
52+
const filterOutput = filter(filterInput, blockCollector, currentop);
53+
if (!displayfixed) {
54+
processedOutput = filterOutput;
55+
}
56+
if (currentop.display === 'true') {
57+
displayfixed = true;
58+
}
59+
}
60+
} else if (currentop.operation === 'extractor') {
61+
const extractor = (extractorlib[currentop.type]) ? extractorlib[currentop.type] : extractorlib['lastexpr'];
62+
const answerEl = document.getElementById(inputIds[answerIndex]);
63+
answerIndex++;
64+
if (extractor && answerEl) {
65+
extractor(raw, answerEl, blockCollector.blocks, currentop);
66+
}
67+
}
68+
});
69+
}
70+
71+
document.getElementById('asciiContainerRow').innerHTML = processedOutput;
5772

5873
// Tell MathJax to typeset only this element.
5974
if (typeof MathJax.typesetPromise === 'function') {
6075
MathJax.typesetPromise([output]); // MathJax 3
6176
} else {
6277
MathJax.Hub.Queue(["Typeset", MathJax.Hub, 'asciiContainerRow']); // MathJax 2
6378
}
64-
65-
if (extractors) {
66-
extractors.forEach((entry, i) => {
67-
const extractor = (extractorlib[entry.type]) ? extractorlib[entry.type] : extractorlib['lastexpr'];
68-
const answerEl = document.getElementById(inputIds[1 + i]);
69-
if (extractor && answerEl) {
70-
extractor(raw, answerEl, blockCollector.blocks, entry);
71-
}
72-
});
73-
}
7479
}
7580

7681
let debounceTimer;

samplequestions/stacklibrary/Features/input-type-sample-questions/Free-text_final_answer.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
<p>Work line by line below, justifying your answer fully. Your last line should be your answer \(f(x)=...\).</p>
1111
1212
<p>[[input:ans1]] [[validation:ans1]]</p>
13-
[[ascii input="ans1" transforms="latexwrap"]]
14-
[[filter type="markdownit" transforms="latexwrap" /]]
13+
[[ascii input="ans1"]]
14+
[[filter type="markdown" transforms="latexwrap" /]]
1515
[[extractor targetinput="ans2" type="finalfunction"/]]
1616
[[/ascii]]
1717
<p>[[hint title="Input help"]][[commonstring key="free_text_fact"/]][[/hint]]</p>

stack/cas/castext2/blocks/ascii.block.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ public function compile($format, $options): ?MP_Node {
7474
if (!$isfilter) {
7575
$defaultfilter = new StdClass();
7676
$defaultfilter->operation = 'filter';
77-
$defaultfilter->type = 'markdownit';
77+
$defaultfilter->type = 'markdown';
7878
$defaultfilter->transforms = 'latexwrap,boldfilter';
79+
array_unshift($operations, $defaultfilter);
7980
}
8081

8182
// Set default width and height here.

0 commit comments

Comments
 (0)