Two whole books must become one file before the next step means anything
When classic TXT moves further down the pipeline, the first step is usually concatenation rather than algorithm: one book per file means every split and search runs twice. This round goes straight for whole books. The template txt-merge.html measured 10126 bytes over 234 lines as it stands; its script section, 6844 bytes across 169 lines, went into node:vm exactly as stored, with no source line rewritten, and the DOM was stubbed around the 9 ids the page reads. The run was pure Node, with no browser opened.
Both samples are public-domain classics: the Guwen Guanzhi copy is 445453 bytes, holds 2089 LF, splits into 2090 segments with string length 150341; the Six Records of a Floating Life copy is 110280 bytes, with 244 LF, 245 segments and string length 36958. Both went into the text merge page in one drop, the dropdown left on its default tier.
Panel, disk, per-book segmentation: three rulers, three readings
The statistics bar reports file count 2, total lines 2334 and total characters 187299; the same artifact on disk is 555733 bytes, and a plain line count returns only 2333. Not a sum: the two books contribute 2335 segments and the merged panel sits 1 below that, while their LF adds to 2333 and the merged file sits 1 above. Across the nine tiers probed the panel count always exceeds the disk count by 1, because that field runs result.split('\n').length and the empty segment after the final newline still takes a slot. Bytes and characters are pure addition instead: the books summed alone and the merged artifact differ by 0 at both layers, and the byte-to-character ratio is 2.9671.
Figure 1: segments, LF, panel lines and disk lines crowd into the three cells from 2333 to 2335.
Four separator tiers: what each adds in lines, characters and bytes
None of the four options carries selected, so the browser lands on the first entry, no separator, and getSeparator (L168–L175) returns an empty string. Lines depend only on how many newlines the separator carries: panel lines = the LF sum + (n−1) × LF(separator) + 1, and ticking the name marker layers another n on top. Over the 13 tier combinations tested the prediction and the panel reading agreed character for character.
| Tier | Panel total lines | Total characters | Bytes on disk |
|---|---|---|---|
| No separator | 2334 | 187299 | 555733 |
| Line break | 2335 | 187300 | 555734 |
| Blank line | 2336 | 187301 | 555735 |
| Custom three hyphens | 2334 | 187302 | 555736 |
| Blank line plus marker | 2338 | 187343 | 555793 |
An empty custom box is not a fallback: that branch takes the empty string as it is, so the output matches the no-separator one byte for byte. A separator without a newline adds characters but no lines - three hyphens add 3 characters and 0 lines, and the seam becomes 12 code units wide; four equals add 4 characters and 0 lines.
The slot that is missing is not the trailing newline
Both books end on byte 0x0A with no carriage return, so the seam is character for character the first line of the Six Records file, 9 code units standing alone. Cutting the last byte off each book produced A2 (445452 bytes, LF 2088, 2089 segments) and B2 (110279 bytes, LF 243, 244 segments); the four pairings read 2334 / 2333 / 2333 / 2332 on the no-separator tier, each 1 below its own two-book segment sum, 0 on the line-break tier and +1 on the blank-line tier. The last byte changes the seam's content instead: with the book that lost its newline first, the seam fuses into 55 code units, that is the previous last line's 46 welded onto 9 more, and three hyphens in front push it to 58.
The file-name marker: every block is 13 plus the name length
The marker string '===== ' + name + ' =====\n' (L186) carries 1 newline of its own, so each block costs 13 code units plus the name length; the checkbox ships without checked, so it is off by default. Four naming schemes went through the same merge:
- Book names in Chinese, 8 code units each: 21 per block, the two blocks adding 2 lines, 42 characters and 58 bytes, since each Han character costs 3 bytes
- Pinyin names of 17 code units each: 60 across the two blocks
- 8 code units paired with 34: 68
- Names with a directory prefix, 20 code units each: 66
The line increment stays at 2, while the byte increment differs by scheme.
Batches, repeated drops, and nothing updating live
A single book reads the same on all four tiers, because joining a one-element array never inserts a separator: 2090 lines, 150341 characters, 445453 bytes. Six parts, each book three times, bring the LF sum to 6999, so the no-separator merge reads 7000 / 561897 / 1667199 and the blank line plus the marker reads 7016 / 562033 / 1667383. On that tier the panel minus the sum of the separate files is always the negative of n−1. Re-dropping a file still enlists it, since push does not de-duplicate, and the list refreshes once per batch, so three clicks on merge merely overwrite the statistics bar three times.
Change the dropdown, fill the custom box, tick the checkbox: the result area does not move. Those controls carry 0 listeners; only another click on merge moves the readout to 2338 lines and 187343 characters. Add a file after a merge and the queue holds 3 while the panel still reports 2.
Encoding, and a few failure paths
The first 3 bytes of both books are e3 80 90, the encoding of 【, and neither carries a byte order mark. A strict decode passes in one go, a lenient one yields 0 replacement characters, so neither account can arise here. Clicking merge with an empty list raises only the toast 请先添加文件. A 0-byte file merged alone gives the contradictory reading of 1 file, 0 lines and 0 characters. A mixed batch with one success and one failure leaves a single row while the toast still claims 2 files added, since the number printed is the number submitted. In the script section the byte threshold, 1024, max= and the truncating slice all score 0 hits, and the whole 1667199-byte artifact goes to the result box in one assignment.
Structure the books themselves give up
In the Guwen Guanzhi file, 238 lines are bracket markers standing alone, and the two bracket code points each occur 238 times, cross-checking the total: 12 volume headers, 4 book-opening markers, 222 piece titles. The headers fall at lines 44, 248, 438, 590, 713, 806, 904, 1020, 1136, 1292, 1510 and 1832; cutting on those 12 lines and counting titles volume by volume gives the distribution below.
Figure 2: the twelve volumes bar by bar, the third and ninth sharing the two tallest bars at 22.
The Six Records file carries only 4 marker lines, at 1, 54, 91 and 148, each 9 code units, marking the four parts inside it. Two pages of the same family share a ruler: the character field there and the character cell here both follow the JavaScript string-length definition.
Against the list copy, 1 of the 13 judgements stands inverted
Judging the txt-merge entry of config/tools.php from line 1608 onward gives 7 agreements, 5 partial agreements and 1 conflict. Three of the partial ones concern accounting: the title names only TXT while the accept attribute admits 11 types; the separator sentence omits the default tier and the missing fallback of an empty custom box; the batch wording reaches only to front-end concatenation. The conflict is precisely the live preview claim. Marked not measured, with reason: what the browser's own decoder returns from the literal bytes, what a file name measures under a disk encoding, how long writing 187299 code units into the box takes, and how tall that box renders.
Frequently asked questions
Q: Why is the panel line count always 1 above the disk count?
Because that cell counts segments after splitting on the newline, and the empty segment after the final newline claims a slot too.
Q: If the separator is custom but the box is empty, does it fall back to no separator?
The result matches, but it is not a fallback: that branch reads the empty string as it is, with nothing behind it, so the output is byte for byte the no-separator one - 555733 bytes, 2334 lines, 187299 characters, all equal.
Q: Does the result area change when the tier or the marker changes?
It does not. Those controls carry no listeners, so after a change you must click merge again to move from 2334 lines to 2338 lines and 187343 characters.