Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5ee98c0
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ์ถ”๊ฐ€
seonghobae Aug 15, 2026
0795997
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ์ถ”๊ฐ€
seonghobae Aug 15, 2026
d257bc1
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ์ถ”๊ฐ€
seonghobae Aug 15, 2026
0a606d8
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ์ถ”๊ฐ€
seonghobae Aug 15, 2026
4c35f1a
test(security): preserve padded sensitive entry identity
seonghobae Aug 16, 2026
d405c3e
fix(security): retain exact padded entry names
seonghobae Aug 16, 2026
2def4a1
test(security): use JUnit 4 padded-name fixture
seonghobae Aug 16, 2026
5c5d99a
docs: make whitespace examples markdownlint-safe
seonghobae Aug 16, 2026
b5e84f4
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ๋ฐ ๋ณด์•ˆ/์ ‘๊ทผ์„ฑ ๊ฐœ์„  ์‚ฌํ•ญ ๋ณ‘ํ•ฉ
seonghobae Aug 18, 2026
075e8b2
test(a11y): require isolated bidi entry labels
seonghobae Aug 27, 2026
79f92bc
test(security): preserve padded sensitive identity
seonghobae Aug 27, 2026
4e29074
test(a11y): bind focus styling to visible entry text
seonghobae Aug 27, 2026
ada594b
fix(a11y): isolate bidi labels and exact exclusions
seonghobae Aug 27, 2026
f52e000
docs(a11y): trace bidi isolation and exact-name boundary
seonghobae Aug 27, 2026
17aa965
docs(changelog): record bidi and exact-name repairs
seonghobae Aug 27, 2026
3dc61ba
merge(master): record translatable-label reconciliation
seonghobae Aug 27, 2026
76a4dfb
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ๋ฐ ์ตœ์‹  ๊ฐœ์„  ์‚ฌํ•ญ ๋ณ‘ํ•ฉ
seonghobae Aug 27, 2026
5e71c87
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ๋ฐ ์ตœ์‹  ๊ฐœ์„  ์‚ฌํ•ญ ๋ณ‘ํ•ฉ
seonghobae Aug 27, 2026
defdbfd
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ๋ฐ ์ตœ์‹  ๊ฐœ์„  ์‚ฌํ•ญ ๋ณ‘ํ•ฉ
seonghobae Aug 27, 2026
73c74bb
๐ŸŽจ Palette: ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ๋ฐ ์ตœ์‹  ๊ฐœ์„  ์‚ฌํ•ญ ๋ณ‘ํ•ฉ
seonghobae Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@
**Learning:** ํŒŒ์ผ ์„œ๋ฒ„ ํƒ์ƒ‰ ์‹œ `<title>` ์š”์†Œ์— ๋‹จ์ˆœํžˆ ๋””๋ ‰ํ† ๋ฆฌ ์ด๋ฆ„๋งŒ ์ œ๊ณตํ•˜๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ํƒญ์„ ์—ด์–ด๋‘์—ˆ๊ฑฐ๋‚˜ ํ™”๋ฉด ํŒ๋…๊ธฐ๋ฅผ ํ†ตํ•ด ์ฐฝ์„ ์ „ํ™˜ํ•  ๋•Œ ํ•ด๋‹น ํŽ˜์ด์ง€๊ฐ€ '๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก'์ž„์„ ์ฆ‰์‹œ ์ธ์ง€ํ•˜๊ธฐ ์–ด๋ ค์›Œ ์ ‘๊ทผ์„ฑ ๋ฐ ์‚ฌ์šฉ์„ฑ์ด ์ €ํ•˜๋ฉ๋‹ˆ๋‹ค.
**Action:** ํ•ญ์ƒ `<title>` ์š”์†Œ์— ํŽ˜์ด์ง€์˜ ๋ชฉ์ ์„ ๋ช…ํ™•ํžˆ ์•Œ ์ˆ˜ ์žˆ๋Š” ์ ‘๋ฏธ์‚ฌ(์˜ˆ: " - ๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก")๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ, ๋ธŒ๋ผ์šฐ์ € ํƒญ ์ด๋ฆ„๋งŒ์œผ๋กœ๋„ ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์‹ญ์‹œ์˜ค.

