Skip to content

Instantly share code, notes, and snippets.

@simonw

simonw/wiki.md Secret

Created June 30, 2026 02:02
Show Gist options
  • Select an option

  • Save simonw/f226fe96f464ec7d81d6996cb466436d to your computer and use it in GitHub Desktop.

Select an option

Save simonw/f226fe96f464ec7d81d6996cb466436d to your computer and use it in GitHub Desktop.

Add Wikipedia import autocomplete

html-table-extractor.html - under the paste box add a "Or import from Wikipedia: [ search box ]" thing - typing in the search box does an autocomplete JSON thing against Wikipedia matching on page titles, when selected it calls https://en.wikipedia.org/w/api.php?action=parse&page=List_of_cities_and_towns_in_the_San_Francisco_Bay_Area&prop=text&format=json&origin=* and then injects the resulting HTML into the textarea and shows the tables from it

39 previous messages

I’ll wire this into the existing page rather than inventing a separate flow. First I’m going to read the current HTML/JS shape so the Wikipedia import lands in the same patterns the tool already uses.

Explored a file, ran 2 commands
Explored a file
  • Read ./html-table-extractor.html
  • Ran pwd && rg --files -g 'html-table-extractor.html' -g 'AGENTS.md'
Explored a file
  • Read ./html-table-extractor.html
  • Ran git status --short

The page is a single-file contenteditable tool, and the existing extraction is driven by innerHTML, so the clean path is to fetch Wikipedia’s parse HTML and drop it straight into that same paste element. I also see unrelated local changes, so I’ll leave those alone.

Explored a file
  • Read ./html-table-extractor.html

I’ve got the insertion point and the extraction flow now. I’m going to add a small autocomplete control, debounce the Wikipedia title lookup, and route the selected page through the same table extraction pipeline.

Edited a file
  • Wrote ./html-table-extractor.html (+346 -51)

The first patch was a little too ambitious and missed an exact context line, so I’m going to split the edit into smaller patches. Same change, just less fussy about anchoring.

Edited a file, explored a file
Explored 3 files
  • Read ./html-table-extractor.html
  • Read ./html-table-extractor.html
  • Read ./html-table-extractor.html
  • Wrote ./html-table-extractor.html (+95 -0)

The styling shell is in place now, including a mobile layout for the import row and a dropdown that stays visually tied to the search field. Next I’m adding the markup and the fetch logic.

Edited a file
  • Wrote ./html-table-extractor.html (+7 -0)

The import control is now in the DOM. I’m going to add the Wikipedia API helpers and event handlers in the script, with selection via click, arrows, or Enter.

Edited a file, explored a file
  • Wrote ./html-table-extractor.html (+10 -0)
Explored 2 files
  • Read ./html-table-extractor.html
  • Read ./html-table-extractor.html
  • Wrote ./html-table-extractor.html (+156 -0)

  • Wrote ./html-table-extractor.html (+27 -0)

The main logic is in now. I’m going to sanity-check the edited file and fetch a sample parse response so I can catch API-shape or browser-side mistakes before calling it done.

Explored a file, ran a command
Explored 2 files
  • Read ./html-table-extractor.html
  • Read ./html-table-extractor.html
  • Ran curl -L 'https://en.wikipedia.org/w/api.php?action=opensearch&search=List%20of%20cities%20and%20towns%20in%20the%20San%20Francisco%20Bay%20Area&namespace=0&limit=5&format=json&origin=*'

The title API returns the expected OpenSearch array for the sample page. I’m doing one more pass now for syntax and the parse response shape, because cross-origin JSON APIs have a way of being just peculiar enough to deserve a check.

Explored a file, ran 2 commands
Explored a file
  • Read ./html-table-extractor.html
  • Ran sed -n '/<script type="module">/,/<\/script>/p' html-table-extractor.html | sed '1d;$d' | node --check -

  • Ran curl -sL 'https://en.wikipedia.org/w/api.php?action=parse&page=List_of_cities_and_towns_in_the_San_Francisco_Bay_Area&prop=text&format=json&origin=*' | node -e 'let s = ""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const data = JSON.parse(s); const html = data.parse && data.parse.text && data.parse.text["*"]; console.log(JSON.stringify({ title: data.parse && data.parse.title, htmlLength: html ? html.length : 0, tableTags: html ? (html.match(/<table/gi) || []).length : 0, hasStyleTag: html ? /<style/i.test(html) : false })); })'

The parse payload is there and includes 13 table tags for the target page. One wrinkle: Wikipedia returns <style> tags in the parsed HTML, so I’m checking whether we should display the imported source as raw HTML text in the paste box rather than executing it as live DOM inside the page.

