Skip to content

Commit 91967c0

Browse files
committed
export
0 parents  commit 91967c0

File tree

11 files changed

+441
-0
lines changed

11 files changed

+441
-0
lines changed

.github/workflows/pages.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Publish documentation to Github Pages
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [main]
7+
paths:
8+
- '*.md'
9+
10+
permissions:
11+
id-token: write # This is required for requesting the JWT
12+
contents: write # This is required for actions/checkout
13+
pages: write # to deploy to Pages
14+
15+
jobs:
16+
PublishGithubPages:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- id: release
20+
name: Create and upload new Github Pages content
21+
uses: encalmo/create-new-release-action@v1.5.0
22+
with:
23+
tag-prefix: 'version'
24+
version-bump: 'keep'
25+
release-flags: ''
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
sonatype-token: ${{ secrets.SONATYPE_TOKEN }}
28+
gpg-secret-key-base64: ${{ secrets.GPG_SECRET_KEY }}
29+
gpg-secret-key-id: ${{ secrets.GPG_SECRET_KEY_ID }}
30+
pages-only: 'true'
31+
markdown-paths: 'README.md'
32+

.github/workflows/release.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Release new version to Sonatype Central
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version-bump:
7+
type: choice
8+
description: 'How to bump a version?'
9+
required: true
10+
default: 'patch'
11+
options:
12+
- major
13+
- minor
14+
- patch
15+
- keep
16+
# push:
17+
# branches: [main]
18+
# paths:
19+
# - '*.scala'
20+
21+
permissions:
22+
id-token: write # This is required for requesting the JWT
23+
contents: write # This is required for actions/checkout
24+
pages: write # to deploy to Pages
25+
26+
jobs:
27+
ReleasePackage:
28+
runs-on: ubuntu-latest
29+
steps:
30+
- id: release
31+
name: Create and upload new release bundles
32+
uses: encalmo/create-new-release-action@v1.5.0
33+
with:
34+
tag-prefix: 'version'
35+
version-bump: ${{ inputs.version-bump || 'patch' }}
36+
release-flags: '--native'
37+
github-token: ${{ secrets.GITHUB_TOKEN }}
38+
sonatype-token: ${{ secrets.SONATYPE_TOKEN }}
39+
gpg-secret-key-base64: ${{ secrets.GPG_SECRET_KEY }}
40+
gpg-secret-key-id: ${{ secrets.GPG_SECRET_KEY_ID }}
41+
pages-only: 'false'
42+
markdown-paths: 'README.md'
43+

.github/workflows/test.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Run tests
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
paths:
7+
- '*.scala'
8+
9+
permissions:
10+
id-token: write # This is required for requesting the JWT
11+
contents: write # This is required for actions/checkout
12+
13+
jobs:
14+
runTests:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 0
20+
- uses: coursier/cache-action@v6
21+
- uses: VirtusLab/scala-cli-setup@main
22+
with:
23+
jvm: adoptium:1.21
24+
apps: scala
25+
power: true
26+
- name: Run tests
27+
run: scala test .
28+
29+

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.bsp/
2+
.scala-build/
3+
.metals/
4+
.vscode/

.scalafmt.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version = "3.7.15"
2+
runner.dialect = scala3
3+
maxColumn = 120

