Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-06-27 - [CLI Output Accessibility]
**Learning:** Even simple generated HTML outputs (like static index pages) from CLI tools often lack basic accessibility semantics out-of-the-box. Emoticons (πŸ“, β–Ή) are insufficient indicators for screen readers identifying interactive elements like file links.
**Action:** Always inject `aria-label` attributes to explicitly describe interactive items (e.g., "Parent directory", "Directory: folderName") when generating static HTML UI, replacing or augmenting icon-only context.
3 changes: 2 additions & 1 deletion src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ fun process_dir(curr_dir: File){
dir_files.forEach {
val isLinkedDirectory = it.isDirectory() && !java.nio.file.Files.isSymbolicLink(it.toPath())
if((it.getName() !in exclude) && (isLinkedDirectory || !it.isDirectory())) {
l += """ <li><a style="display:block; width:100%" href="${if (isLinkedDirectory) { "./${it.getName().urlEncodePath()}/" } else { "./${it.getName().urlEncodePath()}" }}">${if (isLinkedDirectory) { "&#128193;" } else { "&rtrif;" }} ${it.getName().escapeHtml()}</a></li>"""+"\n"
val label = if (isLinkedDirectory) "Directory: ${it.getName().escapeHtml()}" else "File: ${it.getName().escapeHtml()}"
l += """ <li><a style="display:block; width:100%" href="${if (isLinkedDirectory) { "./${it.getName().urlEncodePath()}/" } else { "./${it.getName().urlEncodePath()}" }}" aria-label="$label">${if (isLinkedDirectory) { "&#128193;" } else { "&rtrif;" }} ${it.getName().escapeHtml()}</a></li>"""+"\n"
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/html4tree/util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class LinkedList {
if(l == null){
return null
} else {
l.next = null
l.next = null
return LinkedListEntry(l.data, l.level)
}
}
Expand Down
70 changes: 70 additions & 0 deletions src/test/kotlin/html4tree/LinkedListTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package html4tree

import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import java.io.File

class LinkedListTest {

@Test
fun testLinkedList() {
val ll = LinkedList()
assertNull(ll.pull())

val f1 = File("file1")
val f2 = File("file2")
val f3 = File("file3")

ll.push(LinkedListEntry(f1, 0))
ll.push(LinkedListEntry(f2, 1))
ll.push(LinkedListEntry(f3, 2))

val e1 = ll.pull()
assertEquals(f1, e1?.file)
assertEquals(0, e1?.level)

val e2 = ll.pull()
assertEquals(f2, e2?.file)
assertEquals(1, e2?.level)

val e3 = ll.pull()
assertEquals(f3, e3?.file)
assertEquals(2, e3?.level)

assertNull(ll.pull())
}

@Test
fun testLinkedListSettersGetters() {
val ll = LinkedList()
val e = Entry(File("a"), 0, null)
ll.first = e
ll.last = e
assertEquals(e, ll.first)
assertEquals(e, ll.last)
}

@Test
fun testPushToNonEmpty() {
val ll = LinkedList()
val lle1 = LinkedListEntry(File("1"), 1)
val lle2 = LinkedListEntry(File("2"), 2)
ll.push(lle1)
ll.push(lle2)

// This exercises the else branch in push
assertEquals("2", ll.first?.data?.name)
assertEquals("1", ll.last?.data?.name)
}

@Test
fun testPushWithNullFirst() {
val ll = LinkedList()
val lle1 = LinkedListEntry(File("1"), 1)
val lle2 = LinkedListEntry(File("2"), 2)
ll.push(lle1)
ll.first = null // artificially make first null
ll.push(lle2) // will trigger first?.next safe calls returning null
}
}
162 changes: 162 additions & 0 deletions src/test/kotlin/html4tree/MainKtTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package html4tree

import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
import kotlin.test.assertNull
import java.io.File
import org.junit.Rule
import org.junit.rules.TemporaryFolder

class MainKtTest {

@Rule
@JvmField
val tempFolder = TemporaryFolder()

@Test
fun testEscapeHtml() {
assertEquals("&amp;", "&".escapeHtml())
assertEquals("&lt;", "<".escapeHtml())
assertEquals("&gt;", ">".escapeHtml())
assertEquals("&quot;", "\"".escapeHtml())
assertEquals("&#x27;", "'".escapeHtml())
assertEquals("test", "test".escapeHtml())
}

@Test
fun testUrlEncodePath() {
assertEquals("hello%20world", "hello world".urlEncodePath())
assertEquals("hello%2Bworld", "hello+world".urlEncodePath())
assertEquals("%2Fhello%2Fworld", "/hello/world".urlEncodePath())
}

@Test
fun testProcessIgnoreFileNoIgnore() {
val dir = tempFolder.newFolder("testdir")
val exclude = process_ignore_file(dir)
assertEquals(listOf("index.html"), exclude)
}

@Test
fun testProcessIgnoreFileWithIgnore() {
val dir = tempFolder.newFolder("testdir2")
File(dir, "file1.txt").createNewFile()
File(dir, "file2.md").createNewFile()
val ignoreFile = File(dir, ".html4ignore")
ignoreFile.writeText(".*\\.txt")

val exclude = process_ignore_file(dir)
assertTrue(exclude.contains("index.html"))
assertTrue(exclude.contains("file1.txt"))
assertFalse(exclude.contains("file2.md"))
}

@Test
fun testProcessDir() {
val dir = tempFolder.newFolder("processdirtest")
File(dir, "a.txt").createNewFile()
val subdir = File(dir, "subdir")
subdir.mkdir()
File(dir, ".html4ignore").writeText(".*\\.txt")

process_dir(dir)

val indexFile = File(dir, "index.html")
assertTrue(indexFile.exists())
val content = indexFile.readText()
assertTrue(content.contains("<!doctype html>"))
assertTrue(content.contains("<html lang=\"ko\">"))
assertTrue(content.contains("subdir/"))
assertTrue(content.contains("aria-label=\"Directory: subdir\""))
assertFalse(content.contains("a.txt")) // ignored
}

@Test
fun testGo() {
val root = tempFolder.newFolder("root")
val subdir1 = File(root, "subdir1")
subdir1.mkdir()
val subdir2 = File(subdir1, "subdir2")
subdir2.mkdir()

go(root.absolutePath, -1)

assertTrue(File(root, "index.html").exists())
assertTrue(File(subdir1, "index.html").exists())
assertTrue(File(subdir2, "index.html").exists())
}

@Test
fun testGoMaxLevel() {
val root = tempFolder.newFolder("root_max")
val subdir1 = File(root, "subdir1")
subdir1.mkdir()
val subdir2 = File(subdir1, "subdir2")
subdir2.mkdir()

go(root.absolutePath, 0)

assertTrue(File(root, "index.html").exists())
assertFalse(File(subdir1, "index.html").exists())
assertFalse(File(subdir2, "index.html").exists())
}

@Test
fun testHelp() {
// help() just prints to standard output, we just call it to cover it
help()
}

@Test
fun testCliCommand() {
val root = tempFolder.newFolder("cli_test")
val cmd = Html4tree()
cmd.parse(arrayOf("--max-level", "0", root.absolutePath))
assertTrue(File(root, "index.html").exists())
}

@Test
fun testMainArgs() {
val root = tempFolder.newFolder("main_args_test")
html4tree.main(arrayOf(root.absolutePath))
assertTrue(File(root, "index.html").exists())
}

@Test(expected = IllegalArgumentException::class)
fun testGoRequireFileExists() {
go("does_not_exist_xyz", -1)
}

@Test(expected = IllegalArgumentException::class)
fun testGoRequireFileIsDirectory() {
val f = tempFolder.newFile("not_a_dir.txt")
go(f.absolutePath, -1)
}

@Test
fun testGoWithNonDirectoryInside() {
val root = tempFolder.newFolder("root_mixed")
File(root, "file1.txt").createNewFile()
val subdir = File(root, "subdir1")
subdir.mkdir()
File(subdir, "file2.txt").createNewFile()

go(root.absolutePath, -1)

assertTrue(File(root, "index.html").exists())
assertTrue(File(subdir, "index.html").exists())
}

@Test
fun testPullReturnsPushedEntry() {
val root = tempFolder.newFolder("root_for_pull")
val ll = LinkedList()
ll.push(LinkedListEntry(root, 0))
val pulled = ll.pull()
assertEquals(root, pulled?.file)
assertEquals(0, pulled?.level)
assertNull(ll.pull())
}
}