## 2026-08-17 - ๋ธŒ๋ผ์šฐ์ € ๋ฒˆ์—ญ๊ณผ ํ™”๋ฉด ํŒ๋…๊ธฐ์˜ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•œ ํ…์ŠคํŠธ ์ฒ˜๋ฆฌ
**Learning:** `aria-label` ์†์„ฑ์œผ๋กœ ์ง€์ •๋œ ํ™”๋ฉด ํŒ๋…๊ธฐ์šฉ ๋Œ€์ฒด ํ…์ŠคํŠธ๋Š” Chrome Translate ๋“ฑ ๋ธŒ๋ผ์šฐ์ € ๋ฒˆ์—ญ ๋„๊ตฌ์— ์˜ํ•ด ๋ฒˆ์—ญ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ฌธ์„œ ์–ธ์–ด๊ฐ€ ๋ณ€ํ™˜๋˜์–ด๋„ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”์—์„œ๋Š” ์›๋ณธ ์–ธ์–ด(์˜ˆ: ์˜์–ด)๋กœ ์ฝํ˜€ ๋‹ค๊ตญ์–ด ์ ‘๊ทผ์„ฑ์ด ์ €ํ•˜๋ฉ๋‹ˆ๋‹ค.
**Action:** ํ™”๋ฉด ํŒ๋…๊ธฐ๋ฅผ ์œ„ํ•œ ์ˆจ๊ฒจ์ง„ ์„ค๋ช… ํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•  ๋•Œ `aria-label` ๋Œ€์‹  CSS `.visually-hidden` ํด๋ž˜์Šค๋ฅผ ์ ์šฉํ•œ `<span>` ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ผ๋ฐ˜ ํ…์ŠคํŠธ๋กœ ์ธ์‹ํ•˜๊ณ  ๋ฒˆ์—ญํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์—ฌ ๋‹ค๊ตญ์–ด ์ ‘๊ทผ์„ฑ ํ˜ธํ™˜์„ฑ์„ ํ™•๋ณดํ•˜์‹ญ์‹œ์˜ค.
## 2024-08-17 - ํ…์ŠคํŠธ ์ •๋ ฌ์„ ์œ„ํ•œ ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(BiDi) ์ง€์› ์ถ”๊ฐ€
**Learning:** ํŒŒ์ผ ์„œ๋ฒ„ ํƒ์ƒ‰ ์‹œ ๋””๋ ‰ํ† ๋ฆฌ ๋˜๋Š” ํŒŒ์ผ ์ด๋ฆ„์ด ์šฐ์ธก์—์„œ ์ขŒ์ธก์œผ๋กœ(Right-To-Left) ์ฝํžˆ๋Š” ๋ฌธ์ž(์˜ˆ: ์•„๋ž์–ด, ํžˆ๋ธŒ๋ฆฌ์–ด ๋“ฑ)๋กœ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ ์„ž์—ฌ ์žˆ์„ ๋•Œ, ๊ธฐ๋ณธ ์ขŒ์ธก์—์„œ ์šฐ์ธก์œผ๋กœ(Left-To-Right) ํ๋ฆ„ ์•ˆ์—์„œ๋Š” ๋ ˆ์ด์•„์›ƒ๊ณผ ํ…์ŠคํŠธ๊ฐ€ ๊นจ์ ธ ๋ณด์ด๊ณ  ๊ฐ€๋…์„ฑ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
**Action:** ํ•ญ์ƒ ํŒŒ์ผ ์ด๋ฆ„, ๋””๋ ‰ํ† ๋ฆฌ ์ด๋ฆ„, ์„ค๋ช… ๋“ฑ ๋™์  ํ…์ŠคํŠธ๊ฐ€ ํฌํ•จ๋œ ์—˜๋ฆฌ๋จผํŠธ(`<h1>`, `<span>`)์— `dir="auto"` ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ•ด๋‹น ์ฝ˜ํ…์ธ ์˜ ์–ธ์–ด ํŠน์„ฑ์— ๋งž์ถฐ ํ…์ŠคํŠธ ๋ฐฉํ–ฅ์„ ์ž๋™์œผ๋กœ ์ ์ ˆํ•˜๊ฒŒ ๋ Œ๋”๋งํ•˜๋„๋ก ๋ณด์žฅํ•˜์‹ญ์‹œ์˜ค.
8 changes: 8 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,11 @@
**Root cause:** The protected implementation added canonical names to the exclusion set but did not compare each observed directory entry through a locale-stable normalized key.
**Prevention:** Build one `Locale.ROOT` lowercase set from the canonical sensitive names, compare every observed name against it, and add the original spelling to the exclusion set so downstream exact membership remains correct.
**Evidence:** `testProcessIgnoreFileTreatsSensitiveNamesCaseInsensitively` failed on test-only commit `472b916cd40f70693c4e1eb48956042a25353feb` (CI run `31469596932`) and passed with the source fix at `bb113d858ccfc42ddaecf6729749b238e5ade2d0` (CI run `31469921661`).

