Skip to content
Closed
4 changes: 0 additions & 4 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,3 @@
## 2024-08-01 - ๋„ค์ดํ‹ฐ๋ธŒ ๋ธŒ๋ผ์šฐ์ € UI์˜ ๋‹คํฌ ๋ชจ๋“œ ์ง€์› ๊ฐ•์ œ
**ํ•™์Šต:** CSS ๋ฏธ๋””์–ด ์ฟผ๋ฆฌ(`@media (prefers-color-scheme: dark)`)๋ฅผ ํ†ตํ•ด ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์ง€์›ํ•˜๋”๋ผ๋„, ๋ธŒ๋ผ์šฐ์ €์˜ ๋„ค์ดํ‹ฐ๋ธŒ UI ์š”์†Œ(์Šคํฌ๋กค๋ฐ”, ๊ธฐ๋ณธ ํผ ์ปจํŠธ๋กค, ๊ธฐ๋ณธ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋“ฑ)๋Š” ํ…Œ๋งˆ ๋ณ€๊ฒฝ์„ ์ธ์‹ํ•˜์ง€ ๋ชปํ•ด ์–ด๋‘์šด ํ…Œ๋งˆ ํ™˜๊ฒฝ์—์„œ ๋ฐ์€ ์Šคํฌ๋กค๋ฐ”๊ฐ€ ํ‘œ์‹œ๋˜๋Š” ๋“ฑ ์‹œ๊ฐ์  ๋ถˆ์ผ์น˜๋ฅผ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค.
**์กฐ์น˜:** ํ•ญ์ƒ HTML ๋ฌธ์„œ์˜ `<head>` ์˜์—ญ์— `<meta name="color-scheme" content="light dark">` ๋ฉ”ํƒ€ ํƒœ๊ทธ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ € ์ˆ˜์ค€์—์„œ ์‚ฌ์šฉ์ž์˜ ์‹œ์Šคํ…œ ํ…Œ๋งˆ(๋‹คํฌ ๋ชจ๋“œ ๋“ฑ)๋ฅผ ์™„์ „ํžˆ ์ƒ์†๋ฐ›์•„ ์ผ๊ด€์„ฑ ์žˆ๋Š” ๋„ค์ดํ‹ฐ๋ธŒ UI๋ฅผ ๋ Œ๋”๋งํ•˜๋„๋ก ๋ณด์žฅํ•˜์‹ญ์‹œ์˜ค.

