diff --git a/.Jules/palette.md b/.Jules/palette.md
new file mode 100644
index 00000000..bc0e84d3
--- /dev/null
+++ b/.Jules/palette.md
@@ -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.
diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt
index 50b2680d..35260166 100644
--- a/src/main/kotlin/html4tree/main.kt
+++ b/src/main/kotlin/html4tree/main.kt
@@ -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 += """
${if (isLinkedDirectory) { "📁" } else { "▸" }} ${it.getName().escapeHtml()}"""+"\n"
+ val label = if (isLinkedDirectory) "Directory: ${it.getName().escapeHtml()}" else "File: ${it.getName().escapeHtml()}"
+ l += """ ${if (isLinkedDirectory) { "📁" } else { "▸" }} ${it.getName().escapeHtml()}"""+"\n"
}
}
diff --git a/src/main/kotlin/html4tree/util.kt b/src/main/kotlin/html4tree/util.kt
index 9a882d74..c8979c31 100644
--- a/src/main/kotlin/html4tree/util.kt
+++ b/src/main/kotlin/html4tree/util.kt
@@ -39,7 +39,7 @@ class LinkedList {
if(l == null){
return null
} else {
- l.next = null
+ l.next = null
return LinkedListEntry(l.data, l.level)
}
}
diff --git a/src/test/kotlin/html4tree/LinkedListTest.kt b/src/test/kotlin/html4tree/LinkedListTest.kt
new file mode 100644
index 00000000..8b5eb631
--- /dev/null
+++ b/src/test/kotlin/html4tree/LinkedListTest.kt
@@ -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
+ }
+}
diff --git a/src/test/kotlin/html4tree/MainKtTest.kt b/src/test/kotlin/html4tree/MainKtTest.kt
new file mode 100644
index 00000000..4392c93b
--- /dev/null
+++ b/src/test/kotlin/html4tree/MainKtTest.kt
@@ -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("&", "&".escapeHtml())
+ assertEquals("<", "<".escapeHtml())
+ assertEquals(">", ">".escapeHtml())
+ assertEquals(""", "\"".escapeHtml())
+ assertEquals("'", "'".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(""))
+ assertTrue(content.contains(""))
+ 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())
+ }
+}