howtophil

LLM-Generated Epub Reader In FreeBASIC (DOS/Linux)

Sep 22nd, 2026 (edited)
24,653
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
FreeBasic 84.03 KB | Source Code | 0 0
  1. ' Generated by Deepseek chat bot 2026/09/21
  2. ' https://chat.deepseek.com/
  3. '
  4. ' PUBLIC DOMAIN BECAUSE GENERATED CODE CANNOT
  5. ' BE COPYRIGHTED OR COPYLEFTED
  6. '
  7. ' Since I was pestered to "really try" "AI" (LLM is not AI),
  8. ' I had it generate an epub reader written in FreeBASIC that
  9. ' works in Linux terminal and MS-DOS/FreeDOS.
  10. '
  11. ' I completed the task of having the LLM generate something
  12. ' sort of like what I wanted...
  13. '
  14. ' It was a painfully stupid process of the machine trying to
  15. ' use reserved words as variables and other such mistakes that
  16. ' even beginners would not make. This is because the LLM does
  17. ' not know anything, it merely generates based on statistics.
  18. '
  19. ' I have tried. I have found the process and output wanting.
  20. '
  21. ' Every LLM is trash that produces garbage.
  22. '
  23. ' However, since I hate to waste the resources, here's the source
  24. ' code for anyone who wants it. I have, sadly, already boiled oceans
  25. ' and burned forests for this experiment. People might as well have
  26. ' the product made from the ill-collected data mined by these LLM
  27. ' companies.
  28. '
  29. ' Modified 2026/09/22 So it saves the place you've read to in the
  30. ' book, in a bookmark file next to the epub.
  31. '
  32. ' Modified 2026/09/29 CP437 substitution for DOS: em/en dashes,
  33. ' curly quotes, and ellipsis map to ASCII; accented Latin letters
  34. ' map to native CP437 bytes. Linux output remains UTF-8.
  35. '
  36. ' Modified 2026/09/30 Added `goto_section` and `next_section`,
  37. ' both keyed to the table of contents. Added a persistent
  38. ' bookmark file. `show`, `goto_section`, `next_section`, and
  39. ' the interactive TOC screen all take or display TOC indexes.
  40. ' `toc` opens the TOC screen. Added `toc_print` and `meta_print`
  41. ' for non-interactive use. Removed the old `list` command.
  42. ' Image alt text is now emitted inline in brackets. When a TOC
  43. ' entry has an anchor, the anchor wins over any explicit skip
  44. ' argument. Bookmark stores TOC index, spine section, and skip.
  45. '
  46. ' ---------------------------------------------------------------------
  47. '
  48. ' "Bugfix" attempt by kimi.ai code generation
  49. '
  50. ' Modified 2026/10/01 Bug fixes (see the comments at each change):
  51. ' CP437 case labels for e-acute/e-grave were &h00A9/&h00A8 instead of
  52. ' &h00E9/&h00E8. Empty stored DEFLATE blocks underflowed the copy loop.
  53. ' hlit/hdist were not capped at 286/30 (one-byte stack overwrite).
  54. ' Malformed numeric character references could reach chr() with an
  55. ' illegal value or emit an overlong UTF-8 NUL. The last buffered line
  56. ' kept its newline and printed a spurious blank row. Whitespace-only
  57. ' source lines vanished, merging paragraphs. CR bytes survived into
  58. ' the output. Unterminated script/style blocks rendered as text.
  59. ' Percent-encoded hrefs were not decoded, and TOC hrefs were resolved
  60. ' against the OPF directory instead of the NCX / nav file's directory.
  61. ' Anchor search could false-match data-id attributes. A named-entity
  62. ' table now covers Latin-1 letters and common symbols. Fixed a
  63. ' next_section/bookmark mixup that could store a TOC index in the
  64. ' bookmark's spine-section field. Removed dead skip/total variables.
  65. '
  66. ' =====================================================================
  67. ' epubrd.bas - Minimal EPUB reader in FreeBASIC
  68. ' =====================================================================
  69. ' Self-contained DEFLATE (RFC 1951). No external libraries.
  70. ' Portable across 32-bit MS-DOS and 64-bit Linux/host.
  71. '
  72. '   Build (Linux/host):  fbc epubrd.bas
  73. '   Build Cross Compile (MS-DOS):      fbc -target dos epubrd.bas
  74. '   Build (MS-DOS): fbc epubrd.bas
  75. '
  76. ' Commands:
  77. '   epubrd <file.epub>                     resume from bookmark
  78. '   epubrd <file.epub> meta                title and author, paged
  79. '   epubrd <file.epub> meta_print          title and author, plain
  80. '   epubrd <file.epub> toc                 interactive TOC screen
  81. '   epubrd <file.epub> toc_print           TOC listing, plain
  82. '   epubrd <file.epub> show [n] [skip]     read TOC entry n
  83. '   epubrd <file.epub> goto_section <n>    same as `show <n>`
  84. '   epubrd <file.epub> next_section <n> [skip]
  85. '
  86. ' Reader keys:
  87. '   [space] or [Enter]  next page
  88. '   [b] or [B]          previous page
  89. '   [t] or [T]          table of contents screen
  90. '   [q] or [Q]          quit
  91. '   [ctrl-C]            quit
  92. '   any other key       ignored
  93. '   On quit, prints the command to resume at the top line on
  94. '   screen and writes the bookmark file.
  95. '
  96. ' TOC screen keys:
  97. '   digits then [Enter] jump to that entry
  98. '   [space], [Enter]    next page of TOC (empty buffer)
  99. '   [b], [B]            previous page of TOC
  100. '   [q], [Q], [Esc]     return to the reader at the same page
  101. '   [ctrl-C]            quit the program
  102. '   any other key       ignored
  103. '
  104. ' Resume and skip:
  105. '   `show <n> [skip]`: read TOC entry n as text. If the entry has
  106. '   an anchor (`file.xhtml#frag`), jumps to it and ignores `skip`.
  107. '   If the entry has no anchor, starts at line 0 or at `skip`.
  108. '   `show` with no argument reads the bookmark, if any.
  109. '
  110. ' next_section:
  111. '   Read TOC entry n, then continue into n+1, n+2, and so on
  112. '   until the end of the TOC or until you quit. `skip` applies
  113. '   only to the first entry, and only if that entry has no
  114. '   anchor. On quit, the resume hint names the TOC entry you
  115. '   were in.
  116. '
  117. ' Bookmark:
  118. '   On quit, writes `<basename>.bmk` next to the epub:
  119. '     line 1  TOC index of the reading position
  120. '     line 2  spine section index
  121. '     line 3  line skip within the section
  122. '   `epubrd <file>` with no command, or `show` with no
  123. '   arguments, resumes from the bookmark in spine terms,
  124. '   bypassing TOC resolution. Delete the .bmk to start over.
  125. '   For `book.epub` the bookmark is `book.bmk`.
  126. '
  127. ' Text extraction:
  128. '   Tags are stripped, entities decoded, and block-level tags
  129. '   (`<p>`, `<div>`, `<br>`, headings, list items, table rows,
  130. '   block quotes, preformatted blocks, and sectioning tags)
  131. '   produce newlines. `<script>` and `<style>` blocks are
  132. '   dropped entirely. `<img alt="...">` contributes the alt
  133. '   text inline, in brackets. No CSS, no layout, no emphasis
  134. '   markers.
  135. '
  136. ' Environment variables:
  137. '   EPUBREAD_MAX    max epub size in bytes (default 16777216)
  138. '   EPUBREAD_COLS   wrap width, 20..78 (default 78)
  139. '   EPUBREAD_ROWS   terminal rows (default 25); lines/page = rows-4
  140. '   EPUBREAD_LINES  content lines per page, overrides rows-4
  141. '
  142. ' All file-mapped integers use `ulong` (fixed 32 bits on every
  143. ' target), not `uinteger` (pointer-sized, 64 bits on x86_64).
  144. ' =====================================================================
  145.  
  146. ' ---------------------------------------------------------------------
  147. ' Constants
  148. ' ---------------------------------------------------------------------
  149.  
  150. const NULL_PTR          as ubyte ptr = 0
  151. const LOCAL_SIG         as ulong = &h04034b50
  152. const CDIR_SIG          as ulong = &h02014b50
  153. const EOCD_SIG          as ulong = &h06054b50
  154. const MAX_EOCD_SEARCH   as ulong = 65557
  155. const DEFAULT_MAX_EPUB  as ulong = 16777216
  156. const DEFAULT_WRAP      as integer = 78
  157. const DEFAULT_ROWS      as integer = 25
  158. const DEFAULT_PAGE_LINES as integer = 21
  159. const MAX_MANIFEST      as integer = 1023
  160. const MAX_TOC           as integer = 255
  161.  
  162. const KEY_CTRL_C  as integer = 3
  163. const KEY_ESC     as integer = 27
  164. const KEY_ENTER   as integer = 13
  165. const KEY_SPACE   as integer = 32
  166. const KEY_B_LOWER as integer = 98
  167. const KEY_B_UPPER as integer = 66
  168. const KEY_Q_LOWER as integer = 113
  169. const KEY_Q_UPPER as integer = 81
  170. const KEY_T_LOWER as integer = 116
  171. const KEY_T_UPPER as integer = 84
  172.  
  173. ' =====================================================================
  174. ' Section 1: ZIP container structures (on-disk layout)
  175. ' =====================================================================
  176. ' These types map directly onto bytes in the .zip file. `field = 1`
  177. ' forces byte packing so sizeof() matches the on-disk record size,
  178. ' and every multi-byte field uses a fixed-width type (ulong, ushort)
  179. ' so the layout is identical on 32-bit and 64-bit builds.
  180.  
  181. type ZipLocalHeader field = 1
  182.     signature   as ulong
  183.     version     as ushort
  184.     flags       as ushort
  185.     method      as ushort
  186.     modtime     as ushort
  187.     moddate     as ushort
  188.     crc32       as ulong
  189.     compsize    as ulong
  190.     uncompsize  as ulong
  191.     namelen     as ushort
  192.     extralen    as ushort
  193. end type
  194.  
  195. type ZipCentralHeader field = 1
  196.     signature   as ulong
  197.     versionmade as ushort
  198.     versionneed as ushort
  199.     flags       as ushort
  200.     method      as ushort
  201.     modtime     as ushort
  202.     moddate     as ushort
  203.     crc32       as ulong
  204.     compsize    as ulong
  205.     uncompsize  as ulong
  206.     namelen     as ushort
  207.     extralen    as ushort
  208.     commentlen  as ushort
  209.     diskstart   as ushort
  210.     intattr     as ushort
  211.     extattr     as ulong
  212.     offset      as ulong
  213. end type
  214.  
  215. type ZipEndRecord field = 1
  216.     signature   as ulong
  217.     disknum     as ushort
  218.     cddisk      as ushort
  219.     diskentries as ushort
  220.     totalentries as ushort
  221.     cdsize      as ulong
  222.     cdoffset    as ulong
  223.     commentlen  as ushort
  224. end type
  225.  
  226. ' =====================================================================
  227. ' Section 2: Bit reader for DEFLATE streams
  228. ' =====================================================================
  229.  
  230. type BitReader
  231.     rawbuf as ubyte ptr
  232.     rawlen as ulong
  233.     pos    as ulong
  234.     bitbuf as ulong
  235.     bitcnt as integer
  236. end type
  237.  
  238. sub br_init(br as BitReader ptr, buf as ubyte ptr, sz as ulong)
  239.     br->rawbuf = buf
  240.     br->rawlen = sz
  241.     br->pos    = 0
  242.     br->bitbuf = 0
  243.     br->bitcnt = 0
  244. end sub
  245.  
  246. ' Pull n bits, LSB first. Zero-fills past end of buffer.
  247. function br_bits(br as BitReader ptr, n as integer) as ulong
  248.     while br->bitcnt < n
  249.         dim as ulong b = 0
  250.         if br->pos < br->rawlen then
  251.             b = br->rawbuf[br->pos]
  252.             br->pos += 1
  253.         end if
  254.         br->bitbuf or= b shl br->bitcnt
  255.         br->bitcnt += 8
  256.     wend
  257.     dim as ulong v = br->bitbuf and ((1u shl n) - 1)
  258.     br->bitbuf shr= n
  259.     br->bitcnt -= n
  260.     return v
  261. end function
  262.  
  263. sub br_align(br as BitReader ptr)
  264.     dim as integer drop = br->bitcnt and 7
  265.     br->bitbuf shr= drop
  266.     br->bitcnt -= drop
  267. end sub
  268.  
  269. ' =====================================================================
  270. ' Section 3: Huffman decoding
  271. ' =====================================================================
  272.  
  273. type HuffTable
  274.     counts(0 to 15) as integer
  275.     symbols(0 to 287) as integer
  276.     nsym as integer
  277. end type
  278.  
  279. sub huff_build(h as HuffTable ptr, lengths as ubyte ptr, n as integer)
  280.     dim as integer i
  281.     for i = 0 to 15
  282.         h->counts(i) = 0
  283.     next
  284.     for i = 0 to n - 1
  285.         h->counts(lengths[i]) += 1
  286.     next
  287.     h->counts(0) = 0
  288.  
  289.     dim as integer offs(0 to 15)
  290.     offs(0) = 0
  291.     for i = 1 to 15
  292.         offs(i) = offs(i-1) + h->counts(i-1)
  293.     next
  294.     for i = 0 to n - 1
  295.         if lengths[i] <> 0 then
  296.             h->symbols(offs(lengths[i])) = i
  297.             offs(lengths[i]) += 1
  298.         end if
  299.     next
  300.     h->nsym = n
  301. end sub
  302.  
  303. ' Canonical Huffman decode. Returns the symbol or -1 on error.
  304. function huff_decode(br as BitReader ptr, h as HuffTable ptr) as integer
  305.     dim as integer code = 0, first = 0, index = 0
  306.     for ln as integer = 1 to 15
  307.         code or= cast(integer, br_bits(br, 1))
  308.         dim as integer count = h->counts(ln)
  309.         if code - first < count then
  310.             return h->symbols(index + code - first)
  311.         end if
  312.         index += count
  313.         first = (first + count) shl 1
  314.         code shl= 1
  315.     next
  316.     return -1
  317. end function
  318.  
  319. ' =====================================================================
  320. ' Section 4: DEFLATE (RFC 1951)
  321. ' =====================================================================
  322.  
  323. dim shared as integer LENGTH_BASE(0 to 28) = { _
  324.     3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, _
  325.     35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 }
  326.  
  327. dim shared as integer LENGTH_EXTRA(0 to 28) = { _
  328.     0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, _
  329.     3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }
  330.  
  331. dim shared as integer DIST_BASE(0 to 29) = { _
  332.     1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, _
  333.     257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 }
  334.  
  335. dim shared as integer DIST_EXTRA(0 to 29) = { _
  336.     0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, _
  337.     7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }
  338.  
  339. ' Inflate a raw DEFLATE stream (no zlib header).
  340. ' Returns the number of uncompressed bytes written, or -1 on error.
  341. function inflate_raw(src as ubyte ptr, srcsize as ulong, dst as ubyte ptr, dstsize as ulong) as integer
  342.     dim as BitReader br
  343.     br_init(@br, src, srcsize)
  344.     dim as ulong outpos = 0
  345.  
  346.     do
  347.         dim as ulong bfinal = br_bits(@br, 1)
  348.         dim as ulong btype  = br_bits(@br, 2)
  349.  
  350.         select case btype
  351.         case 0
  352.             ' Stored block
  353.             br_align(@br)
  354.             dim as ulong ln   = br_bits(@br, 16)
  355.             dim as ulong nln  = br_bits(@br, 16)
  356.             if (ln xor &hFFFF) <> nln then return -1
  357.             ' Count in a signed type: with ln = 0 (a legal empty stored
  358.             ' block) `for i as ulong = 0 to ln - 1` wraps the end bound
  359.             ' to ~4 billion and the loop never ends normally.
  360.             dim as long stored_n = cast(long, ln)
  361.             for i as long = 0 to stored_n - 1
  362.                 if outpos >= dstsize then return -1
  363.                 dst[outpos] = cast(ubyte, br_bits(@br, 8))
  364.                 outpos += 1
  365.             next
  366.  
  367.         case 1
  368.             ' Fixed Huffman
  369.             dim as ubyte litlen(0 to 287)
  370.             for i as integer = 0 to 143: litlen(i) = 8: next
  371.             for i as integer = 144 to 255: litlen(i) = 9: next
  372.             for i as integer = 256 to 279: litlen(i) = 7: next
  373.             for i as integer = 280 to 287: litlen(i) = 8: next
  374.  
  375.             dim as ubyte distlen(0 to 31)
  376.             for i as integer = 0 to 31: distlen(i) = 5: next
  377.  
  378.             dim as HuffTable lit_h, dist_h
  379.             huff_build(@lit_h,  @litlen(0),  288)
  380.             huff_build(@dist_h, @distlen(0), 32)
  381.  
  382.             do
  383.                 dim as integer sym = huff_decode(@br, @lit_h)
  384.                 if sym < 0 then return -1
  385.                 if sym < 256 then
  386.                     if outpos >= dstsize then return -1
  387.                     dst[outpos] = cast(ubyte, sym)
  388.                     outpos += 1
  389.                 elseif sym = 256 then
  390.                     exit do
  391.                 else
  392.                     dim as integer li = sym - 257
  393.                     if li < 0 or li > 28 then return -1
  394.                     dim as uinteger length = LENGTH_BASE(li) + br_bits(@br, LENGTH_EXTRA(li))
  395.  
  396.                     dim as integer dsym = huff_decode(@br, @dist_h)
  397.                     if dsym < 0 or dsym > 29 then return -1
  398.                     dim as uinteger dist = DIST_BASE(dsym) + br_bits(@br, DIST_EXTRA(dsym))
  399.                     if dist > outpos then return -1
  400.  
  401.                     for j as ulong = 0 to length - 1
  402.                         if outpos >= dstsize then return -1
  403.                         dst[outpos] = dst[outpos - dist]
  404.                         outpos += 1
  405.                     next
  406.                 end if
  407.             loop
  408.  
  409.         case 2
  410.             ' Dynamic Huffman
  411.             dim as ulong hlit  = br_bits(@br, 5) + 257
  412.             dim as ulong hdist = br_bits(@br, 5) + 1
  413.             dim as ulong hclen = br_bits(@br, 4) + 4
  414.  
  415.             ' RFC 1951 caps these at 286 and 30. Without the check a
  416.             ' crafted stream (hlit = 288, hdist = 32) makes hlit + hdist
  417.             ' exceed the lengths() array by one byte.
  418.             if hlit > 286 orelse hdist > 30 then return -1
  419.  
  420.             dim as ubyte order(0 to 18) = { _
  421.                 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }
  422.  
  423.             dim as ubyte cl_lengths(0 to 18)
  424.             for i as integer = 0 to 18: cl_lengths(i) = 0: next
  425.             for i as ulong = 0 to hclen - 1
  426.                 cl_lengths(order(i)) = cast(ubyte, br_bits(@br, 3))
  427.             next
  428.  
  429.             dim as HuffTable cl_h
  430.             huff_build(@cl_h, @cl_lengths(0), 19)
  431.  
  432.             dim as ubyte lengths(0 to 318)
  433.             dim as ulong total = hlit + hdist
  434.             dim as ulong n = 0
  435.             while n < total
  436.                 dim as integer sym = huff_decode(@br, @cl_h)
  437.                 if sym < 0 then return -1
  438.                 if sym < 16 then
  439.                     lengths(n) = cast(ubyte, sym)
  440.                     n += 1
  441.                 elseif sym = 16 then
  442.                     if n = 0 then return -1
  443.                     dim as ubyte prev = lengths(n-1)
  444.                     dim as ulong rep = 3 + br_bits(@br, 2)
  445.                     while rep > 0 andalso n < total
  446.                         lengths(n) = prev
  447.                         n += 1
  448.                         rep -= 1
  449.                     wend
  450.                 elseif sym = 17 then
  451.                     dim as ulong rep = 3 + br_bits(@br, 3)
  452.                     while rep > 0 andalso n < total
  453.                         lengths(n) = 0
  454.                         n += 1
  455.                         rep -= 1
  456.                     wend
  457.                 elseif sym = 18 then
  458.                     dim as ulong rep = 11 + br_bits(@br, 7)
  459.                     while rep > 0 andalso n < total
  460.                         lengths(n) = 0
  461.                         n += 1
  462.                         rep -= 1
  463.                     wend
  464.                 else
  465.                     return -1
  466.                 end if
  467.             wend
  468.  
  469.             dim as HuffTable lit_h, dist_h
  470.             huff_build(@lit_h,  @lengths(0),    hlit)
  471.             huff_build(@dist_h, @lengths(hlit), hdist)
  472.  
  473.             do
  474.                 dim as integer sym = huff_decode(@br, @lit_h)
  475.                 if sym < 0 then return -1
  476.                 if sym < 256 then
  477.                     if outpos >= dstsize then return -1
  478.                     dst[outpos] = cast(ubyte, sym)
  479.                     outpos += 1
  480.                 elseif sym = 256 then
  481.                     exit do
  482.                 else
  483.                     dim as integer li = sym - 257
  484.                     if li < 0 or li > 28 then return -1
  485.                     dim as uinteger length = LENGTH_BASE(li) + br_bits(@br, LENGTH_EXTRA(li))
  486.  
  487.                     dim as integer dsym = huff_decode(@br, @dist_h)
  488.                     if dsym < 0 or dsym > 29 then return -1
  489.                     dim as uinteger dist = DIST_BASE(dsym) + br_bits(@br, DIST_EXTRA(dsym))
  490.                     if dist > outpos then return -1
  491.  
  492.                     for j as ulong = 0 to length - 1
  493.                         if outpos >= dstsize then return -1
  494.                         dst[outpos] = dst[outpos - dist]
  495.                         outpos += 1
  496.                     next
  497.                 end if
  498.             loop
  499.  
  500.         case else
  501.             return -1
  502.         end select
  503.  
  504.         if bfinal <> 0 then exit do
  505.     loop
  506.  
  507.     return cast(integer, outpos)
  508. end function
  509.  
  510. ' =====================================================================
  511. ' Section 5: In-memory ZIP archive
  512. ' =====================================================================
  513.  
  514. type ZipEntry
  515.     zname       as string
  516.     zmethod     as ushort
  517.     zcompsize   as ulong
  518.     zuncompsize as ulong
  519.     zoffset     as ulong
  520. end type
  521.  
  522. type ZipArchive
  523.     rawbuf   as ubyte ptr
  524.     rawlen   as ulong
  525.     entries(any) as ZipEntry
  526.     nentries as integer
  527. end type
  528.  
  529. function dos_max_bytes() as ulong
  530.     dim as string s = environ("EPUBREAD_MAX")
  531.     if s = "" then return DEFAULT_MAX_EPUB
  532.     dim as ulong v = val(s)
  533.     if v < 65536 then v = 65536
  534.     return v
  535. end function
  536.  
  537. ' Forward declaration: zip_open's error paths release the archive.
  538. declare sub zip_close(byref za as ZipArchive)
  539.  
  540. ' Read the whole file into memory and parse the central directory.
  541. ' Returns 0 on success, -1 on any failure.
  542. function zip_open(byref za as ZipArchive, filename as string) as integer
  543.     dim as integer f = freefile
  544.     if open(filename for binary as #f) <> 0 then
  545.         print "zip_open: open failed for [" & filename & "]"
  546.         return -1
  547.     end if
  548.  
  549.     za.rawlen = lof(f)
  550.     if za.rawlen = 0 then
  551.         close #f
  552.         print "zip_open: zero-length file"
  553.         return -1
  554.     end if
  555.     if za.rawlen > dos_max_bytes() then
  556.         close #f
  557.         print "zip_open: file too large: " & str(za.rawlen) & " bytes"
  558.         return -1
  559.     end if
  560.  
  561.     za.rawbuf = callocate(za.rawlen + 1)
  562.     if za.rawbuf = NULL_PTR then
  563.         close #f
  564.         print "zip_open: callocate failed for " & str(za.rawlen) & " bytes"
  565.         return -1
  566.     end if
  567.  
  568.     if za.rawlen > 0 then
  569.         get #f, , *za.rawbuf, za.rawlen
  570.         ' LOC is the byte position of the last byte read, so it equals
  571.         ' the number of bytes actually fetched. A value short of the
  572.         ' file size means the archive buffer is incomplete.
  573.         dim as ulong got_bytes = loc(f)
  574.         close #f
  575.         if got_bytes <> za.rawlen then
  576.             print "zip_open: short read (" & str(got_bytes) & " of " & str(za.rawlen) & " bytes)"
  577.             zip_close(za)
  578.             return -1
  579.         end if
  580.     else
  581.         close #f
  582.     end if
  583.  
  584.     if za.rawlen < sizeof(ZipEndRecord) then
  585.         print "zip_open: smaller than end record"
  586.         zip_close(za)
  587.         return -1
  588.     end if
  589.  
  590.     ' Locate the end-of-central-directory record.
  591.     dim as ulong searchstart = 0
  592.     if za.rawlen > MAX_EOCD_SEARCH then searchstart = za.rawlen - MAX_EOCD_SEARCH
  593.  
  594.     dim as integer epos = -1
  595.     for i as integer = cast(integer, za.rawlen) - 22 to cast(integer, searchstart) step -1
  596.         if za.rawbuf[i]   = &h50 andalso _
  597.            za.rawbuf[i+1] = &h4b andalso _
  598.            za.rawbuf[i+2] = &h05 andalso _
  599.            za.rawbuf[i+3] = &h06 then
  600.             epos = i
  601.             exit for
  602.         end if
  603.     next
  604.     if epos < 0 then
  605.         print "zip_open: no end-of-central-directory signature found"
  606.         zip_close(za)
  607.         return -1
  608.     end if
  609.  
  610.     dim as ZipEndRecord ptr eocd = cast(ZipEndRecord ptr, za.rawbuf + epos)
  611.     za.nentries = eocd->totalentries
  612.     redim za.entries(0 to za.nentries - 1)
  613.  
  614.     ' Walk the central directory.
  615.     dim as ulong cdpos = eocd->cdoffset
  616.     for i as integer = 0 to za.nentries - 1
  617.         if cdpos + sizeof(ZipCentralHeader) > za.rawlen then
  618.             print "zip_open: cdpos past end at entry " & str(i)
  619.             zip_close(za)
  620.             return -1
  621.         end if
  622.  
  623.         dim as ZipCentralHeader ptr cdhdr = cast(ZipCentralHeader ptr, za.rawbuf + cdpos)
  624.         if cdhdr->signature <> CDIR_SIG then
  625.             print "zip_open: bad central header sig at entry " & str(i) & _
  626.                   " = " & hex(cdhdr->signature)
  627.             zip_close(za)
  628.             return -1
  629.         end if
  630.  
  631.         dim as string nm = ""
  632.         for j as ulong = 0 to cdhdr->namelen - 1
  633.             nm &= chr(za.rawbuf[cdpos + sizeof(ZipCentralHeader) + j])
  634.         next
  635.  
  636.         za.entries(i).zname       = nm
  637.         za.entries(i).zmethod     = cdhdr->method
  638.         za.entries(i).zcompsize   = cdhdr->compsize
  639.         za.entries(i).zuncompsize = cdhdr->uncompsize
  640.         za.entries(i).zoffset     = cdhdr->offset
  641.  
  642.         cdpos += sizeof(ZipCentralHeader) + cdhdr->namelen + cdhdr->extralen + cdhdr->commentlen
  643.     next
  644.  
  645.     return 0
  646. end function
  647.  
  648. sub zip_close(byref za as ZipArchive)
  649.     if za.rawbuf <> NULL_PTR then deallocate(za.rawbuf)
  650.     za.rawbuf = NULL_PTR
  651.     erase za.entries
  652. end sub
  653.  
  654. ' Return the index of the entry with this exact name, or -1.
  655. function zip_find(byref za as ZipArchive, nm as string) as integer
  656.     for i as integer = 0 to za.nentries - 1
  657.         if za.entries(i).zname = nm then return i
  658.     next
  659.     return -1
  660. end function
  661.  
  662. ' Extract a named entry straight into a string's backing buffer.
  663. ' No intermediate uncompressed allocation, no copy.
  664. function zip_extract_string(byref za as ZipArchive, nm as string) as string
  665.     dim as integer idx = zip_find(za, nm)
  666.     if idx < 0 then
  667.         print "zip_extract_string: not found: " & nm
  668.         return ""
  669.     end if
  670.  
  671.     dim as ZipEntry ptr e = @za.entries(idx)
  672.  
  673.     if e->zoffset + sizeof(ZipLocalHeader) > za.rawlen then return ""
  674.     dim as ZipLocalHeader ptr localhdr = cast(ZipLocalHeader ptr, za.rawbuf + e->zoffset)
  675.     if localhdr->signature <> LOCAL_SIG then return ""
  676.  
  677.     dim as ulong dataoff = e->zoffset + sizeof(ZipLocalHeader) + localhdr->namelen + localhdr->extralen
  678.     dim as ulong csize   = e->zcompsize
  679.     if dataoff + csize > za.rawlen then
  680.         csize = localhdr->compsize
  681.         if dataoff + csize > za.rawlen then return ""
  682.     end if
  683.  
  684.     dim as ulong usize = e->zuncompsize
  685.     if usize = 0 then usize = localhdr->uncompsize
  686.     if usize = 0 then usize = 1
  687.  
  688.     dim as string s = space(usize)
  689.     dim as ubyte ptr out_ptr = cast(ubyte ptr, strptr(s))
  690.  
  691.     select case e->zmethod
  692.     case 0
  693.         dim as ulong n = iif(csize < usize, csize, usize)
  694.         for i as ulong = 0 to n - 1
  695.             out_ptr[i] = za.rawbuf[dataoff + i]
  696.         next
  697.     case 8
  698.         dim as integer got = inflate_raw(za.rawbuf + dataoff, csize, out_ptr, usize)
  699.         if got <> cast(integer, usize) then return ""
  700.     case else
  701.         return ""
  702.     end select
  703.  
  704.     return s
  705. end function
  706.  
  707. ' =====================================================================
  708. ' Section 6: String, XML, and CP437 helpers
  709. ' =====================================================================
  710.  
  711. ' Map a Unicode code point to the byte sequence that should be emitted
  712. ' to the current output device. On DOS, that is a sequence of CP437
  713. ' bytes. On Linux/host, it is the UTF-8 encoding.
  714. function unicode_to_output(code as integer) as string
  715.     #ifdef __FB_DOS__
  716.         ' CP437 has no em dash, en dash, curly quotes, or ellipsis.
  717.         ' Substitute the ASCII equivalents, which every DOS display
  718.         ' shows correctly regardless of the active code page.
  719.         select case code
  720.         case &h00A0 : return chr(32)          ' nbsp -> space
  721.         case &h2014 : return "-"              ' em dash
  722.         case &h2013 : return "-"              ' en dash
  723.         case &h2018 : return "'"              ' left single quote
  724.         case &h2019 : return "'"              ' right single quote
  725.         case &h201C : return """"             ' left double quote
  726.         case &h201D : return """"             ' right double quote
  727.         case &h2026 : return "..."            ' ellipsis
  728.         case &h2022 : return "*"              ' bullet
  729.         case &h2122 : return "(TM)"           ' trademark
  730.  
  731.         ' Accented Latin letters that CP437 does have.
  732.         case &h00E9 : return chr(&h82)        ' é
  733.         case &h00E8 : return chr(&h8A)        ' è
  734.         case &h00E0 : return chr(&h85)        ' à
  735.         case &h00E2 : return chr(&h83)        ' â
  736.         case &h00E7 : return chr(&h87)        ' ç
  737.         case &h00EA : return chr(&h88)        ' ê
  738.         case &h00EB : return chr(&h89)        ' ë
  739.         case &h00EE : return chr(&h8B)        ' î
  740.         case &h00EF : return chr(&h8C)        ' ï
  741.         case &h00F4 : return chr(&h93)        ' ô
  742.         case &h00F6 : return chr(&h94)        ' ö
  743.         case &h00FB : return chr(&h96)        ' û
  744.         case &h00FC : return chr(&h81)        ' ü
  745.         case &h00E1 : return chr(&hA0)        ' á
  746.         case &h00ED : return chr(&hA1)        ' í
  747.         case &h00F3 : return chr(&hA2)        ' ó
  748.         case &h00FA : return chr(&hA3)        ' ú
  749.         case &h00F1 : return chr(&hA4)        ' ñ
  750.         case &h00D1 : return chr(&hA5)        ' Ñ
  751.         case else
  752.             if code > 0 andalso code < 128 then
  753.                 return chr(code)
  754.             end if
  755.             return "?"
  756.         end select
  757.     #else
  758.         if code < 1 then
  759.             return "?"
  760.         elseif code < 128 then
  761.             return chr(code)
  762.         elseif code < 2048 then
  763.             return chr(&hC0 or (code shr 6)) & _
  764.                    chr(&h80 or (code and &h3F))
  765.         elseif code < 65536 then
  766.             return chr(&hE0 or (code shr 12)) & _
  767.                    chr(&h80 or ((code shr 6) and &h3F)) & _
  768.                    chr(&h80 or (code and &h3F))
  769.         else
  770.             return "?"
  771.         end if
  772.     #endif
  773. end function
  774.  
  775. function str_lower(s as string) as string
  776.     dim as string r = s
  777.     for i as integer = 1 to len(r)
  778.         dim as integer c = asc(r, i)
  779.         if c >= asc("A") andalso c <= asc("Z") then
  780.             mid(r, i, 1) = chr(c + 32)
  781.         end if
  782.     next
  783.     return r
  784. end function
  785.  
  786. function str_trim(s as string) as string
  787.     dim as integer a = 1, b = len(s)
  788.     while a <= b andalso (asc(s, a) = 32 or asc(s, a) = 9 or asc(s, a) = 10 or asc(s, a) = 13)
  789.         a += 1
  790.     wend
  791.     while b >= a andalso (asc(s, b) = 32 or asc(s, b) = 9 or asc(s, b) = 10 or asc(s, b) = 13)
  792.         b -= 1
  793.     wend
  794.     if b < a then return ""
  795.     return mid(s, a, b - a + 1)
  796. end function
  797.  
  798. ' Map a named character reference (the text between "&" and ";") to a
  799. ' Unicode code point, or -1 when it is not in the built-in table.
  800. ' Covers the Latin-1 letters, punctuation, and the common HTML 4
  801. ' symbols most often found in epubs, so that &eacute; and friends do
  802. ' not pass through literally. Decoding goes through
  803. ' unicode_to_output(), so the DOS build still gets CP437 bytes.
  804. function entity_code(entname as string) as integer
  805.     select case entname
  806.     case "nbsp"    : return &h00A0
  807.     case "iexcl"   : return &h00A1
  808.     case "cent"    : return &h00A2
  809.     case "pound"   : return &h00A3
  810.     case "curren"  : return &h00A4
  811.     case "yen"     : return &h00A5
  812.     case "brvbar"  : return &h00A6
  813.     case "sect"    : return &h00A7
  814.     case "uml"     : return &h00A8
  815.     case "copy"    : return &h00A9
  816.     case "ordf"    : return &h00AA
  817.     case "laquo"   : return &h00AB
  818.     case "not"     : return &h00AC
  819.     case "shy"     : return &h00AD
  820.     case "reg"     : return &h00AE
  821.     case "macr"    : return &h00AF
  822.     case "deg"     : return &h00B0
  823.     case "plusmn"  : return &h00B1
  824.     case "sup2"    : return &h00B2
  825.     case "sup3"    : return &h00B3
  826.     case "acute"   : return &h00B4
  827.     case "micro"   : return &h00B5
  828.     case "para"    : return &h00B6
  829.     case "middot"  : return &h00B7
  830.     case "cedil"   : return &h00B8
  831.     case "sup1"    : return &h00B9
  832.     case "ordm"    : return &h00BA
  833.     case "raquo"   : return &h00BB
  834.     case "frac14"  : return &h00BC
  835.     case "frac12"  : return &h00BD
  836.     case "frac34"  : return &h00BE
  837.     case "iquest"  : return &h00BF
  838.     case "Agrave"  : return &h00C0
  839.     case "Aacute"  : return &h00C1
  840.     case "Acirc"   : return &h00C2
  841.     case "Atilde"  : return &h00C3
  842.     case "Auml"    : return &h00C4
  843.     case "Aring"   : return &h00C5
  844.     case "AElig"   : return &h00C6
  845.     case "Ccedil"  : return &h00C7
  846.     case "Egrave"  : return &h00C8
  847.     case "Eacute"  : return &h00C9
  848.     case "Ecirc"   : return &h00CA
  849.     case "Euml"    : return &h00CB
  850.     case "Igrave"  : return &h00CC
  851.     case "Iacute"  : return &h00CD
  852.     case "Icirc"   : return &h00CE
  853.     case "Iuml"    : return &h00CF
  854.     case "ETH"     : return &h00D0
  855.     case "Ntilde"  : return &h00D1
  856.     case "Ograve"  : return &h00D2
  857.     case "Oacute"  : return &h00D3
  858.     case "Ocirc"   : return &h00D4
  859.     case "Otilde"  : return &h00D5
  860.     case "Ouml"    : return &h00D6
  861.     case "times"   : return &h00D7
  862.     case "Oslash"  : return &h00D8
  863.     case "Ugrave"  : return &h00D9
  864.     case "Uacute"  : return &h00DA
  865.     case "Ucirc"   : return &h00DB
  866.     case "Uuml"    : return &h00DC
  867.     case "Yacute"  : return &h00DD
  868.     case "THORN"   : return &h00DE
  869.     case "szlig"   : return &h00DF
  870.     case "agrave"  : return &h00E0
  871.     case "aacute"  : return &h00E1
  872.     case "acirc"   : return &h00E2
  873.     case "atilde"  : return &h00E3
  874.     case "auml"    : return &h00E4
  875.     case "aring"   : return &h00E5
  876.     case "aelig"   : return &h00E6
  877.     case "ccedil"  : return &h00E7
  878.     case "egrave"  : return &h00E8
  879.     case "eacute"  : return &h00E9
  880.     case "ecirc"   : return &h00EA
  881.     case "euml"    : return &h00EB
  882.     case "igrave"  : return &h00EC
  883.     case "iacute"  : return &h00ED
  884.     case "icirc"   : return &h00EE
  885.     case "iuml"    : return &h00EF
  886.     case "eth"     : return &h00F0
  887.     case "ntilde"  : return &h00F1
  888.     case "ograve"  : return &h00F2
  889.     case "oacute"  : return &h00F3
  890.     case "ocirc"   : return &h00F4
  891.     case "otilde"  : return &h00F5
  892.     case "ouml"    : return &h00F6
  893.     case "divide"  : return &h00F7
  894.     case "oslash"  : return &h00F8
  895.     case "ugrave"  : return &h00F9
  896.     case "uacute"  : return &h00FA
  897.     case "ucirc"   : return &h00FB
  898.     case "uuml"    : return &h00FC
  899.     case "yacute"  : return &h00FD
  900.     case "thorn"   : return &h00FE
  901.     case "yuml"    : return &h00FF
  902.     case "OElig"   : return &h0152
  903.     case "oelig"   : return &h0153
  904.     case "Scaron"  : return &h0160
  905.     case "scaron"  : return &h0161
  906.     case "Yuml"    : return &h0178
  907.     case "fnof"    : return &h0192
  908.     case "circ"    : return &h02C6
  909.     case "tilde"   : return &h02DC
  910.     case "ensp"    : return &h2002
  911.     case "emsp"    : return &h2003
  912.     case "thinsp"  : return &h2009
  913.     case "dagger"  : return &h2020
  914.     case "Dagger"  : return &h2021
  915.     case "permil"  : return &h2030
  916.     case "lsaquo"  : return &h2039
  917.     case "rsaquo"  : return &h203A
  918.     case "euro"    : return &h20AC
  919.     case else      : return -1
  920.     end select
  921. end function
  922.  
  923. ' Single-pass entity decoder. UTF-8 input is preserved as-is on Linux;
  924. ' on DOS, cp437_fix() converts it after the fact.
  925. function xml_unescape(s as string) as string
  926.     dim as string o = ""
  927.     dim as integer i = 1
  928.     dim as integer n = len(s)
  929.  
  930.     while i <= n
  931.         dim as integer c = asc(s, i)
  932.         if c = asc("&") then
  933.             dim as integer j = instr(i, s, ";")
  934.             if j > 0 andalso j - i <= 12 then
  935.                 dim as string entity = mid(s, i + 1, j - i - 1)
  936.  
  937.                 select case entity
  938.                 case "lt"
  939.                     o &= "<" : i = j + 1 : continue while
  940.                 case "gt"
  941.                     o &= ">" : i = j + 1 : continue while
  942.                 case "quot"
  943.                     o &= """" : i = j + 1 : continue while
  944.                 case "apos"
  945.                     o &= "'" : i = j + 1 : continue while
  946.                 case "amp"
  947.                     o &= "&" : i = j + 1 : continue while
  948.                 case "mdash"
  949.                     o &= unicode_to_output(&h2014) : i = j + 1 : continue while
  950.                 case "ndash"
  951.                     o &= unicode_to_output(&h2013) : i = j + 1 : continue while
  952.                 case "hellip"
  953.                     o &= unicode_to_output(&h2026) : i = j + 1 : continue while
  954.                 case "lsquo"
  955.                     o &= unicode_to_output(&h2018) : i = j + 1 : continue while
  956.                 case "rsquo"
  957.                     o &= unicode_to_output(&h2019) : i = j + 1 : continue while
  958.                 case "ldquo"
  959.                     o &= unicode_to_output(&h201C) : i = j + 1 : continue while
  960.                 case "rdquo"
  961.                     o &= unicode_to_output(&h201D) : i = j + 1 : continue while
  962.                 case "nbsp"
  963.                     o &= unicode_to_output(&h00A0) : i = j + 1 : continue while
  964.                 end select
  965.  
  966.                 ' Named references beyond the XML five and the handful
  967.                 ' handled above (Latin-1 letters, common punctuation,
  968.                 ' symbols). Unknown names fall through as literal text.
  969.                 dim as integer ecode = entity_code(entity)
  970.                 if ecode >= 0 then
  971.                     o &= unicode_to_output(ecode) : i = j + 1 : continue while
  972.                 end if
  973.  
  974.                 if left(entity, 1) = "#" then
  975.                     dim as string num = mid(entity, 2)
  976.                     dim as integer code = -1
  977.                     if left(num, 1) = "x" orelse left(num, 1) = "X" then
  978.                         code = val("&h" & mid(num, 2))
  979.                     elseif num <> "" then
  980.                         code = val(num)
  981.                     end if
  982.                     ' Reject empty, negative, and out-of-range values so a
  983.                     ' malformed reference can never reach chr() with an
  984.                     ' illegal argument, and never emit an overlong UTF-8
  985.                     ' encoding of U+0000.
  986.                     if code < 1 orelse code > &h10FFFF then code = -1
  987.                     if code < 0 then
  988.                         o &= "?"
  989.                     else
  990.                         o &= unicode_to_output(code)
  991.                     end if
  992.                     i = j + 1
  993.                     continue while
  994.                 end if
  995.             end if
  996.             o &= "&"
  997.             i += 1
  998.         else
  999.             o &= chr(c)
  1000.             i += 1
  1001.         end if
  1002.     wend
  1003.  
  1004.     return o
  1005. end function
  1006.  
  1007. ' Convert a string that may contain raw UTF-8 to CP437 on DOS.
  1008. ' On Linux this is a no-op.
  1009. function cp437_fix(s as string) as string
  1010.     #ifndef __FB_DOS__
  1011.         return s
  1012.     #endif
  1013.  
  1014.     dim as string o = ""
  1015.     dim as integer i = 1
  1016.     dim as integer n = len(s)
  1017.  
  1018.     while i <= n
  1019.         dim as ubyte b0 = asc(s, i)
  1020.  
  1021.         if b0 < 128 then
  1022.             o &= chr(b0)
  1023.             i += 1
  1024.             continue while
  1025.         end if
  1026.  
  1027.         if (b0 and &hE0) = &hC0 andalso i + 1 <= n then
  1028.             dim as ubyte b1 = asc(s, i + 1)
  1029.             if (b1 and &hC0) = &h80 then
  1030.                 dim as integer code = ((b0 and &h1F) shl 6) or (b1 and &h3F)
  1031.                 o &= unicode_to_output(code)
  1032.                 i += 2
  1033.                 continue while
  1034.             end if
  1035.         end if
  1036.  
  1037.         if (b0 and &hF0) = &hE0 andalso i + 2 <= n then
  1038.             dim as ubyte b1 = asc(s, i + 1)
  1039.             dim as ubyte b2 = asc(s, i + 2)
  1040.             if (b1 and &hC0) = &h80 andalso (b2 and &hC0) = &h80 then
  1041.                 dim as integer code = ((b0 and &h0F) shl 12) or ((b1 and &h3F) shl 6) or (b2 and &h3F)
  1042.                 o &= unicode_to_output(code)
  1043.                 i += 3
  1044.                 continue while
  1045.             end if
  1046.         end if
  1047.  
  1048.         if (b0 and &hF8) = &hF0 andalso i + 3 <= n then
  1049.             dim as ubyte b1 = asc(s, i + 1)
  1050.             dim as ubyte b2 = asc(s, i + 2)
  1051.             dim as ubyte b3 = asc(s, i + 3)
  1052.             if (b1 and &hC0) = &h80 andalso (b2 and &hC0) = &h80 andalso (b3 and &hC0) = &h80 then
  1053.                 ' Valid four-byte sequence. CP437 has nothing for it, but
  1054.                 ' consume the whole sequence so it costs one placeholder
  1055.                 ' instead of four.
  1056.                 o &= "?"
  1057.                 i += 4
  1058.                 continue while
  1059.             end if
  1060.         end if
  1061.  
  1062.         ' Not a valid UTF-8 lead byte, or a truncated sequence.
  1063.         ' Emit a placeholder and advance one byte.
  1064.         o &= "?"
  1065.         i += 1
  1066.     wend
  1067.  
  1068.     return o
  1069. end function
  1070.  
  1071. ' Return the value of attrname="..." inside a tag string, or "".
  1072. ' Does not match attrname if it appears as the tail of another
  1073. ' attribute name (e.g. `id` inside `grid`).
  1074. function xml_attr(tag as string, attrname as string) as string
  1075.     dim as string lt = str_lower(tag)
  1076.     dim as string an = str_lower(attrname)
  1077.     dim as integer frompos = 1
  1078.  
  1079.     do
  1080.         dim as integer p = instr(frompos, lt, an & "=")
  1081.         if p = 0 then return ""
  1082.  
  1083.         ' Check the character immediately before the match. If it is
  1084.         ' a letter, digit, underscore, colon, or hyphen, then this
  1085.         ' match is the tail of a longer attribute name and must be
  1086.         ' rejected.
  1087.         dim as integer boundary_ok = 1
  1088.         if p > 1 then
  1089.             dim as integer pc = asc(lt, p - 1)
  1090.             if (pc >= asc("a") andalso pc <= asc("z")) orelse _
  1091.                (pc >= asc("0") andalso pc <= asc("9")) orelse _
  1092.                pc = asc("_") orelse pc = asc(":") orelse pc = asc("-") then
  1093.                 boundary_ok = 0
  1094.             end if
  1095.         end if
  1096.  
  1097.         if boundary_ok = 1 then
  1098.             dim as integer q = p + len(an) + 1
  1099.             dim as string quote = mid(tag, q, 1)
  1100.             if quote <> """" andalso quote <> "'" then
  1101.                 ' Not a quoted value. Keep looking.
  1102.                 frompos = p + 1
  1103.             else
  1104.                 dim as integer e = instr(q + 1, tag, quote)
  1105.                 if e = 0 then return ""
  1106.                 return mid(tag, q + 1, e - q - 1)
  1107.             end if
  1108.         else
  1109.             ' Move past this false match and keep looking.
  1110.             frompos = p + len(an) + 1
  1111.         end if
  1112.     loop
  1113. end function
  1114.  
  1115. type XmlTag
  1116.     raw        as string
  1117.     tname      as string
  1118.     attrs      as string
  1119.     inner      as string
  1120.     selfclosed as integer
  1121. end type
  1122.  
  1123. ' Find all occurrences of <tagname ...> in s. Fills outtags() and
  1124. ' returns the count. Only handles non-nested tags.
  1125. function xml_find_tags(s as string, tagname as string, outtags() as XmlTag) as integer
  1126.     dim as integer count = 0
  1127.     redim outtags(0 to 63)
  1128.     dim as string tn = str_lower(tagname)
  1129.     dim as integer i = 1
  1130.  
  1131.     while i <= len(s)
  1132.         dim as integer p = instr(i, s, "<")
  1133.         if p = 0 then exit while
  1134.         dim as integer e = instr(p, s, ">")
  1135.         if e = 0 then exit while
  1136.  
  1137.         dim as string raw   = mid(s, p, e - p + 1)
  1138.         dim as string inner = mid(s, p + 1, e - p - 1)
  1139.         dim as string inner_lc = str_lower(inner)
  1140.  
  1141.         dim as integer space_pos = instr(inner_lc, " ")
  1142.         dim as integer slash_pos = instr(inner_lc, "/")
  1143.         dim as integer cut = len(inner_lc)
  1144.         if space_pos > 0 andalso space_pos - 1 < cut then cut = space_pos - 1
  1145.         if slash_pos > 0 andalso slash_pos - 1 < cut then cut = slash_pos - 1
  1146.         if cut < 1 then cut = 1
  1147.  
  1148.         dim as string firstword = left(inner_lc, cut)
  1149.  
  1150.         if firstword = tn then
  1151.             if count > ubound(outtags) then redim preserve outtags(0 to count + 63)
  1152.             outtags(count).raw        = raw
  1153.             outtags(count).tname      = firstword
  1154.             outtags(count).attrs      = mid(inner, cut + 1)
  1155.             outtags(count).selfclosed = iif(right(raw, 2) = "/>", 1, 0)
  1156.  
  1157.             if outtags(count).selfclosed = 0 then
  1158.                 dim as integer cp = instr(e, s, "</" & tagname & ">")
  1159.                 if cp = 0 then cp = instr(e, s, "</" & ucase(tagname) & ">")
  1160.                 if cp > 0 then
  1161.                     outtags(count).inner = mid(s, e + 1, cp - e - 1)
  1162.                 end if
  1163.             end if
  1164.             count += 1
  1165.         end if
  1166.  
  1167.         i = e + 1
  1168.     wend
  1169.  
  1170.     return count
  1171. end function
  1172.  
  1173. ' Find the byte offset in xhtml of the element with id="frag".
  1174. ' Returns -1 if not found. Handles both single and double quotes.
  1175. function find_anchor_offset(xhtml as string, frag as string) as integer
  1176.     if frag = "" then return -1
  1177.     dim as string needle1 = "id=""" & frag & """"
  1178.     dim as string needle2 = "id='" & frag & "'"
  1179.     ' A hit is rejected when the character immediately before it is a
  1180.     ' name character (letter, digit, underscore, colon, hyphen), so
  1181.     ' that data-id="frag" does not count as a match.
  1182.     dim as integer frompos = 1
  1183.     do
  1184.         dim as integer p1 = instr(frompos, xhtml, needle1)
  1185.         dim as integer p2 = instr(frompos, xhtml, needle2)
  1186.         dim as integer p = p1
  1187.         if p = 0 orelse (p2 > 0 andalso p2 < p1) then p = p2
  1188.         if p = 0 then return -1
  1189.         dim as integer ok = 1
  1190.         if p > 1 then
  1191.             dim as integer pc = asc(xhtml, p - 1)
  1192.             if (pc >= asc("a") andalso pc <= asc("z")) orelse _
  1193.                (pc >= asc("A") andalso pc <= asc("Z")) orelse _
  1194.                (pc >= asc("0") andalso pc <= asc("9")) orelse _
  1195.                pc = asc("_") orelse pc = asc(":") orelse pc = asc("-") then
  1196.                 ok = 0
  1197.             end if
  1198.         end if
  1199.         if ok then return p
  1200.         frompos = p + 1
  1201.     loop
  1202. end function
  1203.  
  1204. ' =====================================================================
  1205. ' Section 7: EPUB structure (OPF, spine, TOC)
  1206. ' =====================================================================
  1207.  
  1208. type SpineItem
  1209.     idref     as string
  1210.     href      as string
  1211.     mediatype as string
  1212. end type
  1213.  
  1214. type TocEntry
  1215.     label as string
  1216.     href  as string
  1217. end type
  1218.  
  1219. type Epub
  1220.     za       as ZipArchive
  1221.     opfpath  as string
  1222.     opfdir   as string
  1223.     title    as string
  1224.     author   as string
  1225.     spine(any) as SpineItem
  1226.     nspine   as integer
  1227.     toc(any)   as TocEntry
  1228.     ntoc     as integer
  1229. end type
  1230.  
  1231. ' Join a relative path against a base directory, normalizing ./ and ../.
  1232. function path_join(basedir as string, rel as string) as string
  1233.     if rel = "" then return basedir
  1234.     if left(rel, 1) = "/" then return mid(rel, 2)
  1235.  
  1236.     dim as string b = basedir
  1237.     if b <> "" andalso right(b, 1) <> "/" then b &= "/"
  1238.     dim as string r = b & rel
  1239.  
  1240.     dim as string parts(0 to 1023)
  1241.     dim as integer np = 0
  1242.     dim as integer i = 1
  1243.     while i <= len(r)
  1244.         dim as integer j = i
  1245.         while j <= len(r) andalso mid(r, j, 1) <> "/"
  1246.             j += 1
  1247.         wend
  1248.         dim as string seg = mid(r, i, j - i)
  1249.         if seg = ".." then
  1250.             if np > 0 then np -= 1
  1251.         elseif seg <> "" andalso seg <> "." then
  1252.             if np <= ubound(parts) then
  1253.                 parts(np) = seg
  1254.                 np += 1
  1255.             end if
  1256.         end if
  1257.         i = j + 1
  1258.     wend
  1259.  
  1260.     dim as string o = ""
  1261.     for k as integer = 0 to np - 1
  1262.         if k > 0 then o &= "/"
  1263.         o &= parts(k)
  1264.     next
  1265.     return o
  1266. end function
  1267.  
  1268. function dir_of(p as string) as string
  1269.     for i as integer = len(p) to 1 step -1
  1270.         if mid(p, i, 1) = "/" then return left(p, i - 1)
  1271.     next
  1272.     return ""
  1273. end function
  1274.  
  1275. ' Decode percent-escapes (%20 etc.) in a URL path or fragment. Invalid
  1276. ' or truncated escapes are copied through literally. Zip entry names in
  1277. ' epubs are the decoded byte strings, so hrefs must be decoded before
  1278. ' they can be matched against the archive.
  1279. function url_decode(s as string) as string
  1280.     dim as string o = ""
  1281.     dim as integer i = 1
  1282.     dim as integer n = len(s)
  1283.     while i <= n
  1284.         if asc(s, i) = asc("%") andalso i + 2 <= n then
  1285.             dim as integer ok = 1
  1286.             for k as integer = 1 to 2
  1287.                 dim as integer hc = asc(s, i + k)
  1288.                 dim as integer hex_ok = (hc >= asc("0") andalso hc <= asc("9")) orelse _
  1289.                                         (hc >= asc("a") andalso hc <= asc("f")) orelse _
  1290.                                         (hc >= asc("A") andalso hc <= asc("F"))
  1291.                 if not hex_ok then ok = 0
  1292.             next
  1293.             if ok then
  1294.                 o &= chr(cint(val("&h" & mid(s, i + 1, 2))))
  1295.                 i += 3
  1296.                 continue while
  1297.             end if
  1298.         end if
  1299.         o &= chr(asc(s, i))
  1300.         i += 1
  1301.     wend
  1302.     return o
  1303. end function
  1304.  
  1305. ' Return the spine index whose href matches the given href, or -1.
  1306. ' Comparison ignores any leading "./" or "/" and is exact otherwise.
  1307. function spine_index_by_href(byref ep as Epub, href as string) as integer
  1308.     dim as string h = href
  1309.     if left(h, 2) = "./" then h = mid(h, 3)
  1310.     if left(h, 1) = "/" then h = mid(h, 2)
  1311.     for i as integer = 0 to ep.nspine - 1
  1312.         dim as string s = ep.spine(i).href
  1313.         if left(s, 2) = "./" then s = mid(s, 3)
  1314.         if left(s, 1) = "/" then s = mid(s, 2)
  1315.         if s = h then return i
  1316.     next
  1317.     return -1
  1318. end function
  1319.  
  1320. ' Split "path#frag" into path and frag. If no '#', frag is "".
  1321. sub split_href(href as string, byref path as string, byref frag as string)
  1322.     dim as integer p = instr(href, "#")
  1323.     if p > 0 then
  1324.         path = left(href, p - 1)
  1325.         frag = mid(href, p + 1)
  1326.     else
  1327.         path = href
  1328.         frag = ""
  1329.     end if
  1330. end sub
  1331.  
  1332. ' Parse container.xml, the OPF, and either NCX or nav.xhtml.
  1333. ' Returns 0 on success, -1 on failure.
  1334. function epub_load(byref ep as Epub, filename as string) as integer
  1335.     if zip_open(ep.za, filename) <> 0 then return -1
  1336.  
  1337.     dim as string cont = zip_extract_string(ep.za, "META-INF/container.xml")
  1338.     if cont = "" then
  1339.         print "epub_load: cannot read META-INF/container.xml"
  1340.         zip_close(ep.za)
  1341.         return -1
  1342.     end if
  1343.  
  1344.     dim as XmlTag roots()
  1345.     dim as integer n = xml_find_tags(cont, "rootfile", roots())
  1346.     if n = 0 then
  1347.         print "epub_load: no rootfile in container.xml"
  1348.         zip_close(ep.za)
  1349.         return -1
  1350.     end if
  1351.  
  1352.     dim as string fullpath = xml_attr(roots(0).raw, "full-path")
  1353.     if fullpath = "" then
  1354.         print "epub_load: rootfile has no full-path"
  1355.         zip_close(ep.za)
  1356.         return -1
  1357.     end if
  1358.  
  1359.     ep.opfpath = fullpath
  1360.     ep.opfdir  = dir_of(fullpath)
  1361.  
  1362.     dim as string opf = zip_extract_string(ep.za, fullpath)
  1363.     if opf = "" then
  1364.         print "epub_load: cannot read opf"
  1365.         zip_close(ep.za)
  1366.         return -1
  1367.     end if
  1368.  
  1369.     ' Metadata
  1370.     dim as XmlTag tags()
  1371.     dim as integer c = xml_find_tags(opf, "title", tags())
  1372.     if c > 0 then ep.title = xml_unescape(str_trim(tags(0).inner))
  1373.     c = xml_find_tags(opf, "creator", tags())
  1374.     if c > 0 then ep.author = xml_unescape(str_trim(tags(0).inner))
  1375.  
  1376.     ' Manifest: collect id -> href, media-type
  1377.     dim as string manifest_ids(0 to MAX_MANIFEST)
  1378.     dim as string manifest_hrefs(0 to MAX_MANIFEST)
  1379.     dim as string manifest_types(0 to MAX_MANIFEST)
  1380.     dim as integer nman = 0
  1381.  
  1382.     c = xml_find_tags(opf, "item", tags())
  1383.     for i as integer = 0 to c - 1
  1384.         dim as string id_ = xml_attr(tags(i).raw, "id")
  1385.         dim as string hr  = xml_attr(tags(i).raw, "href")
  1386.         dim as string mt  = xml_attr(tags(i).raw, "media-type")
  1387.         if id_ <> "" andalso nman <= MAX_MANIFEST then
  1388.             manifest_ids(nman)   = id_
  1389.             ' Manifest hrefs are URLs: decode percent-escapes so they
  1390.             ' can be matched against the raw byte names of zip entries.
  1391.             manifest_hrefs(nman) = url_decode(hr)
  1392.             manifest_types(nman) = mt
  1393.             nman += 1
  1394.         end if
  1395.     next
  1396.  
  1397.     ' Spine: resolve each itemref to a manifest href
  1398.     c = xml_find_tags(opf, "itemref", tags())
  1399.     redim ep.spine(0 to iif(c > 0, c - 1, 0))
  1400.     ep.nspine = 0
  1401.     for i as integer = 0 to c - 1
  1402.         dim as string idref = xml_attr(tags(i).raw, "idref")
  1403.         if idref = "" then continue for
  1404.         for j as integer = 0 to nman - 1
  1405.             if manifest_ids(j) = idref then
  1406.                 ep.spine(ep.nspine).idref     = idref
  1407.                 ep.spine(ep.nspine).href      = path_join(ep.opfdir, manifest_hrefs(j))
  1408.                 ep.spine(ep.nspine).mediatype = manifest_types(j)
  1409.                 ep.nspine += 1
  1410.                 exit for
  1411.             end if
  1412.         next
  1413.     next
  1414.  
  1415.     ' Locate NCX and/or nav.xhtml
  1416.     dim as string ncxpath = ""
  1417.     dim as string navpath = ""
  1418.     for j as integer = 0 to nman - 1
  1419.         if manifest_types(j) = "application/x-dtbncx+xml" then
  1420.             ncxpath = path_join(ep.opfdir, manifest_hrefs(j))
  1421.         end if
  1422.         if manifest_types(j) = "application/xhtml+xml" andalso navpath = "" then
  1423.             ' Match against the file name, not any substring of the whole
  1424.             ' path, so that e.g. "renavigate.xhtml" is not mistaken for a
  1425.             ' nav document.
  1426.             dim as string navbase = manifest_hrefs(j)
  1427.             for k as integer = len(navbase) to 1 step -1
  1428.                 if mid(navbase, k, 1) = "/" then
  1429.                     navbase = mid(navbase, k + 1)
  1430.                     exit for
  1431.                 end if
  1432.             next
  1433.             if left(navbase, 3) = "nav" orelse instr(navbase, "nav.") > 0 then
  1434.                 navpath = path_join(ep.opfdir, manifest_hrefs(j))
  1435.             end if
  1436.         end if
  1437.     next
  1438.  
  1439.     dim as integer ntoc = 0
  1440.     redim ep.toc(0 to MAX_TOC)
  1441.  
  1442.     ' Prefer NCX.
  1443.     if ncxpath <> "" then
  1444.         dim as string ncx = zip_extract_string(ep.za, ncxpath)
  1445.         if ncx <> "" then
  1446.             dim as XmlTag navs()
  1447.             dim as integer nn = xml_find_tags(ncx, "navPoint", navs())
  1448.             for i as integer = 0 to nn - 1
  1449.                 dim as XmlTag lbls()
  1450.                 dim as integer nl = xml_find_tags(navs(i).inner, "text", lbls())
  1451.                 dim as XmlTag cts()
  1452.                 dim as integer nc = xml_find_tags(navs(i).inner, "content", cts())
  1453.                 if nl > 0 andalso nc > 0 then
  1454.                     if ntoc > ubound(ep.toc) then redim preserve ep.toc(0 to ntoc + MAX_TOC)
  1455.                     ep.toc(ntoc).label = xml_unescape(str_trim(lbls(0).inner))
  1456.                     ' content src is relative to the NCX document, not to
  1457.                     ' the OPF package; make it archive-rooted here.
  1458.                     ep.toc(ntoc).href  = path_join(dir_of(ncxpath), url_decode(xml_attr(cts(0).raw, "src")))
  1459.                     ntoc += 1
  1460.                 end if
  1461.             next
  1462.         end if
  1463.     end if
  1464.  
  1465.     ' Fall back to nav.xhtml anchors.
  1466.     if ntoc = 0 andalso navpath <> "" then
  1467.         dim as string nav = zip_extract_string(ep.za, navpath)
  1468.         if nav <> "" then
  1469.             dim as XmlTag as_()
  1470.             dim as integer na = xml_find_tags(nav, "a", as_())
  1471.             for i as integer = 0 to na - 1
  1472.                 dim as string hr = xml_attr(as_(i).raw, "href")
  1473.                 if hr <> "" then
  1474.                     if ntoc > ubound(ep.toc) then redim preserve ep.toc(0 to ntoc + MAX_TOC)
  1475.                     ep.toc(ntoc).label = xml_unescape(str_trim(as_(i).inner))
  1476.                     ' nav.xhtml hrefs are relative to the nav document.
  1477.                     ep.toc(ntoc).href  = path_join(dir_of(navpath), url_decode(hr))
  1478.                     ntoc += 1
  1479.                 end if
  1480.             next
  1481.         end if
  1482.     end if
  1483.  
  1484.     ep.ntoc = ntoc
  1485.     return 0
  1486. end function
  1487.  
  1488. sub epub_close(byref ep as Epub)
  1489.     zip_close(ep.za)
  1490.     erase ep.spine
  1491.     erase ep.toc
  1492. end sub
  1493.  
  1494. ' =====================================================================
  1495. ' Section 8: HTML to plain text
  1496. ' =====================================================================
  1497.  
  1498. ' Strip tags, decode entities, and insert newlines for block-level tags.
  1499. ' Image alt text, if present, is emitted inline in brackets.
  1500. ' On DOS, the output is translated from UTF-8 to CP437 at the end.
  1501. function html_to_text(h as string) as string
  1502.     dim as string txt = ""
  1503.     dim as integer i = 1
  1504.     dim as integer n = len(h)
  1505.  
  1506.     while i <= n
  1507.         dim as integer lt = instr(i, h, "<")
  1508.         if lt = 0 then
  1509.             txt &= xml_unescape(mid(h, i))
  1510.             exit while
  1511.         end if
  1512.         if lt > i then txt &= xml_unescape(mid(h, i, lt - i))
  1513.  
  1514.         dim as integer gt = instr(lt, h, ">")
  1515.         if gt = 0 then
  1516.             txt &= xml_unescape(mid(h, lt))
  1517.             exit while
  1518.         end if
  1519.  
  1520.         dim as string tag = str_lower(mid(h, lt + 1, gt - lt - 1))
  1521.         dim as integer space_pos = instr(tag, " ")
  1522.         dim as integer slash_pos = instr(tag, "/")
  1523.         dim as integer cut = len(tag)
  1524.         if space_pos > 0 andalso space_pos - 1 < cut then cut = space_pos - 1
  1525.         if slash_pos > 0 andalso slash_pos - 1 < cut then cut = slash_pos - 1
  1526.         if cut < 1 then cut = 1
  1527.  
  1528.         dim as string nm = left(tag, cut)
  1529.         if left(nm, 1) = "/" then nm = mid(nm, 2)
  1530.  
  1531.         select case nm
  1532.         case "br", "p", "div", "li", "h1", "h2", "h3", "h4", "h5", "h6", "tr", "hr", _
  1533.              "blockquote", "pre", "figcaption", "figure", "article", "section", _
  1534.              "header", "footer", "aside", "nav", "table", "ul", "ol", "dl", "dt", "dd"
  1535.             txt &= chr(10)
  1536.         case "img"
  1537.             dim as string alttext = xml_attr(mid(h, lt + 1, gt - lt - 1), "alt")
  1538.             alttext = xml_unescape(str_trim(alttext))
  1539.             if alttext <> "" then
  1540.                 txt &= "[" & alttext & "]"
  1541.             end if
  1542.         end select
  1543.  
  1544.         if nm = "script" or nm = "style" then
  1545.             dim as integer cp = instr(gt, h, "</" & nm & ">")
  1546.             if cp = 0 then cp = instr(gt, h, "</" & ucase(nm) & ">")
  1547.             if cp > 0 then
  1548.                 i = cp + len(nm) + 3
  1549.             else
  1550.                 ' Unterminated block: drop everything to the end of the
  1551.                 ' document rather than render the body as text.
  1552.                 i = n + 1
  1553.             end if
  1554.             continue while
  1555.         end if
  1556.  
  1557.         i = gt + 1
  1558.     wend
  1559.  
  1560.     ' CRLF and lone CR bytes in the source would survive as chr(13) in
  1561.     ' the output and show up as control characters on screen; drop them.
  1562.     if instr(txt, chr(13)) > 0 then
  1563.         dim as string cleaned = ""
  1564.         for k as integer = 1 to len(txt)
  1565.             dim as integer cc = asc(txt, k)
  1566.             if cc <> 13 then cleaned &= chr(cc)
  1567.         next
  1568.         txt = cleaned
  1569.     end if
  1570.  
  1571.     return cp437_fix(txt)
  1572. end function
  1573.  
  1574. ' =====================================================================
  1575. ' Section 9: Pager, word wrap, back-paging
  1576. ' =====================================================================
  1577. ' Page text is accumulated into a single big string (g_page_text) with
  1578. ' an integer array of line-start offsets (g_page_off). This avoids the
  1579. ' thousands of small string allocations that a string array would need
  1580. ' on DOS, and makes page redraws cheap: slice on demand.
  1581. '
  1582. ' Every wrapped line is retained, including lines before the resume
  1583. ' point, so back-paging can walk into the skipped region.
  1584.  
  1585. dim shared as integer g_page_lines    = DEFAULT_PAGE_LINES
  1586. dim shared as integer g_interactive   = -1
  1587. dim shared as integer g_wrap_cols     = DEFAULT_WRAP
  1588. dim shared as integer g_show_mode     = 0
  1589. dim shared as string  g_resume_file
  1590. dim shared as string  g_resume_idx
  1591. dim shared as integer g_resume_toc    = 0
  1592.  
  1593. dim shared as string  g_page_text
  1594. dim shared as integer g_page_off(any)
  1595. dim shared as integer g_page_count = 0
  1596. dim shared as integer g_page_start = 0
  1597.  
  1598. ' Append one wrapped line to the flat page buffer.
  1599. ' Lines before the resume point are kept, not discarded.
  1600. sub buf_append(s as string)
  1601.     if g_page_count > ubound(g_page_off) then
  1602.         dim as integer newcap = iif(g_page_count = 0, 1024, g_page_count * 2)
  1603.         redim preserve g_page_off(0 to newcap - 1)
  1604.     end if
  1605.  
  1606.     g_page_off(g_page_count) = len(g_page_text) + 1
  1607.     g_page_text &= s & chr(10)
  1608.     g_page_count += 1
  1609. end sub
  1610.  
  1611. ' Return line i (0-based) from the flat buffer.
  1612. function page_line(i as integer) as string
  1613.     if i < 0 or i >= g_page_count then return ""
  1614.     dim as integer startpos = g_page_off(i)
  1615.     dim as integer endpos
  1616.     if i + 1 < g_page_count then
  1617.         endpos = g_page_off(i+1) - 1
  1618.     else
  1619.         ' The last line still carries the chr(10) appended by buf_append;
  1620.         ' exclude it so the final line does not print a spurious blank row.
  1621.         endpos = len(g_page_text)
  1622.         if endpos >= 1 andalso asc(g_page_text, endpos) = 10 then endpos -= 1
  1623.     end if
  1624.     if endpos < startpos then return ""
  1625.     return mid(g_page_text, startpos, endpos - startpos)
  1626. end function
  1627.  
  1628. ' Word-wrap a single source line and append each output line.
  1629. sub wrap_and_append(s as string)
  1630.     ' Empty and whitespace-only lines both produce one blank line, so a
  1631.     ' blank line that merely contains stray spaces does not silently
  1632.     ' merge the paragraphs on either side of it.
  1633.     if str_trim(s) = "" then
  1634.         buf_append ""
  1635.         return
  1636.     end if
  1637.  
  1638.     dim as integer i = 1
  1639.     dim as integer n = len(s)
  1640.     dim as string curword = ""
  1641.     dim as string curout  = ""
  1642.  
  1643.     while i <= n
  1644.         dim as integer c = asc(s, i)
  1645.         if c = 32 or c = 9 then
  1646.             if curword <> "" then
  1647.                 if curout = "" then
  1648.                     curout = curword
  1649.                 elseif len(curout) + 1 + len(curword) <= g_wrap_cols then
  1650.                     curout = curout & " " & curword
  1651.                 else
  1652.                     buf_append curout
  1653.                     curout = curword
  1654.                 end if
  1655.                 curword = ""
  1656.             end if
  1657.         else
  1658.             curword &= chr(c)
  1659.         end if
  1660.         i += 1
  1661.     wend
  1662.  
  1663.     if curword <> "" then
  1664.         if curout = "" then
  1665.             curout = curword
  1666.         elseif len(curout) + 1 + len(curword) <= g_wrap_cols then
  1667.             curout = curout & " " & curword
  1668.         else
  1669.             buf_append curout
  1670.             curout = curword
  1671.         end if
  1672.     end if
  1673.  
  1674.     if curout <> "" then buf_append curout
  1675. end sub
  1676.  
  1677. sub reset_pager()
  1678.     g_page_text = ""
  1679.     erase g_page_off
  1680.     g_page_count    = 0
  1681.     g_page_start    = 0
  1682. end sub
  1683.  
  1684. ' Return the path of the bookmark file for a given epub.
  1685. ' Strips the extension of the final path component, then appends
  1686. ' ".bmk". So "book.epub" -> "book.bmk", and "dir/x.epub" -> "dir/x.bmk".
  1687. function bookmark_path(filename as string) as string
  1688.     dim as integer last_slash = 0
  1689.     for i as integer = len(filename) to 1 step -1
  1690.         if mid(filename, i, 1) = "/" orelse mid(filename, i, 1) = "\" then
  1691.            last_slash = i
  1692.            exit for
  1693.        end if
  1694.    next
  1695.  
  1696.    dim as integer last_dot = 0
  1697.    for i as integer = len(filename) to last_slash + 1 step -1
  1698.        if mid(filename, i, 1) = "." then
  1699.            last_dot = i
  1700.            exit for
  1701.        end if
  1702.    next
  1703.  
  1704.    if last_dot = 0 then
  1705.        return filename & ".bmk"
  1706.    end if
  1707.  
  1708.    return left(filename, last_dot - 1) & ".bmk"
  1709. end function
  1710.  
  1711. ' Write the bookmark. Returns 0 on success, -1 on failure.
  1712. ' Layout: line 1 = TOC index, line 2 = spine section, line 3 = skip.
  1713. function bookmark_write(filename as string, toc as integer, section as integer, skip as integer) as integer
  1714.    dim as string bm = bookmark_path(filename)
  1715.    dim as integer f = freefile
  1716.    if open(bm for output as #f) <> 0 then return -1
  1717.    print #f, str(toc)
  1718.    print #f, str(section)
  1719.    print #f, str(skip)
  1720.    close #f
  1721.    return 0
  1722. end function
  1723.  
  1724. ' Read the bookmark. Returns 0 on success, -1 on failure.
  1725. ' On success, sets toc, section, and skip.
  1726. function bookmark_read(filename as string, byref toc as integer, byref section as integer, byref skip as integer) as integer
  1727.    dim as string bm = bookmark_path(filename)
  1728.    dim as integer f = freefile
  1729.    if open(bm for input as #f) <> 0 then return -1
  1730.    dim as string line1, line2, line3
  1731.    if eof(f) then
  1732.        close #f
  1733.        return -1
  1734.    end if
  1735.    line input #f, line1
  1736.    if eof(f) then
  1737.        close #f
  1738.        return -1
  1739.    end if
  1740.    line input #f, line2
  1741.    if eof(f) then
  1742.        close #f
  1743.        return -1
  1744.    end if
  1745.    line input #f, line3
  1746.    close #f
  1747.    toc     = val(line1)
  1748.    section = val(line2)
  1749.    skip    = val(line3)
  1750.    if toc < 0 then toc = 0
  1751.    if section < 0 then section = 0
  1752.    if skip < 0 then skip = 0
  1753.    return 0
  1754. end function
  1755.  
  1756. ' Print the resume command for the current page top, and save the
  1757. ' bookmark so a later `epubrd <file>` with no command resumes here.
  1758. ' Only the bookmark form is printed. The explicit `show` form is
  1759. ' not shown because, when the current TOC entry has an anchor,
  1760. ' `show` would jump to the anchor and not to the exact line.
  1761. sub print_resume_hint()
  1762.    if g_show_mode = 0 then return
  1763.    dim as integer section = val(g_resume_idx)
  1764.    dim as integer toc = g_resume_toc
  1765.    if toc < 0 then toc = 0
  1766.    print
  1767.    print "to resume: epubrd " & g_resume_file
  1768.    if section < 0 then
  1769.        print "(no section was loaded; bookmark left unchanged)"
  1770.        exit sub
  1771.    end if
  1772.    if bookmark_write(g_resume_file, toc, section, g_page_start) <> 0 then
  1773.        print "(warning: could not write " & bookmark_path(g_resume_file) & ")"
  1774.    end if
  1775. end sub
  1776.  
  1777. sub draw_page()
  1778.    cls
  1779.    dim as integer first = g_page_start
  1780.    dim as integer last  = g_page_start + g_page_lines - 1
  1781.    if last > g_page_count - 1 then last = g_page_count - 1
  1782.    for i as integer = first to last
  1783.        print page_line(i)
  1784.    next
  1785. end sub
  1786.  
  1787. ' Extract one spine item to text and wrap it into the page buffer.
  1788. ' Applies the given skip as g_page_start.
  1789. sub load_section(byref ep as Epub, idx as integer, skip as integer)
  1790.    dim as string xhtml = zip_extract_string(ep.za, ep.spine(idx).href)
  1791.    reset_pager()
  1792.    if xhtml = "" then
  1793.        buf_append "(could not read " & ep.spine(idx).href & ")"
  1794.        return
  1795.    end if
  1796.  
  1797.    dim as string body = html_to_text(xhtml)
  1798.  
  1799.    dim as integer i  = 1
  1800.    dim as integer bn = len(body)
  1801.    dim as string curline = ""
  1802.  
  1803.    while i <= bn
  1804.        dim as integer c = asc(body, i)
  1805.        if c = 10 then
  1806.            wrap_and_append curline
  1807.            curline = ""
  1808.        else
  1809.            curline &= chr(c)
  1810.        end if
  1811.        i += 1
  1812.    wend
  1813.    if curline <> "" then wrap_and_append curline
  1814.  
  1815.    if skip > 0 then
  1816.        g_page_start = skip
  1817.        if g_page_start > g_page_count then g_page_start = g_page_count
  1818.    end if
  1819. end sub
  1820.  
  1821. ' Given a TOC index, resolve it to a spine index and a line skip.
  1822. ' Returns 0 on success. Returns -1 if the TOC entry cannot be
  1823. ' resolved to a spine item.
  1824. function resolve_toc_to_section(byref ep as Epub, tocidx as integer, byref section as integer, byref skip as integer) as integer
  1825.    if tocidx < 0 or tocidx >= ep.ntoc then return -1
  1826.  
  1827.    dim as string tpath = ""
  1828.    dim as string tfrag = ""
  1829.    split_href(ep.toc(tocidx).href, tpath, tfrag)
  1830.  
  1831.    ' TOC hrefs are made archive-rooted when the TOC is parsed, so they
  1832.    ' can be matched against spine hrefs directly. The opfdir join stays
  1833.    ' only as a fallback for books that root their TOC hrefs at the OPF
  1834.    ' package directory.
  1835.    dim as integer idx = spine_index_by_href(ep, tpath)
  1836.    if idx < 0 then
  1837.        idx = spine_index_by_href(ep, path_join(ep.opfdir, tpath))
  1838.    end if
  1839.    if idx < 0 then return -1
  1840.  
  1841.    section = idx
  1842.    skip = 0
  1843.  
  1844.    if tfrag <> "" then
  1845.        dim as string xhtml = zip_extract_string(ep.za, ep.spine(idx).href)
  1846.        if xhtml <> "" then
  1847.            dim as integer off = find_anchor_offset(xhtml, tfrag)
  1848.            if off > 0 then
  1849.                dim as string prefix = html_to_text(left(xhtml, off - 1))
  1850.                reset_pager()
  1851.                dim as integer pi = 1
  1852.                dim as string pcur = ""
  1853.                while pi <= len(prefix)
  1854.                    dim as integer pc = asc(prefix, pi)
  1855.                    if pc = 10 then
  1856.                        wrap_and_append pcur
  1857.                        pcur = ""
  1858.                    else
  1859.                        pcur &= chr(pc)
  1860.                    end if
  1861.                    pi += 1
  1862.                wend
  1863.                if pcur <> "" then wrap_and_append pcur
  1864.                skip = g_page_count
  1865.                reset_pager()
  1866.            end if
  1867.        end if
  1868.    end if
  1869.  
  1870.    return 0
  1871. end function
  1872.  
  1873. ' Draw the TOC screen and read a selection. Paginates if the TOC
  1874. ' has more entries than fit on one screen. Returns the chosen TOC
  1875. ' index, or -1 if the user cancelled with q/Q/Esc.
  1876. function show_toc_screen(byref ep as Epub) as integer
  1877.    if ep.ntoc = 0 then
  1878.        cls
  1879.        print "Table of contents"
  1880.        print
  1881.        print "(no toc found)"
  1882.        print
  1883.        print "press any key to return"
  1884.        getkey
  1885.        return -1
  1886.    end if
  1887.  
  1888.    ' Lines available for entries: page height minus two for the
  1889.    ' header and the prompt.
  1890.    dim as integer per_page = g_page_lines - 2
  1891.    if per_page < 4 then per_page = 4
  1892.  
  1893.    dim as integer start_idx = 0
  1894.    dim as string buf = ""
  1895.  
  1896.    do
  1897.        if start_idx < 0 then start_idx = 0
  1898.        if start_idx > ep.ntoc - 1 then
  1899.            start_idx = ep.ntoc - per_page
  1900.            if start_idx < 0 then start_idx = 0
  1901.        end if
  1902.  
  1903.        cls
  1904.        print "Table of contents"
  1905.        print
  1906.  
  1907.        dim as integer last_idx = start_idx + per_page - 1
  1908.        if last_idx > ep.ntoc - 1 then last_idx = ep.ntoc - 1
  1909.  
  1910.        for i as integer = start_idx to last_idx
  1911.            dim as string label = ep.toc(i).label
  1912.            if label = "" then label = ep.toc(i).href
  1913.            print right("   " & str(i), 5) & "  " & label
  1914.        next
  1915.  
  1916.        print
  1917.  
  1918.        dim as integer more = 0
  1919.        dim as integer prev = 0
  1920.        if last_idx < ep.ntoc - 1 then more = 1
  1921.        if start_idx > 0 then prev = 1
  1922.  
  1923.        dim as string prompt = "[" & buf & "]  "
  1924.        if prev = 1 then prompt &= "b=back  "
  1925.        if more = 1 then prompt &= "space/Enter=more  "
  1926.        prompt &= "number then Enter=jump  q/Q/Esc=return"
  1927.        print prompt
  1928.  
  1929.        dim as integer k = getkey
  1930.        if k = KEY_CTRL_C then
  1931.            print_resume_hint()
  1932.            end 0
  1933.        end if
  1934.        if k = KEY_Q_LOWER or k = KEY_Q_UPPER or k = KEY_ESC then
  1935.            return -1
  1936.        end if
  1937.        if k = KEY_B_LOWER or k = KEY_B_UPPER then
  1938.            if prev = 1 then
  1939.                start_idx -= per_page
  1940.                if start_idx < 0 then start_idx = 0
  1941.            end if
  1942.        elseif k = KEY_ENTER then
  1943.            if buf <> "" then
  1944.                dim as integer chosen = val(buf)
  1945.                if chosen >= 0 andalso chosen < ep.ntoc then
  1946.                    return chosen
  1947.                end if
  1948.                print chr(7);
  1949.                buf = ""
  1950.            else
  1951.                if more = 1 then
  1952.                    start_idx = last_idx + 1
  1953.                end if
  1954.            end if
  1955.        elseif k = KEY_SPACE then
  1956.            if more = 1 then
  1957.                start_idx = last_idx + 1
  1958.            end if
  1959.        elseif k >= 48 andalso k <= 57 then
  1960.            buf &= chr(k)
  1961.        else
  1962.            ' ignore unrecognized keys: no action
  1963.        end if
  1964.    loop
  1965. end function
  1966.  
  1967. sub page_loop(byref ep as Epub)
  1968.    if g_interactive = 0 then
  1969.        for i as integer = g_page_start to g_page_count - 1
  1970.            print page_line(i)
  1971.        next
  1972.        return
  1973.    end if
  1974.  
  1975.    if g_page_count = 0 then return
  1976.  
  1977.    do
  1978.        draw_page()
  1979.  
  1980.        dim as integer shown_last = g_page_start + g_page_lines - 1
  1981.        if shown_last > g_page_count - 1 then shown_last = g_page_count - 1
  1982.  
  1983.        print
  1984.        print "-- [space]/[Enter]=next  [b]=back  [t]=toc  [q]=quit --"
  1985.  
  1986.        select case getkey
  1987.        case KEY_CTRL_C, KEY_Q_LOWER, KEY_Q_UPPER
  1988.            print_resume_hint()
  1989.            end 0
  1990.        case KEY_B_LOWER, KEY_B_UPPER
  1991.            if g_page_start > 0 then
  1992.                g_page_start -= g_page_lines
  1993.                if g_page_start < 0 then g_page_start = 0
  1994.            end if
  1995.        case KEY_ENTER, KEY_SPACE
  1996.            if shown_last >= g_page_count - 1 then
  1997.                print_resume_hint()
  1998.                end 0
  1999.            end if
  2000.            g_page_start = shown_last + 1
  2001.        case KEY_T_LOWER, KEY_T_UPPER
  2002.            dim as integer newsection = 0
  2003.            dim as integer newskip = 0
  2004.            dim as integer chosen = show_toc_screen(ep)
  2005.            if chosen >= 0 then
  2006.                if resolve_toc_to_section(ep, chosen, newsection, newskip) = 0 then
  2007.                    g_resume_toc = chosen
  2008.                    g_resume_idx = str(newsection)
  2009.                    load_section(ep, newsection, newskip)
  2010.                else
  2011.                    print "cannot resolve toc entry"
  2012.                    sleep 1000
  2013.                end if
  2014.            end if
  2015.        case else
  2016.            ' ignore all other keys
  2017.        end select
  2018.    loop
  2019. end sub
  2020.  
  2021. ' =====================================================================
  2022. ' Section 10: Main
  2023. ' =====================================================================
  2024.  
  2025. sub show_usage()
  2026.    print "epubrd"
  2027.    print "usage:"
  2028.    print "  epubrd <file.epub>                     resume from bookmark"
  2029.    print "  epubrd <file.epub> meta"
  2030.    print "  epubrd <file.epub> meta_print"
  2031.    print "  epubrd <file.epub> toc"
  2032.    print "  epubrd <file.epub> toc_print"
  2033.    print "  epubrd <file.epub> show [n] [skip]"
  2034.    print "  epubrd <file.epub> goto_section <toc-index>"
  2035.    print "  epubrd <file.epub> next_section <n> [skip]"
  2036.    print
  2037.    print "toc opens the table of contents screen and lets you pick"
  2038.    print "an entry to read. toc_print prints it as plain text, no"
  2039.    print "interaction. `show`, `goto_section`, and `next_section`"
  2040.    print "all take TOC indexes."
  2041.    print
  2042.    print "show [n] [skip]: read TOC entry n as text. If the entry"
  2043.    print "has an anchor, jumps to it; otherwise starts at line 0 or"
  2044.    print "at <skip> if given. With no argument, resumes from"
  2045.    print "bookmark."
  2046.    print
  2047.    print "goto_section <toc-index>: same as `show <toc-index>`."
  2048.    print
  2049.    print "next_section <n> [skip]: read TOC entry n, then continue"
  2050.    print "into n+1, n+2, and so on until the end of the TOC or quit."
  2051.    print "skip applies to the first entry only if it has no anchor."
  2052.    print
  2053.    print "meta_print: print title and author as plain text, no"
  2054.    print "interaction. Use this when you want to script or grep"
  2055.    print "the metadata."
  2056.    print
  2057.    print "At each prompt:"
  2058.    print "  [space] or [Enter]  next page"
  2059.    print "  [b] or [B]          previous page"
  2060.    print "  [t] or [T]          table of contents"
  2061.    print "  [q] or [Q]          quit"
  2062.    print "  [ctrl-C]            quit"
  2063.    print "  any other key       ignored"
  2064.    print "On quit, prints the command to resume at the top line on screen."
  2065.    print
  2066.    print "TOC screen:"
  2067.    print "  Type a number and press [Enter] to jump to that entry."
  2068.    print "  [space]/[Enter] pages forward; [b] pages back."
  2069.    print "  [q], [Q], or [Esc] returns to the page you were on."
  2070.    print
  2071.    print "Bookmark:"
  2072.    print "  On quit, writes <basename>.bmk next to the epub with the"
  2073.    print "  current TOC index, section, and line. `epubrd <file>` with"
  2074.    print "  no command resumes from it. For book.epub the bookmark is"
  2075.    print "  book.bmk."
  2076.    print
  2077.    print "Environment:"
  2078.    print "  EPUBREAD_MAX    max epub size in bytes (default 16777216)"
  2079.    print "  EPUBREAD_COLS   wrap width, 20..78 (default 78)"
  2080.    print "  EPUBREAD_ROWS   terminal rows (default 25); lines/page = rows-4"
  2081.    print "  EPUBREAD_LINES  content lines per page, overrides rows-4"
  2082. end sub
  2083.  
  2084. ' Return argv[n], trimmed of trailing CR/LF/space that DOS may leave.
  2085. function clean_arg(n as integer) as string
  2086.    dim as string s = command(n)
  2087.    while len(s) > 0
  2088.        dim as integer c = asc(s, len(s))
  2089.        if c = 13 or c = 10 or c = 32 or c = 9 then
  2090.            s = left(s, len(s) - 1)
  2091.        else
  2092.            exit while
  2093.        end if
  2094.    wend
  2095.    return s
  2096. end function
  2097.  
  2098. sub read_environment()
  2099.    #ifdef __FB_DOS__
  2100.        g_interactive = -1
  2101.    #else
  2102.        g_interactive = iif(environ("TERM") <> "", -1, 0)
  2103.    #endif
  2104.  
  2105.    dim as string cols_env = environ("EPUBREAD_COLS")
  2106.    if cols_env = "" then cols_env = environ("COLUMNS")
  2107.    if cols_env <> "" then
  2108.        dim as integer c = val(cols_env)
  2109.        if c >= 20 andalso c <= DEFAULT_WRAP then g_wrap_cols = c
  2110.    end if
  2111.  
  2112.    dim as integer term_rows = DEFAULT_ROWS
  2113.    dim as string rows_env = environ("EPUBREAD_ROWS")
  2114.    if rows_env <> "" then
  2115.        dim as integer r = val(rows_env)
  2116.        if r >= 10 andalso r <= 250 then term_rows = r
  2117.    end if
  2118.  
  2119.    g_page_lines = term_rows - 4
  2120.    if g_page_lines < 6 then g_page_lines = 6
  2121.  
  2122.    dim as string lines_env = environ("EPUBREAD_LINES")
  2123.    if lines_env <> "" then
  2124.        dim as integer l = val(lines_env)
  2125.        if l >= 6 andalso l <= 240 then g_page_lines = l
  2126.    end if
  2127. end sub
  2128.  
  2129. read_environment()
  2130.  
  2131. dim as string filename = clean_arg(1)
  2132. dim as string cmd      = clean_arg(2)
  2133. dim as string arg      = clean_arg(3)
  2134. dim as string skip_arg = clean_arg(4)
  2135.  
  2136. if filename = "" then
  2137.    show_usage()
  2138.    end 1
  2139. end if
  2140.  
  2141. if cmd = "" then
  2142.    dim as integer btoc = 0
  2143.    dim as integer bsec = 0
  2144.    dim as integer bskip = 0
  2145.    if bookmark_read(filename, btoc, bsec, bskip) = 0 then
  2146.        ' Resume in spine terms, bypassing TOC resolution.
  2147.        cmd      = "resume"
  2148.        arg      = str(bsec)
  2149.        skip_arg = str(bskip)
  2150.        g_resume_toc = btoc
  2151.    else
  2152.        show_usage()
  2153.        end 1
  2154.    end if
  2155. end if
  2156.  
  2157. dim as Epub ep
  2158. if epub_load(ep, filename) <> 0 then
  2159.    print "failed to open epub"
  2160.    end 1
  2161. end if
  2162.  
  2163. ' Clamp g_resume_toc to the TOC bounds, if the TOC has any entries.
  2164. ' A bookmark written against a different epub, or a corrupt one,
  2165. ' could give a TOC index that is out of range for this book. That
  2166. ' only affects the display; it does not affect the resume position,
  2167. ' which is keyed by spine section.
  2168. if ep.ntoc > 0 andalso g_resume_toc >= ep.ntoc then
  2169.    g_resume_toc = 0
  2170. end if
  2171.  
  2172. select case cmd
  2173. case "meta"
  2174.    reset_pager()
  2175.    buf_append "title:  " & ep.title
  2176.    buf_append "author: " & ep.author
  2177.    page_loop(ep)
  2178.  
  2179. case "meta_print"
  2180.    print "title:  " & ep.title
  2181.    print "author: " & ep.author
  2182.    epub_close(ep)
  2183.    end 0
  2184.  
  2185. case "toc"
  2186.    dim as integer chosen = show_toc_screen(ep)
  2187.    if chosen < 0 then
  2188.        epub_close(ep)
  2189.        end 0
  2190.    end if
  2191.  
  2192.    dim as integer idx  = 0
  2193.    dim as integer skip = 0
  2194.    if resolve_toc_to_section(ep, chosen, idx, skip) <> 0 then
  2195.        print "cannot resolve toc entry " & str(chosen)
  2196.        epub_close(ep)
  2197.        end 1
  2198.    end if
  2199.  
  2200.    g_show_mode   = 1
  2201.    g_resume_file = filename
  2202.    g_resume_toc  = chosen
  2203.    g_resume_idx  = str(idx)
  2204.  
  2205.    load_section(ep, idx, skip)
  2206.    page_loop(ep)
  2207.  
  2208. case "toc_print"
  2209.    if ep.ntoc = 0 then
  2210.        print "(no toc found)"
  2211.    else
  2212.        for i as integer = 0 to ep.ntoc - 1
  2213.            dim as string label = ep.toc(i).label
  2214.            if label = "" then label = ep.toc(i).href
  2215.            print str(i) & "  " & label
  2216.        next
  2217.    end if
  2218.    epub_close(ep)
  2219.    end 0
  2220.  
  2221. case "goto_section"
  2222.    dim as integer tocidx = val(arg)
  2223.    if tocidx < 0 or tocidx >= ep.ntoc then
  2224.        print "toc index out of range (see `toc`)"
  2225.        epub_close(ep)
  2226.        end 1
  2227.    end if
  2228.  
  2229.    dim as integer idx = 0
  2230.    dim as integer skip = 0
  2231.    if resolve_toc_to_section(ep, tocidx, idx, skip) <> 0 then
  2232.        print "goto_section: no spine item matches " & ep.toc(tocidx).href
  2233.        epub_close(ep)
  2234.        end 1
  2235.    end if
  2236.  
  2237.    g_show_mode   = 1
  2238.    g_resume_file = filename
  2239.    g_resume_toc  = tocidx
  2240.    g_resume_idx  = str(idx)
  2241.  
  2242.    load_section(ep, idx, skip)
  2243.    page_loop(ep)
  2244.  
  2245. case "resume"
  2246.    ' Resume from bookmark, in spine terms. Used only by the
  2247.    ' no-command path, where the bookmark's second line is the
  2248.    ' spine section and the third is the skip.
  2249.    dim as integer idx = val(arg)
  2250.    dim as integer sk  = 0
  2251.    if skip_arg <> "" then
  2252.        sk = val(skip_arg)
  2253.        if sk < 0 then sk = 0
  2254.    end if
  2255.  
  2256.    if idx < 0 or idx >= ep.nspine then
  2257.        print "resume: stored section out of range"
  2258.        epub_close(ep)
  2259.        end 1
  2260.    end if
  2261.  
  2262.    g_show_mode   = 1
  2263.    g_resume_file = filename
  2264.    g_resume_idx  = str(idx)
  2265.  
  2266.    load_section(ep, idx, sk)
  2267.    page_loop(ep)
  2268.  
  2269. case "show"
  2270.    dim as integer idx = -1
  2271.    dim as integer sk  = 0
  2272.  
  2273.    if arg = "" then
  2274.        ' Resume from bookmark directly, in spine terms.
  2275.        dim as integer btoc = 0
  2276.        dim as integer bsec = 0
  2277.        dim as integer bskip = 0
  2278.        if bookmark_read(filename, btoc, bsec, bskip) = 0 then
  2279.            idx = bsec
  2280.            sk  = bskip
  2281.            g_resume_toc = btoc
  2282.            print "(resuming from bookmark: section " & str(idx) & _
  2283.                  ", line " & str(sk) & ")"
  2284.        else
  2285.            print "show: no argument given and no bookmark found"
  2286.            print "usage: epubrd <file.epub> show <toc-index> [skip]"
  2287.            epub_close(ep)
  2288.            end 1
  2289.        end if
  2290.    else
  2291.        ' Argument is a TOC index. Resolve to a section and an
  2292.        ' anchor-derived skip. If the entry has no anchor, use the
  2293.        ' caller's skip if given, otherwise 0.
  2294.        dim as integer tocidx = val(arg)
  2295.        if tocidx < 0 or tocidx >= ep.ntoc then
  2296.            print "show: toc index out of range (see `toc`)"
  2297.            epub_close(ep)
  2298.            end 1
  2299.        end if
  2300.  
  2301.        dim as integer asec  = 0
  2302.        dim as integer askip = 0
  2303.        if resolve_toc_to_section(ep, tocidx, asec, askip) <> 0 then
  2304.            print "show: cannot resolve toc entry " & str(tocidx)
  2305.            epub_close(ep)
  2306.            end 1
  2307.        end if
  2308.        idx = asec
  2309.  
  2310.        dim as string tpath = ""
  2311.        dim as string tfrag = ""
  2312.        split_href(ep.toc(tocidx).href, tpath, tfrag)
  2313.  
  2314.        if tfrag <> "" then
  2315.            ' Anchor present: TOC wins, ignore caller's skip.
  2316.            sk = askip
  2317.        else
  2318.            ' No anchor: use caller's skip if given.
  2319.            if skip_arg <> "" then
  2320.                sk = val(skip_arg)
  2321.                if sk < 0 then sk = 0
  2322.            else
  2323.                sk = 0
  2324.            end if
  2325.        end if
  2326.  
  2327.        g_resume_toc = tocidx
  2328.    end if
  2329.  
  2330.    if idx < 0 or idx >= ep.nspine then
  2331.        print "show: resolved section out of range"
  2332.        epub_close(ep)
  2333.        end 1
  2334.    end if
  2335.  
  2336.    g_show_mode   = 1
  2337.    g_resume_file = filename
  2338.    g_resume_idx  = str(idx)
  2339.  
  2340.    load_section(ep, idx, sk)
  2341.    page_loop(ep)
  2342.  
  2343. case "next_section"
  2344.    dim as integer start_toc = val(arg)
  2345.    if start_toc < 0 or start_toc >= ep.ntoc then
  2346.        print "next_section: toc index out of range (see `toc`)"
  2347.        epub_close(ep)
  2348.        end 1
  2349.    end if
  2350.  
  2351.    g_show_mode   = 1
  2352.    g_resume_file = filename
  2353.    ' No spine section is known yet; the loop below fills this in with a
  2354.    ' real spine index before any page is shown. Keeping -1 here stops a
  2355.    ' TOC index from ever landing in the bookmark's spine-section field
  2356.    ' when nothing could be resolved at all.
  2357.    g_resume_idx  = "-1"
  2358.  
  2359.    dim as integer first_sk = 0
  2360.    if skip_arg <> "" then
  2361.        first_sk = val(skip_arg)
  2362.        if first_sk < 0 then first_sk = 0
  2363.    end if
  2364.  
  2365.    dim as integer cur_toc = start_toc
  2366.    while cur_toc < ep.ntoc
  2367.        g_resume_toc = cur_toc
  2368.  
  2369.        ' Resolve this TOC entry to a section and anchor skip.
  2370.        dim as integer sec  = 0
  2371.        dim as integer askip = 0
  2372.        if resolve_toc_to_section(ep, cur_toc, sec, askip) <> 0 then
  2373.            cur_toc += 1
  2374.            continue while
  2375.        end if
  2376.  
  2377.        dim as string tpath = ""
  2378.        dim as string tfrag = ""
  2379.        split_href(ep.toc(cur_toc).href, tpath, tfrag)
  2380.  
  2381.        dim as integer sk = askip
  2382.        if tfrag = "" andalso cur_toc = start_toc andalso first_sk > 0 then
  2383.            sk = first_sk
  2384.        end if
  2385.  
  2386.        g_resume_idx = str(sec)
  2387.  
  2388.        load_section(ep, sec, sk)
  2389.  
  2390.        if g_page_count = 0 then
  2391.            cur_toc += 1
  2392.            continue while
  2393.        end if
  2394.  
  2395.        if g_interactive = 0 then
  2396.            for i as integer = g_page_start to g_page_count - 1
  2397.                print page_line(i)
  2398.            next
  2399.            cur_toc += 1
  2400.            continue while
  2401.        end if
  2402.  
  2403.        dim as integer jump_toc = -1
  2404.        dim as integer section_done = 0
  2405.  
  2406.        while section_done = 0
  2407.            draw_page()
  2408.  
  2409.            dim as integer shown_last = g_page_start + g_page_lines - 1
  2410.            if shown_last > g_page_count - 1 then shown_last = g_page_count - 1
  2411.  
  2412.            print
  2413.            print "-- [space]/[Enter]=next  [b]=back  [t]=toc  [q]=quit --"
  2414.  
  2415.            select case getkey
  2416.            case KEY_CTRL_C, KEY_Q_LOWER, KEY_Q_UPPER
  2417.                print_resume_hint()
  2418.                end 0
  2419.            case KEY_B_LOWER, KEY_B_UPPER
  2420.                if g_page_start > 0 then
  2421.                    g_page_start -= g_page_lines
  2422.                    if g_page_start < 0 then g_page_start = 0
  2423.                end if
  2424.            case KEY_ENTER, KEY_SPACE
  2425.                if shown_last >= g_page_count - 1 then
  2426.                    section_done = 1
  2427.                else
  2428.                    g_page_start = shown_last + 1
  2429.                end if
  2430.            case KEY_T_LOWER, KEY_T_UPPER
  2431.                dim as integer chosen = show_toc_screen(ep)
  2432.                if chosen >= 0 then
  2433.                    jump_toc = chosen
  2434.                    section_done = 1
  2435.                end if
  2436.            case else
  2437.                ' ignore
  2438.            end select
  2439.        wend
  2440.  
  2441.        if jump_toc >= 0 then
  2442.            cur_toc = jump_toc
  2443.            continue while
  2444.        end if
  2445.  
  2446.        cur_toc += 1
  2447.    wend
  2448.  
  2449.    print
  2450.    print "(end of book)"
  2451.    print_resume_hint()
  2452.  
  2453. case else
  2454.    show_usage()
  2455.  
  2456. end select
  2457.  
  2458. epub_close(ep)
  2459. end 0
Advertisement
Add Comment
Please, Sign In to add comment