## 2024-07-13 - ๋นˆ ๋””๋ ‰ํ† ๋ฆฌ ์ƒํƒœ์˜ ์ ‘๊ทผ์„ฑ(Accessibility) ๊ฐœ์„ 
**Learning:** ์ •์  ํŒŒ์ผ ์„œ๋ฒ„์˜ ๋นˆ ๋””๋ ‰ํ† ๋ฆฌ ์ƒํƒœ๋Š” ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž์—๊ฒŒ ์ปจํ…์ธ  ๋ˆ„๋ฝ์œผ๋กœ ์˜คํ•ด๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‹œ๊ฐ์ ์œผ๋กœ๋„ ์ผ๋ฐ˜ ๋ฆฌ์ŠคํŠธ ์•„์ดํ…œ๊ณผ ์ •๋ ฌ์ด ๋งž์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
**Action:** ๋นˆ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์š”์†Œ์— `role="status"`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ณ , ์•„์ด์ฝ˜๊ณผ flex ๋ ˆ์ด์•„์›ƒ์„ ํ†ตํ•ด ๋‹ค๋ฅธ ๋ฆฌ์ŠคํŠธ ์•„์ดํ…œ๊ณผ ์ผ๊ด€๋œ ์‹œ๊ฐ์  ํ๋ฆ„์„ ์ œ๊ณตํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ repositories {

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testCompile 'junit:junit:4.13.2'
testCompile 'junit:junit:4.11'
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
compile "com.github.ajalt:clikt:2.7.1"
}
Expand Down
13 changes: 6 additions & 7 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ internal fun crawl_directories(
dirFiles?.forEach {
// โšก Bolt Performance Optimization: Short-circuit OS stat calls (isDirectory/isSymbolicLink)
// by checking cheap in-memory string exclusion rules first
if(!it.name.startsWith(".") && it.name !in exclude && isDirectory(it) && !isSymbolicLink(it)) {
if(!it.name.startsWithHiddenPrefix() && it.name !in exclude && isDirectory(it) && !isSymbolicLink(it)) {
val childEntry = LinkedListEntry(it, currentLevel+1, readIdentity(it).key)
ll.push(childEntry)
}
Expand Down Expand Up @@ -221,7 +221,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array<String>? = null): S

// ๋ณด์•ˆ ํ–ฅ์ƒ: .env, .git ๋“ฑ ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ๋Š” ์ˆจ๊น€ ํŒŒ์ผ(.์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ํ•ญ๋ชฉ)์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋…ธ์ถœํ•˜์ง€ ์•Š๋„๋ก ์ œ์™ธ (์ •๋ณด ๋…ธ์ถœ ๋ฐฉ์ง€)
(dirFilesNames ?: curr_dir.list())?.forEach {
if (it.startsWith(".")) {
if (it.startsWithHiddenPrefix()) {
files_to_exclude.add(it)
}
}
Expand Down Expand Up @@ -304,9 +304,6 @@ fun process_dir(curr_dir: File, excludeSet: Set<String>? = null, dirFiles: Array
}
}
.empty-dir {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.5rem;
opacity: 0.7;
font-style: italic;
Expand All @@ -330,6 +327,8 @@ ${cssContent} </style>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src '${styleHash}'; base-uri 'none'; form-action 'none';">
<!-- ๋ณด์•ˆ ํ–ฅ์ƒ: ๋ฆฌํผ๋Ÿฌ๋ฅผ ํ†ตํ•œ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ ๋…ธ์ถœ ๋ฐฉ์ง€ -->
<meta name="referrer" content="no-referrer">
<!-- ๋ณด์•ˆ ํ–ฅ์ƒ: ๊ฒ€์ƒ‰ ์—”์ง„ ์—ฐ๋™ ์ •๋ณด ๋…ธ์ถœ ๋ฐฉ์ง€ -->
<meta name="robots" content="noindex, nofollow">
<title>${curr_dir.getName().escapeHtml()}</title>
${css}
</head>
Expand All @@ -351,7 +350,7 @@ ${cssContent} </style>
val fileName = it.getName()
// โšก Bolt Performance Optimization: Short-circuit string match before expensive OS filesystem calls
// ๐Ÿ›ก๏ธ Sentinel: Ignore hidden files/directories to prevent sensitive data exposure
if (!fileName.startsWith(".") && fileName !in exclude) {
if (!fileName.startsWithHiddenPrefix() && fileName !in exclude) {
var isLinkedDirectory = false
var isSymbolicLink = false
try {
Expand All @@ -373,7 +372,7 @@ ${cssContent} </style>
}

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

Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/html4tree/util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ data class Entry (val data: File, val level: Int, var next: Entry?, val fileKey:

data class LinkedListEntry(val file: File, val level: Int, var fileKey: Any? = null)

fun String.startsWithHiddenPrefix(): Boolean {
if (this.isEmpty()) return false
val c = this[0]
return c == '.' || c == 'ใ€‚' || c == '๏น’' || c == '๏ผŽ' || c == '๏ฝก' || c == 'โ€ค' || c == 'โ€ฅ'
}

class LinkedList {
var first: Entry? = null
var last: Entry? = null
Expand Down
34 changes: 33 additions & 1 deletion src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ class MainTest {
assertTrue(indexFile.exists())
val htmlContent = indexFile.readText()
assertTrue(htmlContent.contains("<html lang=\"ko\">"))
assertTrue(htmlContent.contains("<meta name=\"robots\" content=\"noindex, nofollow\">"))
assertTrue(htmlContent.contains("์ด ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค."))
assertTrue(htmlContent.contains("role=\"status\""))
assertTrue(htmlContent.contains("role=\"list\""))
}

Expand All @@ -118,6 +118,12 @@ class MainTest {
val normalFile = File(tempDir, "normal_file.txt")
normalFile.createNewFile()

val unicodeHiddenFile = File(tempDir, "ใ€‚unicode_hidden.txt")
unicodeHiddenFile.createNewFile()

val unicodeHiddenDir = File(tempDir, "๏ผŽunicode_hidden_dir")
unicodeHiddenDir.mkdir()

go(tempDir.absolutePath, -1)

val indexFile = File(tempDir, "index.html")
Expand All @@ -127,9 +133,12 @@ class MainTest {
assertTrue(htmlContent.contains("normal_file.txt"), "normal_file.txt should be listed")
assertFalse(htmlContent.contains(".hidden_file.txt"), ".hidden_file.txt should not be listed")
assertFalse(htmlContent.contains(".hidden_dir"), ".hidden_dir should not be listed")
assertFalse(htmlContent.contains("ใ€‚unicode_hidden.txt"), "ใ€‚unicode_hidden.txt should not be listed")
assertFalse(htmlContent.contains("๏ผŽunicode_hidden_dir"), "๏ผŽunicode_hidden_dir should not be listed")

val hiddenDirIndexFile = File(hiddenDir, "index.html")
assertFalse(hiddenDirIndexFile.exists(), "Hidden directories should not be traversed to generate index.html")
assertFalse(File(unicodeHiddenDir, "index.html").exists(), "Unicode hidden directories should not be traversed to generate index.html")
}

@Test
Expand Down Expand Up @@ -310,6 +319,7 @@ class MainTest {
val htmlContent = indexFile.readText()
assertTrue(htmlContent.contains("<html lang=\"ko\">"))
assertTrue(htmlContent.contains("<meta name=\"color-scheme\" content=\"light dark\">"))
assertTrue(htmlContent.contains("<meta name=\"robots\" content=\"noindex, nofollow\">"))
assertTrue(htmlContent.contains("<nav aria-label=\"๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก\">"))
assertTrue(htmlContent.contains("role=\"list\""))
assertTrue(htmlContent.contains("<main>"))
Expand Down Expand Up @@ -382,6 +392,7 @@ class MainTest {
assertTrue(indexFile.exists())
assertFalse(Files.isSymbolicLink(indexFile.toPath()))
assertTrue(indexFile.readText().contains("<html lang=\"ko\">"))
assertTrue(indexFile.readText().contains("<meta name=\"robots\" content=\"noindex, nofollow\">"))
}

@Test
Expand Down Expand Up @@ -555,6 +566,21 @@ class MainTest {
assertTrue(excluded.contains("test.txt"))
}

@Test
fun testStartsWithHiddenPrefix() {
assertFalse("".startsWithHiddenPrefix())
assertFalse("normal.txt".startsWithHiddenPrefix())
assertFalse(" .txt".startsWithHiddenPrefix())

assertTrue(".hidden".startsWithHiddenPrefix())
assertTrue("ใ€‚unicode".startsWithHiddenPrefix())
assertTrue("๏น’unicode".startsWithHiddenPrefix())
assertTrue("๏ผŽunicode".startsWithHiddenPrefix())
assertTrue("๏ฝกunicode".startsWithHiddenPrefix())
assertTrue("โ€คunicode".startsWithHiddenPrefix())
assertTrue("โ€ฅunicode".startsWithHiddenPrefix())
}

@Test
fun testProcessIgnoreFileHiddenFiles() {
// .myhidden/.hiddendir are NOT in the static sensitive-file list,
Expand All @@ -563,11 +589,17 @@ class MainTest {
File(tempDir, ".hiddendir").mkdir()
File(tempDir, ".env").createNewFile()
File(tempDir, "test.txt").createNewFile()
File(tempDir, "ใ€‚unicodehidden").createNewFile()
File(tempDir, "๏ผŽunicodedir").mkdir()
File(tempDir, "โ€คunicodefile").createNewFile()

val excluded = process_ignore_file(tempDir)
assertTrue(excluded.contains(".myhidden"))
assertTrue(excluded.contains(".hiddendir"))
assertTrue(excluded.contains(".env"))
assertTrue(excluded.contains("ใ€‚unicodehidden"))
assertTrue(excluded.contains("๏ผŽunicodedir"))
assertTrue(excluded.contains("โ€คunicodefile"))
assertFalse(excluded.contains("test.txt"))
}

Expand Down
Loading