Counting the lines of two TXT files first
The starting point is plain: two TXT files, one with 555 segments and one with 819. What I want is not their content but the books the line diff page keeps when a whole corpus is poured into it. How common lines are counted, which side of the added-and-removed pair comes first, and how many numbers the stats line actually writes. The two files serve only as bytes and lines.
Bytes and segments of the corpus
Both are published site resources: Shenglü Qimen 《声律启蒙》 at 26353 bytes, 9185 code units, 9183 code points, 555 segments, 403 non-empty lines and 152 blank ones; and Liweng Duiyun 《笠翁对韵》 at 28903 bytes, 10211 code units, 10209 code points, 819 segments, 671 non-empty lines and 148 blank ones. Neither carries CRLF and neither carries a BOM. Code units run 2 above code points in each file because each holds 2 supplementary-plane code points. The page under test is the text diff, whose template is 118 lines and 4487 bytes with 3 script functions.
The three entries of a whole-file run
Feeding both full texts straight into lineDiff gives added 665, removed 401, equal 154, a result of 1220 lines, and a dp table of 556 × 820 = 455920 cells. The price of that scale was computed in an older article, so this one only registers the tier. The identities: 555 minus 154 is 401, 819 minus 154 is 665, and 555 plus 819 minus 154 is 1220. Swapping the two sides trades the added and removed counts, while equal and the cell count do not move.
| input tier | A segments | B segments | removed | added | equal | result lines |
|---|---|---|---|---|---|---|
| whole files as they come | 555 | 819 | 401 | 665 | 154 | 1220 |
| blank lines stripped first | 403 | 671 | 397 | 665 | 6 | 1068 |
| each file against itself | 555 and 819 | same as left | 0 | 0 | 555 and 819 | same as left |
Both blank tiers report the same added of 665, and the whole gap sits in removed: of the 152 blank lines on side A, 148 are absorbed and 4 reach the removals, so removed is 401. The dp table of the non-empty run is 404 × 672 = 271488 cells. Timing is a script-level fact: 5 back-to-back calls returned 113.9, 139.5, 120.5, 122.7 and 117.6 ms with a median of 120.5 ms, and that figure is one function call, not a page response.
The guard fires only when both sides are empty
Inside doDiff() each side is joined and then trimmed, and only when both come back empty does the page raise the toast 「请输入要对比的文本」 and return: the test is on the pair, not on either side alone. Four measured cases:
- Both sides empty, or 3 half-width spaces against nothing: the guard fires, nothing changes, and the stats and the rendered lines freeze on the previous round.
- One side empty against 403 non-empty corpus lines: it runs, and the stats read 「新增 403 行,删除 1 行」, added 403 and removed 1.
- Moving those 403 lines to side A gives the mirror image 「新增 1 行,删除 403 行」, added 1 and removed 403.
- One side empty against all 555 segments: 「新增 554 行,删除 0 行」, added 554 and removed 0.
The last case: splitting an empty string yields 1 segment, and if the other side has no blank lines that empty segment becomes a removal of 1. Against the 152 blank rows it is absorbed into the LCS, removals fall to 0, and added rises to 554.
Figure 1: the two guard tiers and how blank lines rewrite the removals.
The tie-break fixes deletes before adds
The backtrack condition reads dp[i][j-1] >= dp[i][j]: on a tie it steps into the added branch first and decrements j, the walk runs from the far end of the table backwards, and a reverse() at the end flips the stream, so the output always prints removals before additions:
- a, b against b, a yields removed a, equal b, added a; after
doDiff, side A shows a minus line for a, b, and a placeholder, while side B shows a placeholder, b, and a plus line for a. - old against new yields one adjacent removed-and-added pair; a, b against a, c yields equal, removed, added.
- 1, 2, 3, 4 against 3, 4, 1, 2 yields removed 1 and 2, equal 3 and 4, added 1 and 2: the common block anchors on 3 and 4.
In a separate context where only >= becomes >, 4 of the same sample groups flip their order, the block swap re-anchors on 1 and 2, and one group stays byte-for-byte the same, a control record only.
Moving a line: take 10 lines and reinsert line 2 at position 9. Against itself the run is 0 added and 0 removed; after the move it is added 1, removed 1, equal 9, a result of 11 lines. There is no move detection in the code.
The stats line carries only two numbers
The page builds one template string, 新增 ${added} 行,删除 ${removed} 行, and extracting its digits yields exactly 2, so the whole-file run prints added 665 and removed 401 and nothing else. Neither the common-line count nor the LCS length at the end of the dp table enters the string. The copy above the input boxes, 「统计概览:显示新增行数、删除行数、修改行数」, promises a modified count the code cannot produce: the branch that counts modifications runs only when a line type is none of equal, removed and added, while the three push sites write exactly those three literals, and across all 1220 result lines the type set stays at three. Changing a few characters inside a line still moves the whole line onto both sides, and there is no word-level layer here, as an older article logged.
How result lines reach the DOM, and what survives clearing
Every result line is built with createElement and filled through textContent: 1220 divs per side, 2440 in total, all inserted in one pass. The container is max-height 400px with overflow auto, and the control run measured a scrollHeight of 29328px against a client height of 400px. The page ships no escaping helper and no copy button, just 2 matches of <button in the template, so a line containing angle brackets, an &, or quotes is written through textContent and reads back exactly as typed. The counterpart placeholder is a single U+00A0. Clicking 「清空」 only flips the container back to hidden, and 2 lines survive on each side.
CRLF has two layers, reviewed in one line: at the string layer, a line carrying \r against its bare twin is one removal plus one addition; through a textarea the carriage return is normalized into a line feed, and the pasted text runs with 0 additions and 0 removals. Trailing LF is its own entry: splitting leaves 1 extra empty segment on each side, and the file measured against its trailing-LF-trimmed copy gives equal 554, removed 1, added 0.
Figure 2: the three entries of the whole-file run.
Three readings of the common lines, and where they sit
The common lines admit three readings. As a multiset including blanks there are 154: the leaner file contributes 148 blank rows and only 6 non-empty lines survive the intersection. Counting distinct strings gives 6 kinds, or 5 once the empty string is dropped, and the separator made of 28 code units appears twice on each side. Counting identical lines at identical positions leaves only 25. Those 6 shared strings sit at the tails of both files, inside the explanatory block appended to each download: lines 544 to 553 in the first file and lines 808 to 817 in the second, and the strings themselves stay out of this article. The LCS of the whole run is exactly 154, and the LCS of the non-empty run is exactly 6. That these match the multiset intersections follows only from the two sides keeping the same order: it is a coincidence measured once, not an identity.
Frequently asked questions
Q: if I leave one side completely blank, does the page stop me with an error?
No. The guard fires only when both sides together are blank. One empty side against 403 non-empty lines runs anyway, reporting added 403 and removed 1.
Q: why do both the removals and the additions grow when I moved a single line?
Because the result set has no move type. lineDiff only emits equal, added and removed, so a moved line is booked as one addition and one removal.
Q: 154 common lines are counted, so why does the stats bar not show them?
The stats string holds only two numbers, added 665 and removed 401. The common-line count and the LCS length never enter it.
Resource notes
The two resource links exist only to name where the corpus came from. Both books are used here as bytes and lines and nothing else: this article quotes no sentence from them, offers no reading of their content, and treats the 6 shared tail strings purely as line-count bookkeeping. The page has no file-input channel, so whole files can only arrive by pasting.