FilesTree.scala

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package org.encalmo.utils
2+
3+
import java.nio.file.{Path, Paths}
4+
5+
import scala.annotation.tailrec
6+
7+
trait FileTree {
8+
9+
type Node = (Int, String)
10+
type Tree = List[Node]
11+
12+
val middleNode = "├── "
13+
val endNode = "└── "
14+
val link = ""
15+
val space = " " * link.length
16+
17+
private val root = Paths.get("/")
18+
19+
def sort(paths: Seq[Path]): Seq[Path] =
20+
paths.sortWith((pl, pr) => comparePaths(pl, pr, 0))
21+
22+
def compute(paths: Seq[Path]): Tree = {
23+
24+
def leafs(prefix: Path, p2: Path): Tree =
25+
(0 until p2.getNameCount).toList
26+
.map(i => (i + prefix.getNameCount, p2.getName(i).toString))
27+
28+
sort(paths)
29+
.foldLeft((List.empty[Node], Option.empty[Path])) { case ((tree, prevPathOpt), path) =>
30+
(
31+
prevPathOpt
32+
.map { prevPath =>
33+
val (prefix, _, outstandingPath) = commonPrefix(root, prevPath, path)
34+
tree ::: leafs(prefix, outstandingPath)
35+
}
36+
.getOrElse(leafs(root, path)),
37+
Some(path)
38+
)
39+
}
40+
._1
41+
}
42+
43+
def draw(pathsTree: Tree): String = {
44+
45+
def drawLine(node: String, label: String, marks: List[Int]): (String, List[Int]) =
46+
((0 until marks.max).map(i => if (marks.contains(i)) link else space).mkString + node + label, marks)
47+
48+
def draw2(label: String, ls: (List[Int], String)): (String, List[Int]) =
49+
((0 until ls._1.max).map(i => if (ls._1.contains(i)) link else space).mkString + ls._2 + label, ls._1)
50+
51+
def append(lineWithMarks: (String, List[Int]), result: String): (String, List[Int]) =
52+
(trimRight(lineWithMarks._1) + "\n" + result, lineWithMarks._2)
53+
54+
pathsTree.reverse
55+
.foldLeft(("", List.empty[Int])) { case ((result, marks), (offset, label)) =>
56+
marks match {
57+
case Nil => drawLine(endNode, label, offset :: Nil)
58+
case head :: tail =>
59+
append(
60+
if (offset == head) drawLine(middleNode, label, marks)
61+
else if (offset < head)
62+
draw2(
63+
label,
64+
tail match {
65+
case Nil => (offset :: Nil, endNode)
66+
case x :: _ if x == offset => (tail, middleNode)
67+
case _ => (offset :: tail, endNode)
68+
}
69+
)
70+
else {
71+
val l1 = drawLine(endNode, label, offset :: marks)
72+
val l2 = drawLine(space, "", offset :: marks)
73+
(l1._1 + "\n" + l2._1, l1._2)
74+
},
75+
result
76+
)
77+
}
78+
}
79+
._1
80+
}
81+
82+
@tailrec
83+
final def comparePaths(path1: Path, path2: Path, i: Int): Boolean = {
84+
val c = path1.getName(i).toString.compareToIgnoreCase(path2.getName(i).toString)
85+
val pc1 = path1.getNameCount
86+
val pc2 = path2.getNameCount
87+
if (pc1 - 1 == i || pc2 - 1 == i) {
88+
if (c != 0) c < 0 else pc1 < pc2
89+
} else {
90+
if (c != 0) c < 0 else comparePaths(path1, path2, i + 1)
91+
}
92+
}
93+
94+
@tailrec
95+
final def commonPrefix(prefix: Path, path1: Path, path2: Path): (Path, Path, Path) =
96+
if (path1.getNameCount > 0 && path2.getNameCount > 0) {
97+
if (path1.getName(0) != path2.getName(0)) (prefix, path1, path2)
98+
else
99+
commonPrefix(
100+
prefix.resolve(path1.subpath(0, 1)),
101+
if (path1.getNameCount == 1) path1 else path1.subpath(1, path1.getNameCount),
102+
if (path2.getNameCount == 1) path2 else path2.subpath(1, path2.getNameCount)
103+
)
104+
} else (prefix, path1, path2)
105+
106+
def trimRight(string: String): String = string.reverse.dropWhile(_ == ' ').reverse
107+
}
108+
109+
object FileTree extends FileTree

