markdown-table.test.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { parseMarkdownTable } from '../../../src/storage/parsers/markdown-table.js'
  4. import { serializeMarkdownTable } from '../../../src/storage/serializers/markdown-table.js'
  5. test('正常解析时间线表格', () => {
  6. const content = `| 章 | 书内时间 | 一句话事件 | 在场 |
  7. |----|----------|------------|------|
  8. | 1 | 1023春月初一 | 林晚入宗门 | 林晚 |
  9. | 2 | 1023春月初二 | 初遇神秘老者 | 林晚, 神秘老者 |`
  10. const result = parseMarkdownTable(content)
  11. assert.equal(result.ok, true)
  12. assert.deepEqual(result.headers, ['章', '书内时间', '一句话事件', '在场'])
  13. assert.equal(result.rows.length, 2)
  14. assert.equal(result.rows[0].章, '1')
  15. assert.equal(result.rows[0].书内时间, '1023春月初一')
  16. assert.equal(result.rows[1].在场, '林晚, 神秘老者')
  17. })
  18. test('正常解析名册表格', () => {
  19. const content = `| 正名 | 别名 | 类型 | 首现章 |
  20. |------|------|------|---------|
  21. | 林晚 | 晚晚, 林师妹 | character | 1 |
  22. | 神秘老者 | 黑衣人 | character | 1 |`
  23. const result = parseMarkdownTable(content)
  24. assert.equal(result.ok, true)
  25. assert.equal(result.rows.length, 2)
  26. assert.equal(result.rows[0].正名, '林晚')
  27. assert.equal(result.rows[0].别名, '晚晚, 林师妹')
  28. })
  29. test('容错:跳过空行', () => {
  30. const content = `| 章 | 事件 |
  31. |----|------|
  32. | 1 | 事件1 |
  33. | 2 | 事件2 |`
  34. const result = parseMarkdownTable(content)
  35. assert.equal(result.ok, true)
  36. assert.equal(result.rows.length, 2)
  37. })
  38. test('容错:单元格数量不匹配(补空)', () => {
  39. const content = `| A | B | C |
  40. |---|---|---|
  41. | 1 | 2 |`
  42. const result = parseMarkdownTable(content)
  43. assert.equal(result.ok, true)
  44. assert.equal(result.rows.length, 1)
  45. assert.equal(result.rows[0].A, '1')
  46. assert.equal(result.rows[0].B, '2')
  47. assert.equal(result.rows[0].C, '')
  48. })
  49. test('边界:少于两行', () => {
  50. const content = `| A | B |`
  51. const result = parseMarkdownTable(content)
  52. assert.equal(result.ok, false)
  53. assert.ok(result.error.includes('至少需要两行'))
  54. })
  55. test('边界:表头格式错误', () => {
  56. const content = `A | B
  57. |---|---|`
  58. const result = parseMarkdownTable(content)
  59. assert.equal(result.ok, false)
  60. assert.ok(result.error.includes('必须以 | 开头'))
  61. })
  62. test('单字符分隔符 |-| 也要接受(GFM 合法)', () => {
  63. const content = `| 正名 | 别名 | 类型 | 首现章 |
  64. |--|--|--|--|
  65. | 林晚 | 晚晚 | character | 1 |`
  66. const result = parseMarkdownTable(content)
  67. assert.equal(result.ok, true, result.error)
  68. assert.equal(result.rows.length, 1)
  69. assert.equal(result.rows[0].正名, '林晚')
  70. assert.equal(result.rows[0].别名, '晚晚')
  71. })
  72. test('对齐分隔符 |:--:|--:|:--| 也要接受', () => {
  73. const content = `| A | B | C |
  74. |:--|--:|:--:|
  75. | 1 | 2 | 3 |`
  76. const result = parseMarkdownTable(content)
  77. assert.equal(result.ok, true, result.error)
  78. assert.equal(result.rows[0].B, '2')
  79. })
  80. test('分隔符格数与表头不一致 → 报错', () => {
  81. const content = `| A | B | C |
  82. |---|---|
  83. | 1 | 2 | 3 |`
  84. const result = parseMarkdownTable(content)
  85. assert.equal(result.ok, false)
  86. assert.ok(result.error.includes('分隔符'))
  87. })
  88. test('分隔符含非 -: 字符 → 报错', () => {
  89. const content = `| A | B |
  90. |--x--|---|
  91. | 1 | 2 |`
  92. const result = parseMarkdownTable(content)
  93. assert.equal(result.ok, false)
  94. assert.ok(result.error.includes('分隔符'))
  95. })
  96. // —— 批 2 回归:R11 / A7 / A8 转义读写对称 ——
  97. test('R11:单元格含竖线/反斜杠 → 序列化转义,读回原值不丢', () => {
  98. const rows = [
  99. { 正名: '老王', 别名: '刀疤|老王', 类型: '角色', 首现章: '1' },
  100. { 正名: '甲', 别名: '路径\\子目录', 类型: '角色', 首现章: '2' },
  101. { 正名: '乙', 别名: '字面\\|序列', 类型: '角色', 首现章: '3' },
  102. ]
  103. const text = serializeMarkdownTable(['正名', '别名', '类型', '首现章'], rows)
  104. const parsed = parseMarkdownTable(text)
  105. assert.equal(parsed.ok, true, parsed.error)
  106. assert.equal(parsed.rows[0].别名, '刀疤|老王', '竖线后半截不能丢')
  107. assert.equal(parsed.rows[1].别名, '路径\\子目录')
  108. assert.equal(parsed.rows[2].别名, '字面\\|序列')
  109. assert.equal(parsed.rows.length, 3)
  110. })
  111. test('R11:单元格含换行 → 替换为空格,表不散架', () => {
  112. const text = serializeMarkdownTable(['章', '一句话事件'], [{ 章: '1', 一句话事件: '第一行\n第二行' }])
  113. const parsed = parseMarkdownTable(text)
  114. assert.equal(parsed.ok, true)
  115. assert.equal(parsed.rows.length, 1)
  116. assert.equal(parsed.rows[0].一句话事件, '第一行 第二行')
  117. })
  118. test('A7:整表全角管道 | → 归一解析,不再静默变空', () => {
  119. const content = '| 正名 | 别名 |\n|---|---|\n| 林晚 | 晚晚 |'
  120. const result = parseMarkdownTable(content)
  121. assert.equal(result.ok, true, result.error)
  122. assert.equal(result.rows.length, 1)
  123. assert.equal(result.rows[0].正名, '林晚')
  124. assert.equal(result.rows[0].别名, '晚晚')
  125. })
  126. test('A8:数据行格数多于表头 → 截断但记 warning,不再静默', () => {
  127. const content = '| A | B |\n|---|---|\n| 1 | 2 | 3 |'
  128. const result = parseMarkdownTable(content)
  129. assert.equal(result.ok, true)
  130. assert.equal(result.rows[0].B, '2')
  131. assert.ok(result.warnings.some((w) => w.includes('多于表头')), JSON.stringify(result.warnings))
  132. })
  133. test('A11(serialize 半边):行对象带表头之外的额外列 → 追加尾列,不丢', () => {
  134. const text = serializeMarkdownTable(
  135. ['正名', '别名'],
  136. [{ 正名: '林晚', 别名: '晚晚', 备注: '主角' }]
  137. )
  138. const parsed = parseMarkdownTable(text)
  139. assert.deepEqual(parsed.headers, ['正名', '别名', '备注'])
  140. assert.equal(parsed.rows[0].备注, '主角')
  141. })