1+ import org.w3c.dom.Attr
2+ import org.w3c.dom.Document
3+ import org.w3c.dom.Node
4+ import org.w3c.dom.NodeList
5+ import java.io.File
6+ import java.io.FileOutputStream
7+ import java.util.concurrent.TimeUnit
8+ import javax.xml.parsers.DocumentBuilderFactory
9+ import javax.xml.transform.TransformerFactory
10+ import javax.xml.transform.dom.DOMSource
11+ import javax.xml.transform.stream.StreamResult
12+ import javax.xml.xpath.XPathConstants
13+ import javax.xml.xpath.XPathFactory
14+
15+ /* *
16+ * The scripts generates no ligature version of JetBrains Mono called JetBrains Mono NL
17+ *
18+ * ttx command is required to run this script
19+ *
20+ * @author Konstantin Bulenkov
21+ */
22+ @Suppress(" RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS" )
23+ fun main () {
24+ File (" ./ttf/" )
25+ .listFiles { _, name -> name.endsWith(" .ttf" ) && ! name.startsWith(" JetBrainsMonoNL" ) }
26+ .forEach {
27+ val ttx = it.nameWithoutExtension + " .ttx"
28+ val dir = it.parentFile
29+ File (dir, ttx).deleteAndLog()
30+ val doc = ttf2Document(it)
31+ File (dir, ttx).deleteAndLog()
32+ if (doc != null ) {
33+ generateNoLigaturesFont(File (dir, it.name), doc)
34+ }
35+ }
36+ }
37+
38+ fun ttf2Document (file : File ): Document ? {
39+ " ttx ${file.name} " .runCommand(file.parentFile)
40+ val ttx = file.parentFile.listFiles { _, name -> name == " ${file.nameWithoutExtension} .ttx" }?.first() ? : return null
41+ val documentBuilder = DocumentBuilderFactory .newInstance().newDocumentBuilder()
42+ return documentBuilder.parse(ttx)
43+ }
44+
45+ fun generateNoLigaturesFont (file : File , doc : Document ) {
46+ val nlName = file.nameWithoutExtension.replace(" JetBrainsMono" , " JetBrainsMonoNL" )
47+ val ttx = " $nlName .ttx"
48+ val ttf = " $nlName .ttf"
49+ val dir = File (file.parentFile, " No ligatures" )
50+ File (dir, ttf).deleteAndLog()
51+ doc.removeLigas(" /ttFont/GlyphOrder" , " GlyphID" )
52+ doc.removeLigas(" /ttFont/glyf" , " TTGlyph" )
53+ doc.removeLigas(" /ttFont/hmtx" , " mtx" )
54+ doc.removeLigas(" /ttFont/post/extraNames" , " psName" )
55+ doc.removeLigas(" /ttFont/GDEF/GlyphClassDef" , " ClassDef" , attName = " glyph" )
56+ doc.removeNode(" /ttFont/GPOS" )
57+ doc.removeNode(" /ttFont/GSUB" )
58+
59+ val xPath = XPathFactory .newInstance().newXPath()
60+ val nameRecords = (xPath.evaluate(" /ttFont/name/namerecord" , doc, XPathConstants .NODESET ) as NodeList ).asList()
61+ nameRecords.forEach {
62+ if (! it.textContent.contains(" trademark" )) {
63+ it.textContent = it.textContent
64+ .replace(" JetBrains Mono" , " JetBrains Mono NL" )
65+ .replace(" JetBrainsMono" , " JetBrainsMonoNL" )
66+ }
67+ }
68+
69+ val ttxFile = File (dir, ttx)
70+ doc.saveAs(ttxFile)
71+ " ttx $ttx " .runCommand(dir)
72+ ttxFile.deleteAndLog()
73+ }
74+
75+ class NodeListWrapper (val nodeList : NodeList ) : AbstractList<Node>(), RandomAccess {
76+ override val size: Int
77+ get() = nodeList.length
78+
79+ override fun get (index : Int ): Node = nodeList.item(index)
80+ }
81+
82+ // //////////////////// Utility functions and data classes //////////////////////
83+
84+ fun NodeList.asList (): List <Node > = NodeListWrapper (this )
85+
86+ fun String.runCommand (workingDir : File ) {
87+ ProcessBuilder (* split(" " ).toTypedArray())
88+ .directory(workingDir)
89+ .redirectOutput(ProcessBuilder .Redirect .INHERIT )
90+ .redirectError(ProcessBuilder .Redirect .INHERIT )
91+ .start()
92+ .waitFor(1 , TimeUnit .MINUTES )
93+ }
94+
95+ fun Document.saveAs (file : File ) {
96+ val transformer = TransformerFactory .newInstance().newTransformer()
97+ transformer.transform(DOMSource (this ), StreamResult (FileOutputStream (file)))
98+ }
99+
100+ fun Document.removeLigas (parentPath : String , nodeName : String , attName : String = "name") {
101+ val xPath = XPathFactory .newInstance().newXPath()
102+ val parent = xPath.evaluate(parentPath, this , XPathConstants .NODE ) as Node
103+ val nodeFilter = " $parentPath /$nodeName [substring(@$attName , string-length(@$attName )-4) = '.liga']"
104+ val nodes = (xPath.evaluate(nodeFilter, this , XPathConstants .NODESET ) as NodeList ).asList()
105+ nodes.forEach { parent.removeChild(it) }
106+ }
107+
108+ fun Document.removeNode (path : String ) {
109+ val xPath = XPathFactory .newInstance().newXPath()
110+ val parent = xPath.evaluate(path.substringBeforeLast(" /" ), this , XPathConstants .NODE )
111+ if (parent is Node ) {
112+ val child = xPath.evaluate(path, this , XPathConstants .NODE )
113+ if (child is Node ) {
114+ parent.removeChild(child)
115+ }
116+ }
117+ }
118+
119+ fun File.deleteAndLog () {
120+ if (! exists()) return
121+ println (" Deleting $absolutePath " )
122+ val result = delete()
123+ println (" [$result ]" .toUpperCase())
124+ if (! result) deleteOnExit()
125+ }
0 commit comments