From a5faa61f8c92cec6e6179c87f8a62ad4c9907478 Mon Sep 17 00:00:00 2001 From: Yusuke Hirao Date: Mon, 27 Jul 2026 10:52:58 +0900 Subject: [PATCH 1/3] feat(readtext): add position-aware list parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add toListWithPosition/readListWithPosition returning each surviving line's 1-origin line/column, so callers that reject invalid entries can report which line of the source file they came from — toList's plain string[] loses that mapping once blank/comment lines are filtered out. toList is reimplemented on top of the new parser to keep the filtering rules in one place. --- packages/@d-zero/readtext/src/list.ts | 2 + .../readtext/src/read-list-with-position.ts | 21 +++++++++ .../src/to-list-with-position.spec.ts | 44 +++++++++++++++++++ .../readtext/src/to-list-with-position.ts | 44 +++++++++++++++++++ packages/@d-zero/readtext/src/to-list.ts | 15 ++----- packages/@d-zero/readtext/src/types.ts | 13 ++++++ 6 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 packages/@d-zero/readtext/src/read-list-with-position.ts create mode 100644 packages/@d-zero/readtext/src/to-list-with-position.spec.ts create mode 100644 packages/@d-zero/readtext/src/to-list-with-position.ts diff --git a/packages/@d-zero/readtext/src/list.ts b/packages/@d-zero/readtext/src/list.ts index 8cad021b..64a0351d 100644 --- a/packages/@d-zero/readtext/src/list.ts +++ b/packages/@d-zero/readtext/src/list.ts @@ -1,3 +1,5 @@ export { readList } from './read-list.js'; +export { readListWithPosition } from './read-list-with-position.js'; export { toKvList } from './to-kv-list.js'; export { toList } from './to-list.js'; +export { toListWithPosition } from './to-list-with-position.js'; diff --git a/packages/@d-zero/readtext/src/read-list-with-position.ts b/packages/@d-zero/readtext/src/read-list-with-position.ts new file mode 100644 index 00000000..697545ea --- /dev/null +++ b/packages/@d-zero/readtext/src/read-list-with-position.ts @@ -0,0 +1,21 @@ +import fs from 'node:fs/promises'; + +import { toListWithPosition } from './to-list-with-position.js'; + +/** + * Reads a list file and returns each surviving line tagged with its + * 1-origin line/column position in the source file — the position-aware + * companion to `readList`, for callers that need to report which line of + * the file an invalid entry came from. + * @param filePath - The path to the file to read. + * @returns A promise that resolves to the surviving lines with position info. + * @example + * ```typescript + * const items = await readListWithPosition('/path/to/file.txt'); + * // items: [{ value: 'item1', line: 1, column: 1 }, ...] + * ``` + */ +export async function readListWithPosition(filePath: string) { + const fileContent = await fs.readFile(filePath, 'utf8'); + return toListWithPosition(fileContent); +} diff --git a/packages/@d-zero/readtext/src/to-list-with-position.spec.ts b/packages/@d-zero/readtext/src/to-list-with-position.spec.ts new file mode 100644 index 00000000..8186c55e --- /dev/null +++ b/packages/@d-zero/readtext/src/to-list-with-position.spec.ts @@ -0,0 +1,44 @@ +import { describe, test, expect } from 'vitest'; + +import { toListWithPosition } from './to-list-with-position.js'; + +describe('toListWithPosition', () => { + test('assigns 1-origin line numbers, skipping blank and comment lines', () => { + expect( + toListWithPosition(`item1 +# comment + +item2 +`), + ).toStrictEqual([ + { value: 'item1', line: 1, column: 1 }, + { value: 'item2', line: 4, column: 1 }, + ]); + }); + + test('reports the column where leading whitespace ends', () => { + expect(toListWithPosition(' item1\n\titem2\nitem3')).toStrictEqual([ + { value: 'item1', line: 1, column: 3 }, + { value: 'item2', line: 2, column: 2 }, + { value: 'item3', line: 3, column: 1 }, + ]); + }); + + test('treats a whitespace-only line as blank', () => { + expect(toListWithPosition('item1\n \nitem2')).toStrictEqual([ + { value: 'item1', line: 1, column: 1 }, + { value: 'item2', line: 3, column: 1 }, + ]); + }); + + test('strips the trailing \\r from CRLF line endings without shifting the column', () => { + expect(toListWithPosition('item1\r\n item2\r\n')).toStrictEqual([ + { value: 'item1', line: 1, column: 1 }, + { value: 'item2', line: 2, column: 3 }, + ]); + }); + + test('returns an empty array for text with no surviving lines', () => { + expect(toListWithPosition('\n# comment\n\n')).toStrictEqual([]); + }); +}); diff --git a/packages/@d-zero/readtext/src/to-list-with-position.ts b/packages/@d-zero/readtext/src/to-list-with-position.ts new file mode 100644 index 00000000..3b98343a --- /dev/null +++ b/packages/@d-zero/readtext/src/to-list-with-position.ts @@ -0,0 +1,44 @@ +import type { ListItem } from './types.js'; + +/** + * Splits text into non-empty, non-comment lines (same rules as `toList`), + * keeping each surviving line's 1-origin line/column position in the + * original text. + * + * `toList` discards this position by filtering before returning a plain + * `string[]` — a caller that needs to report "line 3 of the source file was + * invalid" back to a user cannot reconstruct it from the filtered array + * alone, since blank lines and `#` comments have already shifted the index. + * @param text - The raw text to split into lines. + * @returns Surviving lines with their original line/column position. + * @example + * ```typescript + * const items = toListWithPosition('item1\n# comment\n\n item2\n'); + * // items: [ + * // { value: 'item1', line: 1, column: 1 }, + * // { value: 'item2', line: 4, column: 3 }, + * // ] + * ``` + */ +export function toListWithPosition(text: string): ListItem[] { + const lines = text.split('\n'); + const items: ListItem[] = []; + + for (const [index, rawLine] of lines.entries()) { + const value = rawLine.trim(); + + // Empty (post-trim) and comment lines are dropped, matching `toList`. + if (value.length === 0 || value.startsWith('#')) { + continue; + } + + const leadingWhitespaceLength = rawLine.length - rawLine.trimStart().length; + items.push({ + value, + line: index + 1, + column: leadingWhitespaceLength + 1, + }); + } + + return items; +} diff --git a/packages/@d-zero/readtext/src/to-list.ts b/packages/@d-zero/readtext/src/to-list.ts index c21f9bd0..247c3023 100644 --- a/packages/@d-zero/readtext/src/to-list.ts +++ b/packages/@d-zero/readtext/src/to-list.ts @@ -1,18 +1,9 @@ +import { toListWithPosition } from './to-list-with-position.js'; + /** * * @param text */ export function toList(text: string) { - const lines = text.split('\n'); - - // Trim - const trimmedLines = lines.map((line) => line.trim()); - - // Remove empty lines - const nonEmptyLines = trimmedLines.filter((line) => line.length > 0); - - // Remove comments - const nonCommentLines = nonEmptyLines.filter((line) => !line.startsWith('#')); - - return nonCommentLines; + return toListWithPosition(text).map((item) => item.value); } diff --git a/packages/@d-zero/readtext/src/types.ts b/packages/@d-zero/readtext/src/types.ts index 794bb379..22eaf7cd 100644 --- a/packages/@d-zero/readtext/src/types.ts +++ b/packages/@d-zero/readtext/src/types.ts @@ -6,3 +6,16 @@ export type KeyValue = { key: string; value: string; }; + +/** + * A single non-empty, non-comment line surviving `toListWithPosition`'s + * filtering, tagged with its position in the original source text. + */ +export type ListItem = { + /** The trimmed line content (comments and blank lines already excluded). */ + value: string; + /** 1-origin line number in the original source text. */ + line: number; + /** 1-origin column of `value`'s first character in the original line (i.e. where leading whitespace ended). */ + column: number; +}; From 69841b224f25c4a12f02c1a0874a32c2a2861495 Mon Sep 17 00:00:00 2001 From: Yusuke Hirao Date: Mon, 27 Jul 2026 11:01:41 +0900 Subject: [PATCH 2/3] feat(readtext): re-export ListItem type from the list subpath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consumers that need to type an intermediate value as ListItem[] (e.g. a caller partitioning valid/invalid entries) could not name the type — list.ts re-exported the parsing functions but not their shared item type. --- packages/@d-zero/readtext/src/list.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@d-zero/readtext/src/list.ts b/packages/@d-zero/readtext/src/list.ts index 64a0351d..a0be9d29 100644 --- a/packages/@d-zero/readtext/src/list.ts +++ b/packages/@d-zero/readtext/src/list.ts @@ -3,3 +3,4 @@ export { readListWithPosition } from './read-list-with-position.js'; export { toKvList } from './to-kv-list.js'; export { toList } from './to-list.js'; export { toListWithPosition } from './to-list-with-position.js'; +export type { ListItem } from './types.js'; From a88f88afb0b521f6598599f6e20099d0b7f2b89e Mon Sep 17 00:00:00 2001 From: Yusuke Hirao Date: Mon, 27 Jul 2026 11:35:34 +0900 Subject: [PATCH 3/3] docs(readtext): document readListWithPosition in the package README README only covered readList/readGrid; the new position-aware companion is a main public API addition and was missing from the usage section. --- packages/@d-zero/readtext/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/@d-zero/readtext/README.md b/packages/@d-zero/readtext/README.md index a4c0594b..5e6e4df7 100644 --- a/packages/@d-zero/readtext/README.md +++ b/packages/@d-zero/readtext/README.md @@ -26,6 +26,19 @@ const list = await readList('path/to/file.txt'); const kv = await readList('path/to/file.txt', ' '); ``` +### `readListWithPosition` — 元の行番号・列番号付き + +`readList` と同じ空行・コメント除外規則だが、各要素の値に加えて元ファイル内の 1-origin の行番号・列番号を保持する。不正な行を警告表示する際など、元の位置に戻す必要がある場合に使う。 + +```ts +import { readListWithPosition } from '@d-zero/readtext/list'; + +const items = await readListWithPosition('path/to/file.txt'); +// items: [{ value: 'item1', line: 1, column: 1 }, ...] +``` + +文字列をそのまま渡したい場合は `toListWithPosition` を使う。 + ### `readGrid` — 区切り文字で 2D 配列 ```ts