Skip to content

Commit 66371b6

Browse files
authored
Merge pull request #25 from bnorm/bnorm/xml
Add support for XML highlighting
2 parents 3ea3758 + caad02d commit 66371b6

File tree

13 files changed

+1427
-4
lines changed

13 files changed

+1427
-4
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
[The "BSD licence"]
3+
Copyright (c) 2013 Terence Parr
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions
8+
are met:
9+
1. Redistributions of source code must retain the above copyright
10+
notice, this list of conditions and the following disclaimer.
11+
2. Redistributions in binary form must reproduce the above copyright
12+
notice, this list of conditions and the following disclaimer in the
13+
documentation and/or other materials provided with the distribution.
14+
3. The name of the author may not be used to endorse or promote products
15+
derived from this software without specific prior written permission.
16+
17+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19+
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20+
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22+
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27+
*/
28+
29+
/** XML lexer derived from ANTLR v4 ref guide book example */
30+
31+
// $antlr-format alignTrailingComments true, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments false, useTab false
32+
// $antlr-format allowShortRulesOnASingleLine true, allowShortBlocksOnASingleLine true, minEmptyLines 0, alignSemicolons ownLine
33+
// $antlr-format alignColons trailing, singleLineOverrulesHangingColon true, alignLexerCommands true, alignLabels true, alignTrailers true
34+
35+
lexer grammar XMLLexer;
36+
37+
// Default "mode": Everything OUTSIDE of a tag
38+
COMMENT : '<!--' .*? '-->';
39+
CDATA : '<![CDATA[' .*? ']]>';
40+
/** Scarf all DTD stuff, Entity Declarations like <!ENTITY ...>,
41+
* and Notation Declarations <!NOTATION ...>
42+
*/
43+
DTD : '<!' .*? '>' -> skip;
44+
EntityRef : '&' Name ';';
45+
CharRef : '&#' DIGIT+ ';' | '&#x' HEXDIGIT+ ';';
46+
SEA_WS : (' ' | '\t' | '\r'? '\n')+;
47+
48+
OPEN : '<' -> pushMode(INSIDE);
49+
XMLDeclOpen : '<?xml' S -> pushMode(INSIDE);
50+
SPECIAL_OPEN : '<?' Name -> more, pushMode(PROC_INSTR);
51+
52+
TEXT: ~[<&]+; // match any 16 bit char other than < and &
53+
54+
// ----------------- Everything INSIDE of a tag ---------------------
55+
mode INSIDE;
56+
57+
CLOSE : '>' -> popMode;
58+
SPECIAL_CLOSE : '?>' -> popMode; // close <?xml...?>
59+
SLASH_CLOSE : '/>' -> popMode;
60+
SLASH : '/';
61+
EQUALS : '=';
62+
STRING : '"' ~[<"]* '"' | '\'' ~[<']* '\'';
63+
Name : NameStartChar NameChar*;
64+
S : [ \t\r\n] -> skip;
65+
66+
fragment HEXDIGIT: [a-fA-F0-9];
67+
68+
fragment DIGIT: [0-9];
69+
70+
fragment NameChar:
71+
NameStartChar
72+
| '-'
73+
| '.'
74+
| DIGIT
75+
| '\u00B7'
76+
| '\u0300' ..'\u036F'
77+
| '\u203F' ..'\u2040'
78+
;
79+
80+
fragment NameStartChar:
81+
[_:a-zA-Z]
82+
| '\u2070' ..'\u218F'
83+
| '\u2C00' ..'\u2FEF'
84+
| '\u3001' ..'\uD7FF'
85+
| '\uF900' ..'\uFDCF'
86+
| '\uFDF0' ..'\uFFFD'
87+
;
88+
89+
// ----------------- Handle <? ... ?> ---------------------
90+
mode PROC_INSTR;
91+
92+
PI : '?>' -> popMode; // close <?...?>
93+
IGNORE : . -> more;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
[The "BSD licence"]
3+
Copyright (c) 2013 Terence Parr
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions
8+
are met:
9+
1. Redistributions of source code must retain the above copyright
10+
notice, this list of conditions and the following disclaimer.
11+
2. Redistributions in binary form must reproduce the above copyright
12+
notice, this list of conditions and the following disclaimer in the
13+
documentation and/or other materials provided with the distribution.
14+
3. The name of the author may not be used to endorse or promote products
15+
derived from this software without specific prior written permission.
16+
17+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19+
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20+
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22+
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27+
*/
28+
29+
/** XML parser derived from ANTLR v4 ref guide book example */
30+
31+
// $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false
32+
// $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanging
33+
34+
parser grammar XMLParser;
35+
36+
options {
37+
tokenVocab = XMLLexer;
38+
}
39+
40+
document
41+
: prolog? misc* element misc* EOF
42+
;
43+
44+
prolog
45+
: XMLDeclOpen attribute* SPECIAL_CLOSE
46+
;
47+
48+
content
49+
: chardata? ((element | reference | CDATA | PI | COMMENT) chardata?)*
50+
;
51+
52+
element
53+
: '<' Name attribute* '>' content '<' '/' Name '>'
54+
| '<' Name attribute* '/>'
55+
;
56+
57+
reference
58+
: EntityRef
59+
| CharRef
60+
;
61+
62+
attribute
63+
: Name '=' STRING
64+
; // Our STRING is AttValue in spec
65+
66+
/** ``All text that is not markup constitutes the character data of
67+
* the document.''
68+
*/
69+
chardata
70+
: TEXT
71+
| SEA_WS
72+
;
73+
74+
misc
75+
: COMMENT
76+
| PI
77+
| SEA_WS
78+
;

storyboard-text/build.gradle.kts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,18 @@ val generateKotlinGrammarSource by tasks.registering(AntlrKotlinTask::class) {
4747
packageName = pkgName
4848
outputDirectory = layout.buildDirectory.dir(outDir).get().asFile
4949
}
50+
51+
val generateXmlGrammarSource by tasks.registering(AntlrKotlinTask::class) {
52+
val pkgName = "dev.bnorm.storyboard.text.highlight.antlr.xml"
53+
val outDir = "generatedAntlr/${pkgName.replace(".", "/")}"
54+
55+
inputs.dir(layout.projectDirectory.dir("antlr"))
56+
outputs.dir(layout.buildDirectory.dir(outDir))
57+
doFirst { delete(layout.buildDirectory.dir(outDir)) }
58+
59+
source = fileTree(layout.projectDirectory.dir("antlr/xml")) {
60+
include("**/*.g4")
61+
}
62+
packageName = pkgName
63+
outputDirectory = layout.buildDirectory.dir(outDir).get().asFile
64+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.bnorm.storyboard.text.highlight
2+
3+
import androidx.compose.ui.text.AnnotatedString
4+
5+
interface Highlighting {
6+
fun style(text: String): AnnotatedString
7+
}
8+
9+
fun String.style(
10+
highlighting: Highlighting,
11+
): AnnotatedString = highlighting.style(this)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package dev.bnorm.storyboard.text.highlight
2+
3+
import androidx.compose.runtime.Immutable
4+
import androidx.compose.ui.text.AnnotatedString
5+
import androidx.compose.ui.text.SpanStyle
6+
import androidx.compose.ui.text.buildAnnotatedString
7+
import dev.bnorm.storyboard.text.highlight.antlr.xml.XMLLexer
8+
import dev.bnorm.storyboard.text.highlight.antlr.xml.XMLParser
9+
import dev.bnorm.storyboard.text.highlight.antlr.xml.XMLParserBaseListener
10+
import org.antlr.v4.kotlinruntime.CharStreams
11+
import org.antlr.v4.kotlinruntime.CommonTokenStream
12+
import org.antlr.v4.kotlinruntime.ParserRuleContext
13+
import org.antlr.v4.kotlinruntime.Token
14+
import org.antlr.v4.kotlinruntime.tree.ParseTreeWalker
15+
16+
@Immutable
17+
class XmlHighlighting(
18+
val attributeName: SpanStyle,
19+
val attributeValue: SpanStyle,
20+
val comment: SpanStyle,
21+
val entityReference: SpanStyle,
22+
val prologue: SpanStyle,
23+
val tag: SpanStyle,
24+
val tagData: SpanStyle,
25+
val tagName: SpanStyle,
26+
) : Highlighting {
27+
companion object {
28+
fun build(
29+
base: SpanStyle = SpanStyle(),
30+
builder: Builder.() -> Unit,
31+
): XmlHighlighting {
32+
return Builder(
33+
base = XmlHighlighting(
34+
attributeName = base,
35+
attributeValue = base,
36+
comment = base,
37+
entityReference = base,
38+
prologue = base,
39+
tag = base,
40+
tagData = base,
41+
tagName = base,
42+
)
43+
).apply(builder).build()
44+
}
45+
}
46+
47+
class Builder internal constructor(base: XmlHighlighting) {
48+
var attributeName: SpanStyle = base.attributeName
49+
var attributeValue: SpanStyle = base.attributeValue
50+
var comment: SpanStyle = base.comment
51+
var entityReference: SpanStyle = base.entityReference
52+
var prologue: SpanStyle = base.prologue
53+
var tag: SpanStyle = base.tag
54+
var tagData: SpanStyle = base.tagData
55+
var tagName: SpanStyle = base.tagName
56+
57+
fun build(): XmlHighlighting {
58+
return XmlHighlighting(
59+
attributeName = attributeName,
60+
attributeValue = attributeValue,
61+
comment = comment,
62+
entityReference = entityReference,
63+
prologue = prologue,
64+
tag = tag,
65+
tagData = tagData,
66+
tagName = tagName,
67+
)
68+
}
69+
}
70+
71+
override fun style(text: String): AnnotatedString {
72+
return buildAnnotatedString {
73+
append(text)
74+
75+
val formatListener = object : XMLParserBaseListener() {
76+
override fun enterDocument(ctx: XMLParser.DocumentContext) {
77+
}
78+
79+
override fun enterProlog(ctx: XMLParser.PrologContext) {
80+
addStyle(prologue, ctx)
81+
}
82+
83+
override fun enterContent(ctx: XMLParser.ContentContext) {
84+
addStyle(tagData, ctx)
85+
}
86+
87+
override fun enterElement(ctx: XMLParser.ElementContext) {
88+
ctx.OPEN().forEach { addStyle(tagName, it.symbol) }
89+
ctx.Name().forEach { addStyle(tagName, it.symbol) }
90+
ctx.CLOSE().forEach { addStyle(tagName, it.symbol) }
91+
ctx.SLASH()?.let { addStyle(tagName, it.symbol) }
92+
ctx.SLASH_CLOSE()?.let { addStyle(tagName, it.symbol) }
93+
94+
}
95+
96+
override fun enterReference(ctx: XMLParser.ReferenceContext) {
97+
addStyle(entityReference, ctx)
98+
}
99+
100+
override fun enterAttribute(ctx: XMLParser.AttributeContext) {
101+
addStyle(attributeName, ctx.Name().symbol)
102+
addStyle(attributeValue, ctx.EQUALS().symbol)
103+
addStyle(attributeValue, ctx.STRING().symbol)
104+
}
105+
106+
override fun enterChardata(ctx: XMLParser.ChardataContext) {
107+
}
108+
109+
override fun enterMisc(ctx: XMLParser.MiscContext) {
110+
ctx.COMMENT()?.let { addStyle(comment, it.symbol) }
111+
}
112+
}
113+
114+
// Make sure the text ends with a new-line.
115+
val stream = CharStreams.fromString(text + "\n")
116+
val lexer = XMLLexer(stream)
117+
val parser = XMLParser(CommonTokenStream(lexer))
118+
parser.content()
119+
120+
val walker = ParseTreeWalker()
121+
walker.walk(formatListener, parser.document())
122+
}
123+
}
124+
}
125+
126+
private fun AnnotatedString.Builder.addStyle(style: SpanStyle, ctx: ParserRuleContext) {
127+
addStyle(style, ctx.start!!.startIndex, ctx.stop!!.stopIndex + 1)
128+
}
129+
130+
private fun AnnotatedString.Builder.addStyle(spanStyle: SpanStyle, token: Token) {
131+
addStyle(spanStyle, token.startIndex, token.stopIndex + 1)
132+
}

storyboard-text/src/commonMain/kotlin/dev/bnorm/storyboard/text/highlight/antlr/kotlin/KotlinLexer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import org.antlr.v4.kotlinruntime.misc.*
1212
"LocalVariableName",
1313
"ConstPropertyName",
1414
)
15-
public open class KotlinLexer(input: CharStream) : Lexer(input) {
15+
internal open class KotlinLexer(input: CharStream) : Lexer(input) {
1616
private companion object {
1717
init {
1818
RuntimeMetaData.checkVersion("4.13.1", RuntimeMetaData.runtimeVersion)

storyboard-text/src/commonMain/kotlin/dev/bnorm/storyboard/text/highlight/antlr/kotlin/KotlinParser.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import kotlin.jvm.JvmField
2222
"ConvertSecondaryConstructorToPrimary",
2323
"CanBeVal",
2424
)
25-
public open class KotlinParser(input: TokenStream) : Parser(input) {
25+
internal open class KotlinParser(input: TokenStream) : Parser(input) {
2626
private companion object {
2727
init {
2828
RuntimeMetaData.checkVersion("4.13.1", RuntimeMetaData.runtimeVersion)

storyboard-text/src/commonMain/kotlin/dev/bnorm/storyboard/text/highlight/antlr/kotlin/KotlinParserBaseListener.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import org.antlr.v4.kotlinruntime.tree.TerminalNode
1010
* which can be extended to create a listener which only needs to handle a subset
1111
* of the available methods.
1212
*/
13-
public open class KotlinParserBaseListener : KotlinParserListener {
13+
internal open class KotlinParserBaseListener : KotlinParserListener {
1414
/**
1515
* The default implementation does nothing.
1616
*/

storyboard-text/src/commonMain/kotlin/dev/bnorm/storyboard/text/highlight/antlr/kotlin/KotlinParserListener.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import org.antlr.v4.kotlinruntime.tree.ParseTreeListener
66
/**
77
* This interface defines a complete listener for a parse tree produced by [KotlinParser].
88
*/
9-
public interface KotlinParserListener : ParseTreeListener {
9+
internal interface KotlinParserListener : ParseTreeListener {
1010
/**
1111
* Enter a parse tree produced by [KotlinParser.kotlinFile].
1212
*

0 commit comments

Comments
 (0)