Ran a command
  • Ran curl -sL 'https://en.wikipedia.org/w/api.php?action=parse&page=List_of_cities_and_towns_in_the_San_Francisco_Bay_Area&prop=text&format=json&origin=*' | node -e 'let s = ""; process.stdin.on("data", c => s += c); process.stdin.on("end", () => { const data = JSON.parse(s); const html = data.parse.text["*"]; const tags = html.match(/<style[\s\S]*?<\/style>/gi) || []; console.log(tags.slice(0, 3).map(t => t.slice(0, 500).replace(/\s+/g, " ")).join("\n---\n")); console.error("styleTags", tags.length); })'

Those styles are mostly scoped to Wikipedia’s parser wrapper, so rendering the imported content as rich HTML should be okay here. I’ll still run the page in a browser if Playwright is available, because that catches the real CORS/autocomplete path better than static inspection.

Ran 2 commands
  • Ran git diff -- html-table-extractor.html
  • Ran node -e 'import("playwright").then(() => console.log("playwright available")).catch(err => { console.error(err.code || err.message); process.exit(1) })'

Playwright is available, so I’m loading the file directly and exercising the actual autocomplete plus import path now. This is the useful bit: it tells us whether file:// plus Wikipedia CORS behaves in the browser, not just in curl.

Ran a command
  • Ran node - <<'NODE' const { chromium } = await import('playwright') const path = await import('node:path') const pagePath = path.resolve('html-table-extractor.html') const browser = await chromium.launch({ headless: true }) const page = await browser.newPage() const messages = [] page.on('console', msg => messages.push(`${msg.type()}: ${msg.text()}`)) page.on('pageerror', err => messages.push(`pageerror: ${err.message}`)) await page.goto(`file://${pagePath}`) await page.fill('#wiki-search', 'List of cities and towns in the San Francisco Bay Area') await page.waitForSelector('.wiki-option', { timeout: 10000 }) const suggestions = await page.$$eval('.wiki-option', els => els.map(el => el.textContent.trim())) await page.click('.wiki-option') await page.waitForFunction(() => document.querySelectorAll('#results .card').length > 0, null, { timeout: 20000 }) const result = await page.evaluate(() => ({ status: document.querySelector('#status').textContent.trim(), cards: document.querySelectorAll('#results .card').length, pasteHasContent: document.querySelector('#paste').classList.contains('has-content'), searchValue: document.querySelector('#wiki-search').value })) await browser.close() console.log(JSON.stringify({ suggestions: suggestions.slice(0, 3), result, messages }, null, 2)) NODE

Playwright is installed but its bundled Chromium is not downloaded. I’m going to try the locally installed browser path next, which should be enough for this one-page smoke test.

Explored 2 lists
  • Listed files in Applications
  • Listed files in Google

Chrome is present. I tripped over the space in the app path on the first check; using the quoted executable path now.

Explored a list, ran a command
Explored a list
  • Listed files in MacOS
  • Ran node - <<'NODE' const { chromium } = await import('playwright') const path = await import('node:path') const pagePath = path.resolve('html-table-extractor.html') const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' }) const page = await browser.newPage() const messages = [] page.on('console', msg => messages.push(`${msg.type()}: ${msg.text()}`)) page.on('pageerror', err => messages.push(`pageerror: ${err.message}`)) await page.goto(`file://${pagePath}`) await page.fill('#wiki-search', 'List of cities and towns in the San Francisco Bay Area') await page.waitForSelector('.wiki-option', { timeout: 10000 }) const suggestions = await page.$$eval('.wiki-option', els => els.map(el => el.textContent.trim())) await page.click('.wiki-option') await page.waitForFunction(() => document.querySelectorAll('#results .card').length > 0, null, { timeout: 20000 }) const result = await page.evaluate(() => ({ status: document.querySelector('#status').textContent.trim(), cards: document.querySelectorAll('#results .card').length, pasteHasContent: document.querySelector('#paste').classList.contains('has-content'), searchValue: document.querySelector('#wiki-search').value })) await browser.close() console.log(JSON.stringify({ suggestions: suggestions.slice(0, 3), result, messages }, null, 2)) NODE

I don't like how the HTML is rendered in the input area when it is selected from Wikipedia, if you paste in HTML that does not happen

The smoke test worked functionally, but rendering Wikipedia HTML directly caused the browser to try loading protocol-relative assets from the imported article. I’m tightening that up by putting the fetched HTML into the paste box as source text and extracting from that string, so the import behaves like a textarea and avoids stray page loads.