## 2026-08-16 - ๊ณต๋ฐฑ ์šฐํšŒ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ํŒŒ์ผ ์ด๋ฆ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
**ํ•™์Šต:** ๋ณด์•ˆ ๋ชฉ์ ์œผ๋กœ ํŒŒ์ผ ์ด๋ฆ„์ด๋‚˜ ํ™•์žฅ์ž๋ฅผ ๋ฏผ๊ฐํ•œ ํŒŒ์ผ ๋ชฉ๋ก(์˜ˆ: `defaultSensitiveFiles`)๊ณผ ๋น„๊ตํ•˜์—ฌ ์ œ์™ธํ•  ๋•Œ, ํ™”์ดํŠธ์ŠคํŽ˜์ด์Šค(๊ณต๋ฐฑ ๋“ฑ)๊ฐ€ ํฌํ•จ๋œ ์ด๋ฆ„(์˜ˆ: ` .git` ๋˜๋Š” `config.json `)์€ ๋‹จ์ˆœ ๋ฌธ์ž์—ด ๋น„๊ต๋ฅผ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
**์กฐ์น˜:** ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ๋‚˜ ํ•„ํ„ฐ๋ง ๊ทœ์น™๊ณผ ๋น„๊ตํ•˜๊ธฐ ์ „์— ํŒŒ์ผ ์ด๋ฆ„์— ๋Œ€ํ•ด `.trim()`์„ ํ˜ธ์ถœํ•˜์—ฌ, ์šฐ๋ฐœ์ ์ด๊ฑฐ๋‚˜ ์•…์˜์ ์ธ ํ™”์ดํŠธ์ŠคํŽ˜์ด์Šค ํŒจ๋”ฉ์œผ๋กœ ์ธํ•œ ์šฐํšŒ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์ •ํ™•ํ•œ ๋ณด์•ˆ ์ •์ฑ…์„ ์ ์šฉํ•˜์‹ญ์‹œ์˜ค.

## 2026-08-16 - ๋””๋ ‰ํ† ๋ฆฌ ์ˆœํšŒ ๊นŠ์ด ํ•˜๋“œ ๋ฆฌ๋ฐ‹(Hard Limit) ์ถ”๊ฐ€
**ํ•™์Šต:** ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ˆœํšŒํ•  ๋•Œ ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ •ํ•œ ๊นŠ์ด(`maxLevel`)๊ฐ€ ๋ฌด์ œํ•œ(`-1`)์ธ ๊ฒฝ์šฐ, ์•…์˜์ ์œผ๋กœ ๋˜๋Š” ์‹ค์ˆ˜๋กœ ์ƒ์„ฑ๋œ ๋งค์šฐ ๊นŠ์€ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋‚˜๋ฉด ์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ๊ณ ๊ฐˆ(Resource Exhaustion)์ด๋‚˜ ์Šคํƒ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
**์กฐ์น˜:** ๋ฌด์ œํ•œ ๊นŠ์ด๋ฅผ ํ—ˆ์šฉํ•˜๋Š” ์˜ต์…˜์ด ์žˆ๋”๋ผ๋„ ๋ฐฑ์—”๋“œ ๋ฐฉ์–ด(defense-in-depth) ์ฐจ์›์—์„œ ์‹œ์Šคํ…œ์˜ ์•ˆ์ „์„ ๋ณด์žฅํ•˜๋Š” ์ตœ๋Œ€ ํ—ˆ์šฉ ๊นŠ์ด(์˜ˆ: `MAX_SAFE_DEPTH = 100`)๋ฅผ ์ƒ์ˆ˜๋กœ ์ •์˜ํ•˜์—ฌ ๋ฃจํ”„ ๋˜๋Š” ์žฌ๊ท€ ํ˜ธ์ถœ ์‹œ ์ฒดํฌํ•จ์œผ๋กœ์จ ๋ฆฌ์†Œ์Šค ๊ณ ๊ฐˆ์„ ์‚ฌ์ „์— ์ฐจ๋‹จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
25 changes: 16 additions & 9 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ li + li {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
Expand Down Expand Up @@ -109,6 +109,7 @@ li + li {

private val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8)))
private val FILE_NAME_COMPARATOR = compareBy<File> { it.name }
private const val MAX_SAFE_DEPTH: Int = 100 // Defense-in-depth: hard limit on directory traversal to prevent resource exhaustion