FilesTree.test.scala

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package org.encalmo.utils
2+
3+
import java.nio.file.Paths
4+
5+
class FilesTreeSpec extends munit.FunSuite {
6+
7+
extension [T](t0: T)
8+
inline def shouldBe(t1: T) =
9+
assertEquals(t0, t1)
10+
11+
extension [T](ts0: Seq[T])
12+
inline def containsOnly(ts1: Seq[T]) =
13+
assert(ts0.sameElements(ts1))
14+
15+
test("compute a tree") {
16+
FileTree.compute(Seq(Paths.get("test.scala"))) shouldBe List(0 -> "test.scala")
17+
FileTree.compute(Seq(Paths.get("/test", "test.scala"))) shouldBe List(0 -> "test", 1 -> "test.scala")
18+
FileTree.compute(Seq(Paths.get("/test"), Paths.get("/test", "test.scala"))) containsOnly (List(
19+
0 -> "test",
20+
1 -> "test.scala"
21+
))
22+
FileTree.compute(Seq(Paths.get("/test", "test.scala"), Paths.get("/test"))) containsOnly (List(
23+
0 -> "test",
24+
1 -> "test.scala"
25+
))
26+
FileTree.compute(Seq(Paths.get("/test", "test.scala"), Paths.get("/test"))) containsOnly (List(
27+
0 -> "test",
28+
1 -> "test.scala"
29+
))
30+
FileTree.compute(Seq(Paths.get("/test"), Paths.get("/test", "test.scala"))) containsOnly (List(
31+
0 -> "test",
32+
1 -> "test.scala"
33+
))
34+
FileTree.compute(
35+
Seq(Paths.get("/test"), Paths.get("/test", "foo", "bar.txt"), Paths.get("/test", "test.scala"))
36+
) containsOnly (List(0 -> "test", 1 -> "foo", 2 -> "bar.txt", 1 -> "test.scala"))
37+
FileTree.compute(
38+
Seq(
39+
Paths.get("/test"),
40+
Paths.get("/test", "foo", "bar.txt"),
41+
Paths.get("foo.bar"),
42+
Paths.get("/test", "test.scala")
43+
)
44+
) containsOnly (List(
45+
0 -> "foo.bar",
46+
0 -> "test",
47+
1 -> "foo",
48+
2 -> "bar.txt",
49+
1 -> "test.scala"
50+
))
51+
FileTree.compute(
52+
Seq(
53+
Paths.get("/test"),
54+
Paths.get("/test", "foo", "bar.txt"),
55+
Paths.get("/bar", "foo.bar"),
56+
Paths.get("/test", "test.scala")
57+
)
58+
) containsOnly (
59+
List(0 -> "bar", 1 -> "foo.bar", 0 -> "test", 1 -> "foo", 2 -> "bar.txt", 1 -> "test.scala")
60+
)
61+
}
62+
63+
test("draw a tree 1") {
64+
val pathTree = FileTree.compute(
65+
Seq(
66+
Paths.get("/test"),
67+
Paths.get("/test", "foo", "bar.txt"),
68+
Paths.get("foo.bar"),
69+
Paths.get("/test", "test.scala")
70+
)
71+
)
72+
FileTree.draw(pathTree) shouldBe
73+
"""├── foo.bar
74+
|└── test
75+
| ├── foo
76+
| │ └── bar.txt
77+
| │
78+
| └── test.scala""".stripMargin
79+
}
80+
81+
test("draw a tree 2") {
82+
val pathTree = FileTree.compute(
83+
Seq(
84+
Paths.get("/test"),
85+
Paths.get("/test", "foo", "bar.txt"),
86+
Paths.get("/bar", "foo.bar"),
87+
Paths.get("/test", "test.scala")
88+
)
89+
)
90+
FileTree.draw(pathTree) shouldBe
91+
"""├── bar
92+
|│ └── foo.bar
93+
|│
94+
|└── test
95+
| ├── foo
96+
| │ └── bar.txt
97+
| │
98+
| └── test.scala""".stripMargin
99+
}
100+
101+
test("draw a tree 3") {
102+
val pathTree = FileTree.compute(Seq(Paths.get("/test")))
103+
FileTree.draw(pathTree) shouldBe
104+
"""└── test""".stripMargin
105+
}
106+
107+
test("draw a tree 4") {
108+
val pathTree = FileTree.compute(
109+
Seq(
110+
Paths.get("zoo.scala"),
111+
Paths.get("/foo", "zoo", "bar.txt"),
112+
Paths.get("/bar", "foo", "foo.bar"),
113+
Paths.get("/zoo", "foo", "bar", "zoo.scala")
114+
)
115+
)
116+
val tree = FileTree.draw(pathTree)
117+
println(tree)
118+
tree shouldBe
119+
"""├── bar
120+
|│ └── foo
121+
|│ └── foo.bar
122+
|│
123+
|├── foo
124+
|│ └── zoo
125+
|│ └── bar.txt
126+
|│
127+
|├── zoo
128+
|│ └── foo
129+
|│ └── bar
130+
|│ └── zoo.scala
131+
|│
132+
|└── zoo.scala""".stripMargin
133+
}
134+
135+
}

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright (c) 2024 encalmo.org
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the " Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

0 commit comments

Comments
 (0)