Edited a file
  • Wrote ./html-table-extractor.html (+15 -1)

Yes, that’s exactly the rough edge I caught too. I’ve changed the Wikipedia import to put the fetched article HTML into the paste area as literal source text, then extract tables from the same HTML string, so it no longer renders Wikipedia’s markup inside the input area.

Ran 2 commands
  • Ran git diff -- html-table-extractor.html
  • Ran sed -n '/<script type="module">/,/<\/script>/p' html-table-extractor.html | sed '1d;$d' | node --check -

The script check is clean. I’m rerunning the browser smoke test with the literal-source display now, and I’ll check that the paste area contains <table... text rather than live table elements.

Ran a command
  • Ran node - <<'NODE' const { chromium } = await import('playwright') const path = await import('node:path') const pagePath = path.resolve('html-table-extractor.html') const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' }) const page = await browser.newPage() const messages = [] page.on('console', msg => messages.push(`${msg.type()}: ${msg.text()}`)) page.on('pageerror', err => messages.push(`pageerror: ${err.message}`)) await page.goto(`file://${pagePath}`) await page.fill('#wiki-search', 'List of cities and towns in the San Francisco Bay Area') await page.waitForSelector('.wiki-option', { timeout: 10000 }) const suggestions = await page.$$eval('.wiki-option', els => els.map(el => el.textContent.trim())) await page.click('.wiki-option') await page.waitForFunction(() => document.querySelectorAll('#results .card').length > 0, null, { timeout: 20000 }) const result = await page.evaluate(() => { const paste = document.querySelector('#paste') return { status: document.querySelector('#status').textContent.trim(), cards: document.querySelectorAll('#results .card').length, pasteHasContent: paste.classList.contains('has-content'), pasteSourceContent: paste.classList.contains('source-content'), renderedTablesInPaste: paste.querySelectorAll('table').length, sourceStartsLikeHtml: paste.textContent.trim().startsWith('<'), sourceIncludesTableTag: paste.textContent.includes('<table'), searchValue: document.querySelector('#wiki-search').value } }) await browser.close() console.log(JSON.stringify({ suggestions: suggestions.slice(0, 3), result, messages }, null, 2)) NODE

The browser smoke test confirms the important behavior now: the paste area has zero rendered table elements, but its text includes the raw <table source, and the extractor still renders the result cards. I’m making one small race-condition cleanup in the autocomplete so stale suggestions don’t pop back after the field is cleared.

Edited a file
  • Wrote ./html-table-extractor.html (+2 -0)

That last cleanup is in. I’m doing the final quick syntax and browser check after the tiny change, then I’ll wrap it up.

Ran 2 commands
  • Ran git status --short -- html-table-extractor.html image-resize-quality.html .playwright-mcp/
  • Ran sed -n '/<script type="module">/,/<\/script>/p' html-table-extractor.html | sed '1d;$d' | node --check -

Syntax is still clean. The repo has the intended html-table-extractor.html change plus unrelated existing changes in image-resize-quality.html and .playwright-mcp/, which I’m leaving untouched.

Ran a command
  • Ran node - <<'NODE' const { chromium } = await import('playwright') const path = await import('node:path') const pagePath = path.resolve('html-table-extractor.html') const browser = await chromium.launch({ headless: true, executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' }) const page = await browser.newPage() const messages = [] page.on('console', msg => messages.push(`${msg.type()}: ${msg.text()}`)) page.on('pageerror', err => messages.push(`pageerror: ${err.message}`)) await page.goto(`file://${pagePath}`) await page.fill('#wiki-search', 'List of cities and towns in the San Francisco Bay Area') await page.waitForSelector('.wiki-option', { timeout: 10000 }) await page.click('.wiki-option') await page.waitForFunction(() => document.querySelectorAll('#results .card').length > 0, null, { timeout: 20000 }) const result = await page.evaluate(() => { const paste = document.querySelector('#paste') return { status: document.querySelector('#status').textContent.trim(), cards: document.querySelectorAll('#results .card').length, renderedTablesInPaste: paste.querySelectorAll('table').length, sourceIncludesTableTag: paste.textContent.includes('<table') } }) await browser.close() console.log(JSON.stringify({ result, messages }, null, 2)) NODE

Done in html-table-extractor.html. Wikipedia imports now place the fetched HTML into the paste area as literal source text, not rendered DOM, while still extracting and showing the tables.

Verified with Chrome/Playwright: the sample Bay Area page imports successfully, shows Found 4 tables., and the paste area contains raw <table text with 0 rendered tables inside it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment