<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Selenium39</title>
    <description>The latest articles on DEV Community by Selenium39 (@selenium39dev).</description>
    <link>https://dev.to/selenium39dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4100439%2F3b9b152b-34c5-4fa1-b2cd-b9dab3290a00.jpg</url>
      <title>DEV Community: Selenium39</title>
      <link>https://dev.to/selenium39dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/selenium39dev"/>
    <language>en</language>
    <item>
      <title>Antes de publicar tu EPUB: vista previa en tinta electrónica, metadatos, portada y tamaño, sin instalar nada</title>
      <dc:creator>Selenium39</dc:creator>
      <pubDate>Fri, 02 Oct 2026 23:41:34 +0000</pubDate>
      <link>https://dev.to/selenium39dev/antes-de-publicar-tu-epub-vista-previa-en-tinta-electronica-metadatos-portada-y-tamano-sin-11m5</link>
      <guid>https://dev.to/selenium39dev/antes-de-publicar-tu-epub-vista-previa-en-tinta-electronica-metadatos-portada-y-tamano-sin-11m5</guid>
      <description>&lt;p&gt;Si generas EPUBs con Pandoc, Sigil, Quarto, mdBook o tu propio script, seguro que te ha pasado: en el ordenador el libro se ve perfecto, pero en un Kindle o un Kobo las imÃ¡genes salen negras, el tÃ­tulo aparece como "Untitled" y el archivo pesa 90 MB.&lt;/p&gt;

&lt;p&gt;Esta es una lista de comprobaciÃ³n para &lt;strong&gt;revisar un EPUB antes de enviarlo a un lector de tinta electrÃ³nica&lt;/strong&gt;. Todo se hace con herramientas gratuitas de &lt;a href="https://e-ink.me/es/tools" rel="noopener noreferrer"&gt;E-Ink&lt;/a&gt; que funcionan &lt;strong&gt;en el navegador, sin subir el archivo&lt;/strong&gt; a ningÃºn servidor.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Para EPUBs sin DRM: libros propios, documentaciÃ³n, apuntes, obras de dominio pÃºblico.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Â¿CÃ³mo se ve en tinta electrÃ³nica?
&lt;/h2&gt;

&lt;p&gt;Abre la &lt;a href="https://e-ink.me/es/tools/epub-eink-preview" rel="noopener noreferrer"&gt;vista previa EPUB para e-ink&lt;/a&gt;. Acepta EPUB, MOBI, AZW3, FB2, FBZ y CBZ.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dispositivo:&lt;/strong&gt; Kindle (11.Âª gen.) 1072Ã—1448, Kindle Paperwhite (11.Âª gen.) 1236Ã—1648, Paperwhite (12.Âª gen.) 1264Ã—1680, Kobo Clara BW, Kobo Libra 2 / Colour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modo de pantalla:&lt;/strong&gt; blanco y negro, 4 niveles de gris o color.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TipografÃ­a:&lt;/strong&gt; tamaÃ±o de letra, con serifa o sin serifa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Texto vertical (CJK)&lt;/strong&gt; para libros en japonÃ©s o chino.&lt;/li&gt;
&lt;li&gt;Marco del dispositivo opcional.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;QuÃ© revisar:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problema&lt;/th&gt;
&lt;th&gt;Por quÃ© pasa&lt;/th&gt;
&lt;th&gt;SoluciÃ³n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Diagramas que se vuelven manchas&lt;/td&gt;
&lt;td&gt;Grises oscuros que se funden con 4 niveles&lt;/td&gt;
&lt;td&gt;MÃ¡s contraste, o lÃ­neas en vez de rellenos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pies de foto ilegibles&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;color: #aaa&lt;/code&gt; en el CSS&lt;/td&gt;
&lt;td&gt;Texto secundario como mucho en &lt;code&gt;#555&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tablas que se salen&lt;/td&gt;
&lt;td&gt;Anchos fijos en &lt;code&gt;px&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Usa &lt;code&gt;%&lt;/code&gt; o &lt;code&gt;em&lt;/code&gt;, o convierte la tabla en lista&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloques de cÃ³digo cortados&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;white-space: pre&lt;/code&gt; sin ajuste&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pre { white-space: pre-wrap; }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ojo: es una simulaciÃ³n con filtros de escala de grises y contraste. Sirve para iterar rÃ¡pido, pero &lt;strong&gt;no sustituye a Kindle Previewer&lt;/strong&gt; para la revisiÃ³n final si vas a publicar en KDP.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Metadatos: lo que se ve en la estanterÃ­a
&lt;/h2&gt;

&lt;p&gt;El tÃ­tulo y el autor que muestra el lector salen del archivo OPF del EPUB, no del nombre del archivo. Si en Pandoc olvidaste el bloque &lt;code&gt;title&lt;/code&gt;/&lt;code&gt;author&lt;/code&gt;, el libro aparecerÃ¡ sin tÃ­tulo.&lt;/p&gt;

&lt;p&gt;El &lt;a href="https://e-ink.me/es/tools/epub-metadata-editor" rel="noopener noreferrer"&gt;editor de metadatos EPUB&lt;/a&gt; permite cambiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tÃ­tulo, autor, ISBN, &lt;strong&gt;idioma&lt;/strong&gt;, editorial y descripciÃ³n&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los campos que dejes vacÃ­os &lt;strong&gt;conservan su valor&lt;/strong&gt; (no se borran). Pon el idioma correcto (&lt;code&gt;es&lt;/code&gt;): el lector lo usa para la separaciÃ³n silÃ¡bica y el diccionario.&lt;/p&gt;

&lt;p&gt;Si despuÃ©s de cambiarlo el lector sigue mostrando el tÃ­tulo antiguo, borra el libro del dispositivo y vuelve a copiarlo; suele ser cachÃ©.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Portada
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://e-ink.me/es/tools/epub-cover-changer" rel="noopener noreferrer"&gt;Cambiar portada EPUB&lt;/a&gt;: reemplazar, aÃ±adir o quitar la portada, con la actual y la nueva lado a lado.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://e-ink.me/es/tools/epub-cover-extractor" rel="noopener noreferrer"&gt;Extraer portada EPUB&lt;/a&gt;: guarda la portada actual como imagen antes de cambiarla.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En tinta electrÃ³nica la portada se ve en grises. Comprueba que el tÃ­tulo contraste por &lt;strong&gt;luminosidad&lt;/strong&gt;, no solo por color.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. TamaÃ±o del archivo
&lt;/h2&gt;

&lt;p&gt;Si tu libro tiene muchas imÃ¡genes a resoluciÃ³n de impresiÃ³n, el &lt;a href="https://e-ink.me/es/compress/epub" rel="noopener noreferrer"&gt;compresor EPUB&lt;/a&gt; te muestra el porcentaje que ocupan las imÃ¡genes y ofrece estos presets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sin pÃ©rdida:&lt;/strong&gt; solo reempaqueta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Equilibrado:&lt;/strong&gt; imÃ¡genes de hasta 1920 px.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lector electrÃ³nico&lt;/strong&gt; (recomendado): hasta 1400 px, pensado para pantallas de 6â€“8".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extremo:&lt;/strong&gt; hasta 800 px.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TamaÃ±o objetivo:&lt;/strong&gt; indicas los MB y reintenta hasta conseguirlo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Recuento de palabras
&lt;/h2&gt;

&lt;p&gt;El &lt;a href="https://e-ink.me/es/tools/epub-word-count" rel="noopener noreferrer"&gt;contador de palabras EPUB&lt;/a&gt; da el total de palabras y caracteres, el &lt;strong&gt;desglose por capÃ­tulo&lt;/strong&gt; y el tiempo estimado de lectura segÃºn la velocidad que elijas. El desglose tambiÃ©n ayuda a detectar capÃ­tulos vacÃ­os (un error tÃ­pico al generar el Ã­ndice).&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Si el EPUB no abre
&lt;/h2&gt;

&lt;p&gt;"No es un EPUB vÃ¡lido" casi siempre significa que el empaquetado estÃ¡ roto (el &lt;code&gt;mimetype&lt;/code&gt; no va primero, falta &lt;code&gt;container.xml&lt;/code&gt;), no el contenido. Pasa el archivo por &lt;a href="https://e-ink.me/es/tools/repair-epub" rel="noopener noreferrer"&gt;Reparar EPUB&lt;/a&gt; y, si el Ã­ndice falla, por &lt;a href="https://e-ink.me/es/tools/fix-epub-toc" rel="noopener noreferrer"&gt;Arreglar Ã­ndice EPUB&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: automatÃ­zalo en tu pipeline
&lt;/h2&gt;

&lt;p&gt;Si generas el libro desde Markdown, puedes convertir con &lt;a href="https://e-ink.me/es/convert/markdown-to-epub" rel="noopener noreferrer"&gt;Markdown a EPUB&lt;/a&gt; o usar la API REST de conversiÃ³n (documentada en &lt;a href="https://e-ink.me/es/developers" rel="noopener noreferrer"&gt;e-ink.me/es/developers&lt;/a&gt;). Las comprobaciones visuales de arriba conviene hacerlas a mano al menos en la primera versiÃ³n.&lt;/p&gt;

&lt;p&gt;Y para enviarlo: el botÃ³n &lt;strong&gt;Send to Kindle&lt;/strong&gt; de E-Ink manda el archivo directamente a tu Kindle (1 crÃ©dito por envÃ­o, despuÃ©s de guardar tu direcciÃ³n &lt;code&gt;@kindle.com&lt;/code&gt; y autorizar el remitente en Amazon).&lt;/p&gt;

&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://e-ink.me/es/tools/epub-eink-preview" rel="noopener noreferrer"&gt;Vista previa e-ink&lt;/a&gt;: blanco y negro, 4 grises.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://e-ink.me/es/tools/epub-metadata-editor" rel="noopener noreferrer"&gt;Metadatos&lt;/a&gt;: tÃ­tulo, autor, idioma.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://e-ink.me/es/tools/epub-cover-changer" rel="noopener noreferrer"&gt;Portada&lt;/a&gt; legible en grises.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://e-ink.me/es/compress/epub" rel="noopener noreferrer"&gt;Comprimir&lt;/a&gt; si pesa demasiado.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://e-ink.me/es/tools/epub-word-count" rel="noopener noreferrer"&gt;Contar palabras&lt;/a&gt; y revisar capÃ­tulos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Y si te apetece un descanso sin salir del Kindle: &lt;a href="https://games.e-ink.me/es/" rel="noopener noreferrer"&gt;games.e-ink.me/es&lt;/a&gt; tiene sudoku, nonogramas y buscaminas sin animaciones, pensados para tinta electrÃ³nica.&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
    <item>
      <title>Reading a whole online programming book on a Kindle: from a table-of-contents URL to your device</title>
      <dc:creator>Selenium39</dc:creator>
      <pubDate>Fri, 02 Oct 2026 23:41:22 +0000</pubDate>
      <link>https://dev.to/selenium39dev/reading-a-whole-online-programming-book-on-a-kindle-from-a-table-of-contents-url-to-your-device-5g45</link>
      <guid>https://dev.to/selenium39dev/reading-a-whole-online-programming-book-on-a-kindle-from-a-table-of-contents-url-to-your-device-5g45</guid>
      <description>&lt;p&gt;A lot of good technical books live on the web as a table-of-contents page with one HTML page per chapter. Free books, official language guides, long tutorial series, and documentation "books" built with mdBook, Sphinx or GitBook all follow this pattern. They read fine in a browser. On an e-reader they're a pain: you'd have to save every chapter by hand, stitch the files together, fix the order, and then get the result onto the device.&lt;/p&gt;

&lt;p&gt;This post walks through a workflow that handles the whole thing in a few minutes with two features E-Ink (&lt;a href="https://e-ink.me/en" rel="noopener noreferrer"&gt;e-ink.me&lt;/a&gt;) added recently: &lt;strong&gt;Whole book&lt;/strong&gt; mode and &lt;strong&gt;Send to Kindle&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Only do this with content you're allowed to keep a personal copy of: open-licensed books, docs under permissive licenses, your own writing, and so on. Respect each site's terms.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why not just "print to PDF"?
&lt;/h2&gt;

&lt;p&gt;PDF is a fixed page. A 6- or 7-inch e-ink screen has to zoom and pan through it, and code blocks become tiny. EPUB is reflowable: the reader picks the font size, long lines wrap, and the table of contents becomes real navigation. For anything you plan to &lt;em&gt;read&lt;/em&gt; rather than print, EPUB wins.&lt;/p&gt;

&lt;p&gt;The catch is that turning 40 web pages into one EPUB with a sane TOC is tedious work. That's the part to automate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: find the table-of-contents page
&lt;/h2&gt;

&lt;p&gt;You want the page that links to every chapter in reading order. Typical candidates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the "Contents" or "Table of contents" page of an online book&lt;/li&gt;
&lt;li&gt;the sidebar index page of an mdBook or Sphinx site (often the book's landing page)&lt;/li&gt;
&lt;li&gt;the index page of a multi-part tutorial series&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pages that list chapters as plain links in order work best. A JavaScript-only menu that appears after a click is harder for any tool to parse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: let the tool detect the chapters
&lt;/h2&gt;

&lt;p&gt;On the &lt;a href="https://e-ink.me/en" rel="noopener noreferrer"&gt;E-Ink homepage&lt;/a&gt; the converter has three tabs: &lt;em&gt;Single page&lt;/em&gt;, &lt;em&gt;Multiple pages&lt;/em&gt; and &lt;strong&gt;Whole book&lt;/strong&gt;. In Whole book mode:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Paste the table-of-contents URL.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Detect chapters&lt;/strong&gt;. You get the book title and a chapter list ("N chapters detected").&lt;/li&gt;
&lt;li&gt;Uncheck what you don't need: forewords, changelogs, "about this book" pages, index pages. &lt;em&gt;Select all&lt;/em&gt; is there if you want everything.&lt;/li&gt;
&lt;li&gt;The button shows the price before anything runs: &lt;strong&gt;3 credits per chapter&lt;/strong&gt;, with a cap badge for long books. New accounts get 10 free credits, so a short series fits in the free tier and longer books cost a few cents per chapter (&lt;a href="https://e-ink.me/en/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Confirm. Progress shows as &lt;code&gt;done/total chapters&lt;/code&gt;. Chapters that fail are refunded automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When it finishes you download the EPUB. If you close the dialog without saving, you can regenerate the same book for free within 24 hours, so a misclick doesn't cost you anything.&lt;/p&gt;

&lt;p&gt;Trimming the chapter list is the most useful step here. Documentation books tend to include pages that make sense in a browser and are pure noise in a book (search pages, "edit this page" stubs, API index dumps). Removing them before conversion keeps the TOC clean and the cost down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: push it to the Kindle without a cable
&lt;/h2&gt;

&lt;p&gt;Amazon's Send to Kindle has accepted EPUB since 2022, and current Kindles open those files natively. E-Ink now has a &lt;strong&gt;Send to Kindle&lt;/strong&gt; button, so you don't have to download the file and email it to yourself.&lt;/p&gt;

&lt;p&gt;One-time setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find your Kindle's email address (&lt;code&gt;something@kindle.com&lt;/code&gt;). It's in Amazon's &lt;em&gt;Manage Your Content and Devices â†’ Preferences â†’ Personal Document Settings&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;In the same section, add E-Ink's sender address to the &lt;strong&gt;Approved Personal Document E-mail List&lt;/strong&gt;. The Send to Kindle dialog has a "Copy sender address" button and a link to Amazon's settings.&lt;/li&gt;
&lt;li&gt;Save your Kindle address in the &lt;a href="https://e-ink.me/en/dashboard" rel="noopener noreferrer"&gt;E-Ink dashboard&lt;/a&gt; under &lt;em&gt;Send to Kindle&lt;/em&gt;, so you don't have to type it again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that, hit &lt;strong&gt;Send to Kindle&lt;/strong&gt; and the book shows up on the device a few minutes later, as long as it's on Wi-Fi. Each delivery costs 1 credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: check the result on the device
&lt;/h2&gt;

&lt;p&gt;A few things are worth a quick look on the first book from a new site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code blocks.&lt;/strong&gt; Long lines wrap on small screens. If a book is code-heavy, try landscape mode or a smaller font for those chapters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images and diagrams.&lt;/strong&gt; They come through, but dense diagrams can be hard to read on a 6" screen. You can preview the EPUB in the browser first with the &lt;a href="https://e-ink.me/en/tools/epub-eink-preview" rel="noopener noreferrer"&gt;EPUB e-ink preview&lt;/a&gt; tool. It simulates Kindle and Kobo screen sizes in B&amp;amp;W or 4-level grayscale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Title and author.&lt;/strong&gt; Some sites put the site name in the page title, and that ends up on your Kindle shelf. The &lt;a href="https://e-ink.me/en/tools/epub-metadata-editor" rel="noopener noreferrer"&gt;EPUB metadata editor&lt;/a&gt; fixes title, author, language and description locally in the browser before you send.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automating the single-page part
&lt;/h2&gt;

&lt;p&gt;If you prefer scripts, the regular web-to-EPUB conversion is also available as a REST API (&lt;code&gt;POST /api/v1/convert/webpage&lt;/code&gt;, Bearer key, documented at &lt;a href="https://e-ink.me/en/developers" rel="noopener noreferrer"&gt;e-ink.me/en/developers&lt;/a&gt;). Pass several URLs and they're merged into one EPUB. That works well for a cron job that bundles a weekly reading list. Whole-book detection with chapter selection is a UI feature for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on reading breaks
&lt;/h2&gt;

&lt;p&gt;If you're working through a long technical book on an e-reader, short breaks work better on the same device than on a phone. There's a set of &lt;a href="https://games.e-ink.me/en/" rel="noopener noreferrer"&gt;free browser puzzle games built for e-ink screens&lt;/a&gt; (Sudoku, Nonogram, Minesweeper and others, with no animations to smear the display) that run in the Kindle's experimental browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Find the table-of-contents page.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Whole book&lt;/strong&gt; mode on &lt;a href="https://e-ink.me/en" rel="noopener noreferrer"&gt;e-ink.me&lt;/a&gt; and trim the chapter list.&lt;/li&gt;
&lt;li&gt;Set up &lt;strong&gt;Send to Kindle&lt;/strong&gt; once, then deliver with one click.&lt;/li&gt;
&lt;li&gt;Preview and fix metadata if the book is going to sit on your shelf for a while.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It takes a few minutes instead of an afternoon of copy-pasting, and you end up reading the book on a screen that doesn't buzz.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>Generating puzzles with exactly one solution: Sudoku, Nonogram and a daily seed, in plain JavaScript</title>
      <dc:creator>Selenium39</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:56:23 +0000</pubDate>
      <link>https://dev.to/selenium39dev/generating-puzzles-with-exactly-one-solution-sudoku-nonogram-and-a-daily-seed-in-plain-javascript-1f57</link>
      <guid>https://dev.to/selenium39dev/generating-puzzles-with-exactly-one-solution-sudoku-nonogram-and-a-daily-seed-in-plain-javascript-1f57</guid>
      <description>&lt;p&gt;When I built a set of browser puzzle games meant to run on e-ink readers (&lt;a href="https://games.e-ink.me/en/" rel="noopener noreferrer"&gt;E-Ink Games&lt;/a&gt;), one requirement shaped almost every piece of code: &lt;strong&gt;every puzzle must have exactly one solution&lt;/strong&gt;. On a slow, paper-like screen nobody wants to guess and backtrack; the fun is pure deduction. That turned out to be a nice little algorithms problem, so here is a simplified version of how generators like these work, with code you can lift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "exactly one solution" matters
&lt;/h2&gt;

&lt;p&gt;A Sudoku or Nonogram with two valid solutions forces the player to guess at some point. Guessing on a phone is annoying. Guessing on an e-ink display, where every mistaken fill costs a visible refresh, is miserable. It also breaks the promise that "logic is always enough", which is the whole appeal of these puzzles.&lt;/p&gt;

&lt;p&gt;So the generator pipeline is always:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Produce a random &lt;strong&gt;complete solution&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Remove information (clues) while a &lt;strong&gt;solver confirms the solution is still unique&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Stop when the target difficulty is reached.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trick is step 2: you need a solver that can count solutions, and it needs to stop at 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sudoku: fill, then dig
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: a random full grid
&lt;/h3&gt;

&lt;p&gt;Backtracking with shuffled candidates gives you a uniformly-ish random valid grid quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canPlace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;br&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;bc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nx"&gt;br&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;bc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canPlace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: count solutions, but stop at two
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;countSolutions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// pick the empty cell with the fewest candidates (MRV heuristic)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestCands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canPlace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bestCands&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bestCands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bestCands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// grid full&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;bestCands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;countSolutions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The minimum-remaining-values heuristic is what keeps this fast enough to run on a Kindle's modest CPU. Without it, an Expert board with ~22 clues can take noticeably long to verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: dig holes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makePuzzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetClues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;solution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;puzzle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;81&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;targetClues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;countSolutions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// put it back&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;solution&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clue count is a rough proxy for difficulty. In the live game, easier boards give around forty givens and Expert leaves just over twenty. A better difficulty metric is "which techniques does a human-style solver need" (naked singles only vs. pairs vs. X-wings), and that is a good next step if you want finer control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nonogram: the uniqueness problem is harder
&lt;/h2&gt;

&lt;p&gt;A nonogram's clues are the run lengths of filled cells in each row and column. Generating a random picture is trivial; the problem is that &lt;strong&gt;most random pictures are ambiguous&lt;/strong&gt;. Two different grids can share the same clues (the classic 2×2 checkerboard is the smallest example).&lt;/p&gt;

&lt;p&gt;A practical approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a random grid at a target fill density (around 50–60% keeps it interesting). Mirroring it gives a symmetric, more "picture-like" result.&lt;/li&gt;
&lt;li&gt;Compute clues.&lt;/li&gt;
&lt;li&gt;Run a &lt;strong&gt;line solver&lt;/strong&gt;: repeatedly, for each row/column, enumerate all placements of its runs consistent with already-known cells, and mark cells that are filled (or empty) in &lt;em&gt;every&lt;/em&gt; placement.&lt;/li&gt;
&lt;li&gt;If line solving alone determines the whole grid, the puzzle is uniquely solvable &lt;em&gt;by logic&lt;/em&gt;. If it stalls, regenerate.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// All placements of runs `clue` in a line of length n, consistent with known cells&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;placements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;clue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;full&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;clue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;placements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Intersecting all placements per line and iterating until nothing changes is exactly what a human does ("overlap from both ends"), so "line-solvable" is a great definition of "fair". It is stricter than "unique" (some unique puzzles need lookahead), and that strictness is a feature for a casual game.&lt;/p&gt;

&lt;p&gt;Boards in the game are 5×5, 10×10 and 15×15. For 15×15 the enumeration can explode on long lines with many short runs; caching placements per (clue, known-mask) keeps it manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Daily puzzles without a server
&lt;/h2&gt;

&lt;p&gt;Both the Wordle-style word game and the daily nonogram give every player worldwide the same puzzle on the same day, with no backend. The idea: derive a seed from the date, feed it to a small deterministic PRNG, and run the same generator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2166136261&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// FNV-1a&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16777619&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mulberry32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mh"&gt;0x6D2B79F5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4294967296&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// pick one timezone convention!&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mulberry32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hashString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nonogram:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never use &lt;code&gt;Math.random()&lt;/code&gt; anywhere in the generator path&lt;/strong&gt; once you go deterministic, or players will get different boards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a timezone convention&lt;/strong&gt; and stick to it, otherwise players on different sides of midnight disagree about which puzzle is "today".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bonus: an unbeatable Tic-Tac-Toe
&lt;/h2&gt;

&lt;p&gt;The Hard mode of the Tic-Tac-Toe game is plain minimax. With only 9 cells the full tree is tiny (&amp;lt; 550k nodes without pruning), so no alpha-beta or memoization is needed even on an e-reader. The best a human can do is a draw, which is a surprisingly fun challenge in itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Generate a full solution, remove clues, and verify uniqueness with a solver that stops at 2.&lt;/li&gt;
&lt;li&gt;MRV makes Sudoku counting fast enough for weak hardware.&lt;/li&gt;
&lt;li&gt;For nonograms, "solvable by line logic" is a better target than "unique".&lt;/li&gt;
&lt;li&gt;Date-seeded PRNGs give you daily shared puzzles with zero backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see these generators in action, all ten games (Sudoku, Nonogram, Minesweeper, 2048, Word Daily and more) are free at &lt;a href="https://games.e-ink.me/en/" rel="noopener noreferrer"&gt;games.e-ink.me&lt;/a&gt;, no account and no ads. They are designed for Kindle and BOOX browsers but work fine on a desktop too.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>EPUB to Markdown: Getting Books Into Obsidian, Git and Your LLM Pipeline</title>
      <dc:creator>Selenium39</dc:creator>
      <pubDate>Thu, 24 Sep 2026 22:45:05 +0000</pubDate>
      <link>https://dev.to/selenium39dev/epub-to-markdown-getting-books-into-obsidian-git-and-your-llm-pipeline-476h</link>
      <guid>https://dev.to/selenium39dev/epub-to-markdown-getting-books-into-obsidian-git-and-your-llm-pipeline-476h</guid>
      <description>&lt;p&gt;Ebooks are strangely locked away. The text is right there (an EPUB is literally a ZIP of XHTML files), yet it lives inside reader apps that don't talk to the rest of your tools. You can't grep it, diff it, link it from your notes, or chunk it for a retrieval pipeline without some glue.&lt;/p&gt;

&lt;p&gt;Markdown is the glue. Once a book is a &lt;code&gt;.md&lt;/code&gt; file, it drops into the plain-text ecosystem: Obsidian vaults, Notion imports, static-site generators, Git repos, and any script that eats text.&lt;/p&gt;

&lt;p&gt;This post walks through converting an EPUB to Markdown in the browser, what survives the conversion, and a couple of small scripts for the most common next steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just extract plain text?
&lt;/h2&gt;

&lt;p&gt;You could &lt;code&gt;unzip&lt;/code&gt; the EPUB and strip tags, or convert to TXT. That works for word-frequency experiments, but you lose the one thing that makes a book navigable: &lt;strong&gt;structure&lt;/strong&gt;. Chapter titles, section headings, lists, block quotes, and links all flatten into an undifferentiated wall.&lt;/p&gt;

&lt;p&gt;Markdown keeps that structure with almost no syntax overhead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;chapter and section titles become &lt;code&gt;#&lt;/code&gt; headings&lt;/li&gt;
&lt;li&gt;lists stay lists (nesting included)&lt;/li&gt;
&lt;li&gt;links stay links&lt;/li&gt;
&lt;li&gt;quotes keep their &lt;code&gt;&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;code blocks and tables survive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For humans, that means the file still reads like a book in a text editor. For machines, it means explicit hierarchy you can split on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting in the browser
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://iloveepub.com/epub-to-markdown" rel="noopener noreferrer"&gt;iloveepub EPUB to Markdown converter&lt;/a&gt; does the whole thing client-side. You pick a file, each chapter is parsed and rebuilt as Markdown, a progress bar tracks it, and you download a single &lt;code&gt;.md&lt;/code&gt; file. The book is never uploaded, which you can confirm yourself: open the Network tab in DevTools while it runs and you'll see zero upload requests. No account or API key is needed, and there's no rate limit.&lt;/p&gt;

&lt;p&gt;Steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the converter and select your &lt;code&gt;.epub&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Convert to Markdown&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;.md&lt;/code&gt; file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's genuinely it. There are no options to tune, which is a feature when you just want the text out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes out (and what doesn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Carried over:&lt;/strong&gt; headings (as &lt;code&gt;#&lt;/code&gt; levels), bold and italics, links, lists including nested ones, block quotes, code blocks, tables, and horizontal rules. The rule of thumb is that anything the book marks up &lt;em&gt;semantically&lt;/em&gt; survives; purely visual styling (fonts, colors, custom spacing) doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Images:&lt;/strong&gt; each one becomes a standard reference like &lt;code&gt;![alt text](original-path)&lt;/code&gt;, preserving alt text and the original path. The image files themselves aren't exported; the output is one self-contained text file. If you need the images too, unzip the EPUB separately and copy its image folder next to the &lt;code&gt;.md&lt;/code&gt; file. The references keep the paths used inside the book, so a quick find-and-replace on the folder prefix is usually all it takes to make them resolve in your vault.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DRM-protected books&lt;/strong&gt; can't be opened by any browser tool, so this is for DRM-free purchases, public-domain titles, and your own documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next step #1: one note per chapter for Obsidian
&lt;/h2&gt;

&lt;p&gt;Dropping a 400 KB single file into a vault works, but one note per chapter is nicer for linking and backlinks. Here's a small Python script that splits on the shallowest heading level that repeats, which is almost always the chapter level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Pick the shallowest heading level that appears more than once
# (so a single "# Book Title" at the top doesn't swallow everything)
&lt;/span&gt;&lt;span class="n"&gt;levels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^(#{1,6}) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no headings found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;repeated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lv&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repeated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repeated&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;rf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^(?=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; )&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;part-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^\w\- ]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrote &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;python split_md.py my-book.md&lt;/code&gt;, then move the folder into your vault. The zero-padded prefix keeps chapters sorted in the file explorer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next step #2: chunking for RAG
&lt;/h2&gt;

&lt;p&gt;If you're feeding books to a language model for Q&amp;amp;A, summaries, or retrieval, Markdown is about as LLM-friendly as a book gets: one clean file, explicit heading hierarchy, and no layout noise for the model to trip over.&lt;/p&gt;

&lt;p&gt;Headings also make natural chunk boundaries. Rather than slicing every N tokens and cutting paragraphs in half, split on headings and carry the heading path as metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;section&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &amp;gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^(#{1,6}) (.*)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[:]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each chunk arrives with its context attached (&lt;code&gt;"Part Two &amp;gt; Chapter 7 &amp;gt; The Harbor"&lt;/code&gt;), which tends to give a retriever (and the model reading the results) far more to work with than anonymous fixed-size windows. Swap the naive character split for a token-aware splitter if you need precise budgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other things people do with it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version a manuscript.&lt;/strong&gt; Convert successive drafts and &lt;code&gt;git diff&lt;/code&gt; them. Prose diffs are much easier to review in Markdown than in XHTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish excerpts.&lt;/strong&gt; Most static-site generators (Hugo, Astro, Jekyll, Eleventy) take Markdown directly, so a public-domain book can become a website with a front-matter block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search your library.&lt;/strong&gt; A folder of &lt;code&gt;.md&lt;/code&gt; books plus &lt;code&gt;ripgrep&lt;/code&gt; is a surprisingly good full-text search engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import into Notion.&lt;/strong&gt; The output is standard CommonMark-flavored Markdown, so Notion's Markdown import keeps headings, lists, and links intact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Books deserve to be first-class text, not blobs trapped in a reader app. Converting EPUB to Markdown is the shortest path from "file on disk" to "something my tools understand." Convert one book, split it, and see how differently you use it once it lives next to your notes.&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Process EPUB files in the browser — no uploads</title>
      <dc:creator>Selenium39</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:31:15 +0000</pubDate>
      <link>https://dev.to/selenium39dev/process-epub-files-in-the-browser-no-uploads-1eee</link>
      <guid>https://dev.to/selenium39dev/process-epub-files-in-the-browser-no-uploads-1eee</guid>
      <description>&lt;p&gt;EPUB files are ZIP-based books, so many useful operations can happen entirely in the browser. That means a reader can inspect metadata, validate a package, or prepare a file without sending their book to a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side processing matters
&lt;/h2&gt;

&lt;p&gt;Books can contain personal notes, unpublished writing, reading history, or licensed material. A local-first workflow keeps the original file on the user’s device and reduces both privacy risk and infrastructure cost. It also works well for offline-capable web apps.&lt;/p&gt;

&lt;p&gt;A typical implementation uses the File API and a ZIP reader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;archive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;unzip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// use a maintained ZIP library&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;META-INF/container.xml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the app can locate &lt;code&gt;content.opf&lt;/code&gt;, read the manifest and spine, and offer a focused transformation. Keep processing in memory where possible, explain permissions clearly, and never upload the file unless the user explicitly chooses a remote feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical browser-local tool
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://iloveepub.com" rel="noopener noreferrer"&gt;I Love EPUB&lt;/a&gt; is a useful collection of browser-based EPUB tools for common tasks such as compressing, converting, and editing books. The local-processing approach is a good default: fast feedback, fewer data transfers, and a simpler privacy story.&lt;/p&gt;

&lt;p&gt;For production apps, test large books, malformed archives, and mobile memory limits. Clear error messages and a visible “nothing uploaded” promise can make local-first EPUB workflows much easier to trust.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>epub</category>
    </item>
    <item>
      <title>A web-to-EPUB pipeline: readable extract, chapter structure, Pandoc</title>
      <dc:creator>Selenium39</dc:creator>
      <pubDate>Sat, 29 Aug 2026 15:37:33 +0000</pubDate>
      <link>https://dev.to/selenium39dev/a-web-to-epub-pipeline-readable-extract-chapter-structure-pandoc-2cda</link>
      <guid>https://dev.to/selenium39dev/a-web-to-epub-pipeline-readable-extract-chapter-structure-pandoc-2cda</guid>
      <description>&lt;p&gt;&lt;code&gt;pandoc -f html -t epub3 https://example.com/post -o post.epub&lt;/code&gt; looks like a complete product. It is not. Pandoc is a typesetter. It will faithfully encode whatever tree you give it, including the cookie banner.&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://e-ink.me" rel="noopener noreferrer"&gt;E-Ink&lt;/a&gt;, a small web-to-EPUB service. This is the pipeline underneath it, and why the interesting work is not the EPUB zip.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Fetch what a reader actually saw
&lt;/h2&gt;

&lt;p&gt;A lot of the pages people want on a Kindle are JS-rendered docs, not static article HTML. A single &lt;code&gt;GET&lt;/code&gt; of the URL returns a shell. You need a rendered DOM (we use Firecrawl for this), not wget. That already puts you in “this costs something” territory, which is why a free-forever scraper that also produces pretty EPUBs is usually lying about one of those adjectives.&lt;/p&gt;

&lt;p&gt;Public pages only. No login cookies, no paywall punching. If the extract is empty, fail loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Extract the document, not the application
&lt;/h2&gt;

&lt;p&gt;Readability-style extraction is the whole game. Keep title, byline, headings, lists, blockquotes, &lt;code&gt;&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;&lt;/code&gt;, figures + captions. Drop nav, footers, related rails, signup modals, comment widgets.&lt;/p&gt;

&lt;p&gt;Two failure modes I still see weekly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Under-extract.&lt;/strong&gt; The article is a docs page whose “body” is a tabbed component. You get the first tab and a blank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-extract.&lt;/strong&gt; You kept the sidebar TOC as if it were chapter one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markdown is a good intermediate. It is diffable, previewable, and Pandoc eats it. If the Markdown looks wrong, do not bother emitting EPUB. We show a Markdown preview in the UI for exactly this reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Headings → spine, not a wall of &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;EPUB readers live and die by the nav document. A 8,000-word essay with one &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; and no &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; is a brick. A docs page that uses &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; for everything produces a TOC that is either empty or 90 entries deep.&lt;/p&gt;

&lt;p&gt;Normalize heading levels against the document you actually got, not against the HTML5 spec the CMS ignored. Promote or demote so the spine has a usable chapter list. This is boring string work and it is more important than your CSS.&lt;/p&gt;

&lt;p&gt;Images: resolve &lt;code&gt;src&lt;/code&gt; / &lt;code&gt;srcset&lt;/code&gt; / &lt;code&gt;data-src&lt;/code&gt;, download, cap dimensions, rewrite to package-relative paths. Lazy-load placeholders are the number one “why is my ebook a gray box” bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Pandoc for EPUB3, not for fetching
&lt;/h2&gt;

&lt;p&gt;Once you have a clean Markdown document plus a media folder, Pandoc is the right tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pandoc article.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from&lt;/span&gt; markdown &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to&lt;/span&gt; epub3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--epub-title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; article.epub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let Pandoc write the zip, the nav, the OPF. Do not hand-roll EPUB unless you enjoy debugging mimetype-must-be-first-and-uncompressed for the tenth time.&lt;/p&gt;

&lt;p&gt;Output formats beyond EPUB are the same document, different writer: PDF, Markdown, or a second pass to MOBI for older Kindles. Newer Kindles take EPUB natively. Do not make MOBI the default in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The public API is that pipeline with a credit meter
&lt;/h2&gt;

&lt;p&gt;Same steps, HTTP in front. Bearer token, one URL (or several), a format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://e-ink.me/api/v1/convert/webpage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer eink_your_api_key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://e-ink.me", "format": "epub"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; article.epub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Webpage → ebook is 3 credits per URL. File conversion, translation, TTS, and mind maps are separate endpoints on the same key. Docs, OpenAPI, llms.txt, and an MCP manifest live at &lt;a href="https://e-ink.me/en/developers" rel="noopener noreferrer"&gt;e-ink.me/en/developers&lt;/a&gt;. If you want the same calls inside Cursor or Claude Code, there is a skill at &lt;a href="https://github.com/Selenium39/e-ink-skill" rel="noopener noreferrer"&gt;github.com/Selenium39/e-ink-skill&lt;/a&gt; — &lt;code&gt;npx e-ink-skill&lt;/code&gt; and &lt;code&gt;EINK_API_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS and the e-ink constraints
&lt;/h2&gt;

&lt;p&gt;Ship almost no CSS. E-readers ignore half of it and fight the other half. Relative font sizes, default margins, &lt;code&gt;break-inside: avoid&lt;/code&gt; on pre/code and figures. No &lt;code&gt;position: fixed&lt;/code&gt;. No dark-mode media queries that assume an emissive screen. Code blocks need wrapping or they clip on a 6-inch Kindle; that is a content problem, not a theme problem.&lt;/p&gt;

&lt;p&gt;If you are tempted to embed a webfont: don't, unless you have tested the file on a 2018 Kobo. The reader already has a font. Your job is structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we refuse to do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No session cookies. If the page needs a login, it is not a public article.&lt;/li&gt;
&lt;li&gt;No “print the whole SPA as screenshots.” That is a PDF of a website, not a book.&lt;/li&gt;
&lt;li&gt;No storing the source file as a convenience cache. Uploads live long enough to produce the output, then they are deleted. The in-browser readers do not upload at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not invent readable extract or Pandoc. I glued them so I could stop sending navbar novels to a Kobo. If you are building your own pipeline, steal the shape: &lt;strong&gt;render → extract → structure → package&lt;/strong&gt;. If you just want the file, paste a URL on the site. New accounts get free credits.&lt;/p&gt;

&lt;p&gt;Questions / broken extracts: &lt;a href="mailto:openminimax@gmail.com"&gt;openminimax@gmail.com&lt;/a&gt;. I am Selenium39.&lt;/p&gt;

</description>
      <category>epub</category>
      <category>pandoc</category>
      <category>ebook</category>
      <category>api</category>
    </item>
  </channel>
</rss>
