<!-- Demo: arc-eager dependency parser for 'The dog chased the cat' -->
<div class="sentence-row" id="sentenceRow" aria-label="Sentence words"></div>
<div class="arc-canvas-wrap">
<canvas id="arcCanvas" aria-hidden="true"></canvas>
</div>
<div class="state-row">
<div class="state-box">
<div class="state-label">Stack</div>
<div class="state-val" id="stackVal"></div>
</div>
<div class="state-box">
<div class="state-label">Buffer</div>
<div class="state-val" id="bufferVal"></div>
</div>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btnShift" type="button">Shift</button>
<button id="btnArcL" type="button">Arc-Left</button>
<button id="btnArcR" type="button">Arc-Right</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="hint-para" id="hintPara">Click <b>Shift</b> to push the next word onto the stack, <b>Arc-Left</b> to make the buffer word the head of the stack-top, or <b>Arc-Right</b> to make the stack-top the head of the buffer word.</div>
/* Styles for the dependency parsing demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.sentence-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 4px; min-height: 36px; }
.word-chip { padding: 4px 10px; border-radius: 20px; font-size: .9rem; font-weight: 600;
background: #e8eef3; border: 1.5px solid #cdd9e3; color: #1d3557; white-space: nowrap; }
.word-chip.on-stack { background: #1d3557; color: #fff; border-color: #1d3557; }
.word-chip.attached { background: #d0f0d8; border-color: #6cbb88; color: #145227; }
.arc-canvas-wrap { width: 100%; overflow: hidden; margin-bottom: 4px; }
canvas { display: block; width: 100%; }
.state-row { display: flex; gap: 10px; margin: 6px 0; }
.state-box { flex: 1; background: #f4f6f8; border-radius: 8px; padding: 6px 10px; min-height: 36px; }
.state-label { font-size: .72rem; font-weight: 700; color: #5a7088; text-transform: uppercase;
letter-spacing: .04em; margin-bottom: 2px; }
.state-val { font-size: .85rem; color: #1d3557; font-family: ui-monospace, monospace; word-break: break-all; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: 4px 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .38; cursor: not-allowed; }
.hint-para { font-size: .82rem; color: #555; line-height: 1.45; }
// Arc-eager transition system (Nivre 2003) for 'The dog chased the cat'
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Sentence words; index 0 is a virtual ROOT node
const WORDS = ['ROOT', 'The', 'dog', 'chased', 'the', 'cat'];
// Gold standard arcs (head index, dependent index, label)
// ROOT->chased(root), chased->dog(nsubj), chased->cat(dobj), dog->The(det), cat->the(det)
const GOLD = [
{ head: 0, dep: 3, label: 'root' },
{ head: 3, dep: 2, label: 'nsubj' },
{ head: 3, dep: 5, label: 'dobj' },
{ head: 2, dep: 1, label: 'det' },
{ head: 5, dep: 4, label: 'det' }
];
// Parser state: stack holds processed indices, buf holds remaining
let stack = [], buf = [], arcs = [], done = false;
function reset() {
// Initialize parser: ROOT on stack, all real words in buffer
stack = [0];
buf = [1, 2, 3, 4, 5];
arcs = [];
done = false;
setHint('Click <b>Shift</b> to push the next word onto the stack, <b>Arc-Left</b> to make the buffer word the head of the stack-top, or <b>Arc-Right</b> to make the stack-top the head of the buffer word.');
setStatus('', '');
render();
}
// Helper predicates for arc existence
function arcExists(h, d) { return arcs.some(a => a.head === h && a.dep === d); }
function hasHead(d) { return arcs.some(a => a.dep === d); }
function doShift() {
if (buf.length === 0) return;
stack.push(buf.shift());
setHint('Shifted. The word is now on top of the stack.');
setStatus('', '');
render();
checkDone();
}
function doArcLeft() {
// Arc-Left: buffer[0] becomes head of stack-top; pop stack-top
if (stack.length < 2 || buf.length === 0) return;
const s = stack[stack.length - 1];
const b = buf[0];
arcs.push({ head: b, dep: s, label: guessLabel(b, s) });
stack.pop();
setHint('Arc-Left attached. The buffer word is now the head; the stack word was popped.');
setStatus('', '');
render();
checkDone();
}
function doArcRight() {
// Arc-Right: stack-top becomes head of buffer[0]; push buffer[0]
if (stack.length === 0 || buf.length === 0) return;
const s = stack[stack.length - 1];
const b = buf[0];
arcs.push({ head: s, dep: b, label: guessLabel(s, b) });
buf.shift();
stack.push(s === stack[stack.length - 1] ? b : b);
// Push the new dependent onto the stack (arc-eager convention)
stack[stack.length - 1] = b;
setHint('Arc-Right attached. The stack word is now the head of the buffer word.');
setStatus('', '');
render();
checkDone();
}
// Return the gold label for a (head,dep) pair, or '?' if not in gold
function guessLabel(h, d) {
const match = GOLD.find(a => a.head === h && a.dep === d);
return match ? match.label : '?';
}
function checkDone() {
if (buf.length === 0 && stack.length === 1) {
done = true;
const correct = GOLD.every(g => arcs.some(a => a.head === g.head && a.dep === g.dep));
if (correct) {
setStatus('✓ Correct dependency tree!', 'ok');
setHint('Perfect parse! All gold arcs are present.');
} else {
setStatus('✗ Tree has errors. Try resetting and using a different action sequence.', 'bad');
setHint('Parse finished but some arcs are wrong. Press Reset to try again.');
}
updateButtons();
}
}
function setStatus(t, cls) {
const el = document.getElementById('status');
el.textContent = t; el.className = 'status ' + cls;
}
function setHint(t) { document.getElementById('hintPara').innerHTML = t; }
// Re-render words, arcs, and state display
function render() {
renderWords();
renderArcs();
renderState();
updateButtons();
}
function renderWords() {
const row = document.getElementById('sentenceRow');
row.innerHTML = '';
WORDS.forEach((w, i) => {
if (i === 0) return; // Don't render the virtual ROOT chip
const chip = document.createElement('span');
chip.className = 'word-chip';
chip.dataset.idx = i;
chip.textContent = w;
if (stack.includes(i)) chip.classList.add('on-stack');
else if (arcs.some(a => a.dep === i)) chip.classList.add('attached');
row.appendChild(chip);
});
}
// Draw dependency arcs as bezier curves above the sentence
function renderArcs() {
const canvas = document.getElementById('arcCanvas');
const wrap = canvas.parentElement;
const chips = [...document.querySelectorAll('.word-chip')];
if (chips.length === 0) { canvas.width = 0; canvas.height = 0; return; }
const H = 54;
canvas.width = wrap.clientWidth || 400;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Compute horizontal center of each word chip in canvas coordinates
const pos = {};
chips.forEach(ch => {
const idx = parseInt(ch.dataset.idx);
const r = ch.getBoundingClientRect();
const wr = wrap.getBoundingClientRect();
pos[idx] = r.left - wr.left + r.width / 2;
});
arcs.forEach(({ head, dep }) => {
const hIdx = head === 0 ? null : head;
if (hIdx === null) return;
const x1 = pos[hIdx], x2 = pos[dep];
if (x1 === undefined || x2 === undefined) return;
const mid = (x1 + x2) / 2;
const span = Math.abs(x2 - x1);
const cy = H - span * 0.28 - 4;
ctx.beginPath();
ctx.moveTo(x1, H);
ctx.bezierCurveTo(x1, cy, x2, cy, x2, H);
ctx.strokeStyle = '#1d7a5f';
ctx.lineWidth = 1.6;
ctx.stroke();
// Draw arrowhead pointing toward the dependent
const angle = x2 > x1 ? Math.PI / 2 : -Math.PI / 2;
const ax = x2, ay = H - 1;
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(ax - 5 * Math.cos(angle - 0.4), ay - 5 * Math.sin(angle - 0.4));
ctx.moveTo(ax, ay);
ctx.lineTo(ax - 5 * Math.cos(angle + 0.4), ay - 5 * Math.sin(angle + 0.4));
ctx.stroke();
});
}
function renderState() {
document.getElementById('stackVal').textContent =
stack.map(i => WORDS[i]).join(' | ') || '—';
document.getElementById('bufferVal').textContent =
buf.map(i => WORDS[i]).join(' ') || '—';
}
function updateButtons() {
const canShift = !done && buf.length > 0;
const canArcL = !done && stack.length >= 2 && buf.length > 0;
const canArcR = !done && stack.length >= 1 && buf.length > 0;
document.getElementById('btnShift').disabled = !canShift;
document.getElementById('btnArcL').disabled = !canArcL;
document.getElementById('btnArcR').disabled = !canArcR;
}
document.getElementById('btnShift').addEventListener('click', doShift);
document.getElementById('btnArcL').addEventListener('click', doArcLeft);
document.getElementById('btnArcR').addEventListener('click', doArcRight);
document.getElementById('btnReset').addEventListener('click', reset);
reset();
<!-- Demo: arc-eager dependency parser for 'The dog chased the cat' -->
<div class="sentence-row" id="sentenceRow" aria-label="Sentence words"></div>
<div class="arc-canvas-wrap">
<canvas id="arcCanvas" aria-hidden="true"></canvas>
</div>
<div class="state-row">
<div class="state-box">
<div class="state-label">Stack</div>
<div class="state-val" id="stackVal"></div>
</div>
<div class="state-box">
<div class="state-label">Buffer</div>
<div class="state-val" id="bufferVal"></div>
</div>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btnShift" type="button">Shift</button>
<button id="btnArcL" type="button">Arc-Left</button>
<button id="btnArcR" type="button">Arc-Right</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="hint-para" id="hintPara">Click <b>Shift</b> to push the next word onto the stack, <b>Arc-Left</b> to make the buffer word the head of the stack-top, or <b>Arc-Right</b> to make the stack-top the head of the buffer word.</div>
/* Styles for the dependency parsing demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.sentence-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 4px; min-height: 36px; }
.word-chip { padding: 4px 10px; border-radius: 20px; font-size: .9rem; font-weight: 600;
background: #e8eef3; border: 1.5px solid #cdd9e3; color: #1d3557; white-space: nowrap; }
.word-chip.on-stack { background: #1d3557; color: #fff; border-color: #1d3557; }
.word-chip.attached { background: #d0f0d8; border-color: #6cbb88; color: #145227; }
.arc-canvas-wrap { width: 100%; overflow: hidden; margin-bottom: 4px; }
canvas { display: block; width: 100%; }
.state-row { display: flex; gap: 10px; margin: 6px 0; }
.state-box { flex: 1; background: #f4f6f8; border-radius: 8px; padding: 6px 10px; min-height: 36px; }
.state-label { font-size: .72rem; font-weight: 700; color: #5a7088; text-transform: uppercase;
letter-spacing: .04em; margin-bottom: 2px; }
.state-val { font-size: .85rem; color: #1d3557; font-family: ui-monospace, monospace; word-break: break-all; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: 4px 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .38; cursor: not-allowed; }
.hint-para { font-size: .82rem; color: #555; line-height: 1.45; }
// Arc-eager transition system (Nivre 2003) for 'The dog chased the cat'
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Sentence words; index 0 is a virtual ROOT node
const WORDS = ['ROOT', 'The', 'dog', 'chased', 'the', 'cat'];
// Gold standard arcs (head index, dependent index, label)
// ROOT->chased(root), chased->dog(nsubj), chased->cat(dobj), dog->The(det), cat->the(det)
const GOLD = [
{ head: 0, dep: 3, label: 'root' },
{ head: 3, dep: 2, label: 'nsubj' },
{ head: 3, dep: 5, label: 'dobj' },
{ head: 2, dep: 1, label: 'det' },
{ head: 5, dep: 4, label: 'det' }
];
// Parser state: stack holds processed indices, buf holds remaining
let stack = [], buf = [], arcs = [], done = false;
function reset() {
// Initialize parser: ROOT on stack, all real words in buffer
stack = [0];
buf = [1, 2, 3, 4, 5];
arcs = [];
done = false;
setHint('Click <b>Shift</b> to push the next word onto the stack, <b>Arc-Left</b> to make the buffer word the head of the stack-top, or <b>Arc-Right</b> to make the stack-top the head of the buffer word.');
setStatus('', '');
render();
}
// Helper predicates for arc existence
function arcExists(h, d) { return arcs.some(a => a.head === h && a.dep === d); }
function hasHead(d) { return arcs.some(a => a.dep === d); }
function doShift() {
if (buf.length === 0) return;
stack.push(buf.shift());
setHint('Shifted. The word is now on top of the stack.');
setStatus('', '');
render();
checkDone();
}
function doArcLeft() {
// Arc-Left: buffer[0] becomes head of stack-top; pop stack-top
if (stack.length < 2 || buf.length === 0) return;
const s = stack[stack.length - 1];
const b = buf[0];
arcs.push({ head: b, dep: s, label: guessLabel(b, s) });
stack.pop();
setHint('Arc-Left attached. The buffer word is now the head; the stack word was popped.');
setStatus('', '');
render();
checkDone();
}
function doArcRight() {
// Arc-Right: stack-top becomes head of buffer[0]; push buffer[0]
if (stack.length === 0 || buf.length === 0) return;
const s = stack[stack.length - 1];
const b = buf[0];
arcs.push({ head: s, dep: b, label: guessLabel(s, b) });
buf.shift();
stack.push(s === stack[stack.length - 1] ? b : b);
// Push the new dependent onto the stack (arc-eager convention)
stack[stack.length - 1] = b;
setHint('Arc-Right attached. The stack word is now the head of the buffer word.');
setStatus('', '');
render();
checkDone();
}
// Return the gold label for a (head,dep) pair, or '?' if not in gold
function guessLabel(h, d) {
const match = GOLD.find(a => a.head === h && a.dep === d);
return match ? match.label : '?';
}
function checkDone() {
if (buf.length === 0 && stack.length === 1) {
done = true;
const correct = GOLD.every(g => arcs.some(a => a.head === g.head && a.dep === g.dep));
if (correct) {
setStatus('✓ Correct dependency tree!', 'ok');
setHint('Perfect parse! All gold arcs are present.');
} else {
setStatus('✗ Tree has errors. Try resetting and using a different action sequence.', 'bad');
setHint('Parse finished but some arcs are wrong. Press Reset to try again.');
}
updateButtons();
}
}
function setStatus(t, cls) {
const el = document.getElementById('status');
el.textContent = t; el.className = 'status ' + cls;
}
function setHint(t) { document.getElementById('hintPara').innerHTML = t; }
// Re-render words, arcs, and state display
function render() {
renderWords();
renderArcs();
renderState();
updateButtons();
}
function renderWords() {
const row = document.getElementById('sentenceRow');
row.innerHTML = '';
WORDS.forEach((w, i) => {
if (i === 0) return; // Don't render the virtual ROOT chip
const chip = document.createElement('span');
chip.className = 'word-chip';
chip.dataset.idx = i;
chip.textContent = w;
if (stack.includes(i)) chip.classList.add('on-stack');
else if (arcs.some(a => a.dep === i)) chip.classList.add('attached');
row.appendChild(chip);
});
}
// Draw dependency arcs as bezier curves above the sentence
function renderArcs() {
const canvas = document.getElementById('arcCanvas');
const wrap = canvas.parentElement;
const chips = [...document.querySelectorAll('.word-chip')];
if (chips.length === 0) { canvas.width = 0; canvas.height = 0; return; }
const H = 54;
canvas.width = wrap.clientWidth || 400;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Compute horizontal center of each word chip in canvas coordinates
const pos = {};
chips.forEach(ch => {
const idx = parseInt(ch.dataset.idx);
const r = ch.getBoundingClientRect();
const wr = wrap.getBoundingClientRect();
pos[idx] = r.left - wr.left + r.width / 2;
});
arcs.forEach(({ head, dep }) => {
const hIdx = head === 0 ? null : head;
if (hIdx === null) return;
const x1 = pos[hIdx], x2 = pos[dep];
if (x1 === undefined || x2 === undefined) return;
const mid = (x1 + x2) / 2;
const span = Math.abs(x2 - x1);
const cy = H - span * 0.28 - 4;
ctx.beginPath();
ctx.moveTo(x1, H);
ctx.bezierCurveTo(x1, cy, x2, cy, x2, H);
ctx.strokeStyle = '#1d7a5f';
ctx.lineWidth = 1.6;
ctx.stroke();
// Draw arrowhead pointing toward the dependent
const angle = x2 > x1 ? Math.PI / 2 : -Math.PI / 2;
const ax = x2, ay = H - 1;
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(ax - 5 * Math.cos(angle - 0.4), ay - 5 * Math.sin(angle - 0.4));
ctx.moveTo(ax, ay);
ctx.lineTo(ax - 5 * Math.cos(angle + 0.4), ay - 5 * Math.sin(angle + 0.4));
ctx.stroke();
});
}
function renderState() {
document.getElementById('stackVal').textContent =
stack.map(i => WORDS[i]).join(' | ') || '—';
document.getElementById('bufferVal').textContent =
buf.map(i => WORDS[i]).join(' ') || '—';
}
function updateButtons() {
const canShift = !done && buf.length > 0;
const canArcL = !done && stack.length >= 2 && buf.length > 0;
const canArcR = !done && stack.length >= 1 && buf.length > 0;
document.getElementById('btnShift').disabled = !canShift;
document.getElementById('btnArcL').disabled = !canArcL;
document.getElementById('btnArcR').disabled = !canArcR;
}
document.getElementById('btnShift').addEventListener('click', doShift);
document.getElementById('btnArcL').addEventListener('click', doArcLeft);
document.getElementById('btnArcR').addEventListener('click', doArcRight);
document.getElementById('btnReset').addEventListener('click', reset);
reset();
Notice that each step commits immediately — no backtracking. That is what makes the algorithm linear. A correct sequence of 2n−1 actions produces the full dependency tree. Wrong actions accumulate and the tree breaks; press Reset to start over and try a different strategy.
Comments
Loading comments...