class Html4tree : CliktCommand() {
val maxLevel:Int by option(help="Number of levels deep for which to generate an index.html file", hidden = false).int().default(-1)
Expand Down Expand Up @@ -184,6 +185,11 @@ internal fun crawl_directories(
}

val currentLevel: Int = lle.level
// Defense-in-depth: prevent excessive resource consumption by limiting directory traversal depth
if (currentLevel >= MAX_SAFE_DEPTH) {
lle = ll.pull()
continue
}

// โšก Bolt Performance Optimization: ๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก์„ ์บ์‹ฑํ•˜์—ฌ ์ค‘๋ณต๋œ I/O ์‹œ์Šคํ…œ ํ˜ธ์ถœ์„ ์ค„์ž„
val dirFiles = listFiles(lle.file)
Expand Down Expand Up @@ -326,7 +332,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S
// โšก Bolt Performance Optimization: ๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก์„ Set์— ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด ํ•„ํ„ฐ๋ง๋งŒ ํ•  ๋•Œ๋Š” ์ •๋ ฌ์ด ๋ถˆํ•„์š”ํ•˜๋ฏ€๋กœ .sorted()๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ O(N log N) ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
val list = dirFilesNames ?: curr_dir.list()
list?.forEach {
val current = it
val current = it.trim()
val pathCurrent = try {
java.nio.file.Paths.get(current)
} catch (_: java.nio.file.InvalidPathException) {
Expand All @@ -351,16 +357,17 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S

// ๋ณด์•ˆ ํ–ฅ์ƒ: dot-like prefixes and case variants of known sensitive names are excluded.
(dirFilesNames ?: curr_dir.list())?.forEach {
val normalizedName = it.toLowerCase(java.util.Locale.ROOT)
val fileName = it.trim()
val normalizedName = fileName.toLowerCase(java.util.Locale.ROOT)
if (
it.isHiddenFile() ||
fileName.isHiddenFile() ||
normalizedName in Constants.defaultSensitiveFileNamesLowercase ||
normalizedName.endsWith("~") ||
Constants.defaultSensitiveExtensions.any { extension ->
normalizedName.endsWith(extension)
}
) {
files_to_exclude.add(it)
files_to_exclude.add(fileName)
}
}

Expand Down Expand Up @@ -426,10 +433,10 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array
</head>
<body>
<main>
<h1>${directoryName.escapeHtml()}</h1>
<h1 dir="auto">${directoryName.escapeHtml()}</h1>
<nav aria-label="๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก">
<ul role="list">
<li><a class="dir-link" href="./.." title="์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™"><span class="icon" aria-hidden="true">&#x21B0;</span> <span aria-hidden="true">..</span> <span class="visually-hidden">์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™</span></a></li>
<li><a class="dir-link" href="./.." title="์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™"><span class="icon" aria-hidden="true">&#x21B0;</span> <span dir="auto" aria-hidden="true">..</span> <span class="visually-hidden">์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™</span></a></li>
"""

val index_middle = fun():String{
Expand Down Expand Up @@ -460,14 +467,14 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array
val ariaLabel = "${fileName} ${if (isLinkedDirectory) { "๋””๋ ‰ํ† ๋ฆฌ" } else { "ํŒŒ์ผ" }}".escapeHtml()
val typeLabel = if (isLinkedDirectory) { "๋””๋ ‰ํ† ๋ฆฌ" } else { "ํŒŒ์ผ" }
val icon = if (isLinkedDirectory) { "&#128193;" } else { "&#128196;" }
l.append(""" <li><a class="dir-link" href="${encodedHref}" title="${ariaLabel}"><span class="icon" aria-hidden="true">${icon}</span> <span>${fileName.escapeHtml()}</span> <span class="visually-hidden">${typeLabel}</span></a></li>""")
l.append(""" <li><a class="dir-link" href="${encodedHref}" title="${ariaLabel}"><span class="icon" aria-hidden="true">${icon}</span> <span dir="auto">${fileName.escapeHtml()}</span> <span class="visually-hidden">${typeLabel}</span></a></li>""")
l.append('\n')
}
}
}

if(l.isEmpty()){
l.append(""" <li><div class="empty-dir" role="status"><span class="icon" aria-hidden="true">&#128194;</span> <span>์ด ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.</span></div></li>""")
l.append(""" <li><div class="empty-dir" role="status"><span class="icon" aria-hidden="true">&#128194;</span> <span dir="auto">์ด ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.</span></div></li>""")
l.append('\n')
}

Expand Down
4 changes: 2 additions & 2 deletions src/test/kotlin/html4tree/GeneratedIndexReadabilityTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class GeneratedIndexReadabilityTest {
)

val generatedHtml = generatedHtml()
val parentIndex = generatedHtml.indexOf("<span aria-hidden=\"true\">..</span>")
val parentIndex = generatedHtml.indexOf("<span dir=\"auto\" aria-hidden=\"true\">..</span>")
val firstIndex = generatedHtml.indexOf("alpha.txt")
val middleIndex = generatedHtml.indexOf("middle.txt")
val lastIndex = generatedHtml.indexOf("zulu.txt")
Expand All @@ -62,7 +62,7 @@ class GeneratedIndexReadabilityTest {

val generatedHtml = generatedHtml()
val expectedEmptyRow =
"""<li><div class="empty-dir" role="status"><span class="icon" aria-hidden="true">&#128194;</span> <span>์ด ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.</span></div></li>"""
"""<li><div class="empty-dir" role="status"><span class="icon" aria-hidden="true">&#128194;</span> <span dir="auto">์ด ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.</span></div></li>"""

assertTrue(generatedHtml.contains(expectedEmptyRow))
assertTrue(generatedHtml.indexOf(expectedEmptyRow) == generatedHtml.lastIndexOf(expectedEmptyRow))
Expand Down
41 changes: 37 additions & 4 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,26 @@ class MainTest {
}
}

@Test
fun testProcessIgnoreFileExcludesSensitiveNamesWithWhitespace() {
val sensitiveNamesWithWhitespace = arrayOf(
" .git",
"config.json ",
"\tsecrets.yml",
".npmrc\r"
)
val safeNames = arrayOf(" public.txt ", "image.png")

val excluded = process_ignore_file(tempDir, sensitiveNamesWithWhitespace + safeNames)

sensitiveNamesWithWhitespace.forEach { name ->
assertTrue(excluded.contains(name.trim()), "'$name' must be excluded after trimming")
}
safeNames.forEach { name ->
assertFalse(excluded.contains(name.trim()), "'$name' must remain visible")
}
}

@Test
fun testGoIgnoresHiddenFilesAndDirectories() {
val hiddenFile = File(tempDir, ".hidden_file.txt")
Expand Down Expand Up @@ -335,12 +355,9 @@ class MainTest {
assertTrue(htmlContent.contains("role=\"list\""))
assertTrue(htmlContent.contains("<main>"))
assertTrue(htmlContent.contains("</main>"))
assertTrue(htmlContent.contains("<span class=\"visually-hidden\">์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™</span>"))
assertTrue(htmlContent.contains("title=\"์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™\""))
assertTrue(htmlContent.contains("aria-hidden=\"true\""))
assertTrue(htmlContent.contains("<span class=\"visually-hidden\">ํŒŒ์ผ</span>"))
assertTrue(htmlContent.contains("title=\"file1.txt ํŒŒ์ผ\""))
assertTrue(htmlContent.contains("<span class=\"visually-hidden\">๋””๋ ‰ํ† ๋ฆฌ</span>"))
assertTrue(htmlContent.contains("title=\"subdir ๋””๋ ‰ํ† ๋ฆฌ\""))
assertTrue(htmlContent.contains("file1.txt"))
assertTrue(htmlContent.contains("subdir/"))
Expand Down Expand Up @@ -943,7 +960,23 @@ class MainTest {
assertTrue(indexHtml.exists())
val content = indexHtml.readText()
assertTrue(content.contains("<title>Root - ๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก</title>"))
assertTrue(content.contains("<h1>Root</h1>"))
assertTrue(content.contains("<h1 dir=\"auto\">Root</h1>"))
}

@Test
fun testCrawlDirectoriesRespectsMaxSafeDepth() {
val maxSafeDepth = Class.forName("html4tree.MainKt").getDeclaredField("MAX_SAFE_DEPTH").apply { isAccessible = true }.get(null) as Int
val queue = LinkedList()
queue.push(LinkedListEntry(tempDir, maxSafeDepth))

var processDirectoryCalled = false
crawl_directories(
queue,
-1,
processDirectory = { _, _, _ -> processDirectoryCalled = true },
processIgnoreFile = { _, _ -> emptySet() },
listFiles = { emptyArray() }
)
assertFalse(processDirectoryCalled, "processDirectory should not be called when currentLevel >= MAX_SAFE_DEPTH")
}
}
Loading