Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Parsing Inline Strings Across Platforms: Buildi...

Parsing Inline Strings Across Platforms: Building a Small Cross Platform Library by Vincent

by Vincent Salamanca-Gagnon

Formatting complex string representations in mobile applications often results in code that is error-prone and difficult to maintain. Encapsulating this logic into a Domain Specific Language (DSL) can simplify your codebase, but building a DSL and its parser can be intimidating.

This talk demystifies the process by introducing the fundamental concepts of constructing a small parser, using a practical use case to illustrate each step. Attendees will learn about building a lexer, parser construction, Abstract Syntax Tree (AST) generation, and error handling. The parsing logic is implemented within a Kotlin Multiplatform Library to accommodate platform-specific rendering, ensure consistency, and avoid code duplication across platforms.

https://youtu.be/xRoF7l1uNGo

DevFest Montreal 2024

GDG Montreal

November 14, 2024
Tweet

More Decks by GDG Montreal

Other Decks in Programming

Transcript

  1. Outline • Demonstrate library use case • Design decisions •

    Implementation of a string parser • Learn something new
  2. DevFest Montreal 2024 buildAnnotatedString { append("DevFest Montreal ") withStyle( style

    = SpanStyle(fontWeight = FontWeight.Bold) ) { append("2024") } }
  3. Design Guidelines • Avoid code duplication between platforms • Make

    it easy to extend and flexible • Minimize API surface • Performance
  4. Design Guidelines • Avoid code duplication between platforms • Make

    it easy to extend and flexible • Minimize API surface • Performance
  5. KMP

  6. KMP

  7. KMP

  8. KMP

  9. Design Considerations • Avoid code duplication between platforms • Make

    it easy to extend and flexible • Minimize API surface • Performance
  10. Design Considerations • Avoid code duplication between platforms ✅ •

    Make it easy to extend and flexible • Minimize API surface • Performance
  11. Design Considerations • Avoid code duplication between platforms ✅ •

    Make it easy to extend and flexible • Minimize API surface • Performance
  12. JSON? • Flexible ✅ • Typed ❌ • Ordering ❌

    • Performance ? {"menu": { "id": "file", "value": "File", "popup": { "menuitem": [ {"value": "New", "onclick": "CreateNewDoc()"}, {"value": "Open", "onclick": "OpenDoc()"}, {"value": "Close", "onclick": "CloseDoc()"} ] } } }
  13. HTML • Markup language ✅ <html> <body> <h1>My First Heading

    </ h1> <p>My first paragraph. </ p> < / body> < / html>
  14. Domain Specific Language (DSL) Stop: Sherbrooke 8 : 10PM <bold>Stop:

    </ bold> Sherbrooke <eta|1731182400|late> • Minimal ✅ • Flexible/Extensible ✅ • Typed ✅ • Performance*
  15. Tokens <NAME|PARAMETER>content </ NAME> • TAGBEGIN: <NAME • TAGEND: >

    • TAGCLOSE: </ NAME> • PARAMETER: between | • CONTENT: Text between tags
  16. Token Class sealed class Token { data class TagBegin(val name:

    String, val position: Int) : Token() data class TagEnd(val position: Int) : Token() data class TagClose(val name: String, val position: Int) : Token() data class Parameter(val value: String, val position: Int) : Token() data class Content(val text: String, val position: Int) : Token() }
  17. Lexer Class class Lexer(private val input: String) { private var

    position: Int = 0 private val length = input.length fun lex(): List<Token> { val tokens = mutableListOf<Token>() return tokens } }
  18. Lexer Class class Lexer(private val input: String) { private var

    position: Int = 0 private val length = input.length fun lex(): List<Token> { val tokens = mutableListOf<Token>() while (position < length) { val currentChar = input[position] if (currentChar == '<') { tokens.addAll(readTag()) } else { tokens.add(readContent()) } } return tokens } }
  19. private fun readContent(): Token.Content { val startPos = position val

    content = StringBuilder() return Token.Content(content.toString(), startPos) } readContent()
  20. private fun readContent(): Token.Content { val startPos = position val

    content = StringBuilder() while (position < length && input[position] != '<') { content.append(input[position]) position++ } return Token.Content(content.toString(), startPos) } readContent()
  21. private fun readTag(): List<Token> { val tokens = mutableListOf<Token>() val

    startPos = position position++ // Skip '<' return tokens } readTag()
  22. private fun readTag(): List<Token> { val tokens = mutableListOf<Token>() val

    startPos = position position++ // Skip '<' if (input[position] == '/') { position++ // Skip '/' val name = readName() tokens.add(Token.TagClose(name, startPos)) expectChar('>') position++ // Skip '>' } else { } return tokens } readTag()
  23. val name = readName() tokens.add(Token.TagBegin(name, startPos)) while (position < length

    && input[position] != '>') { } tokens.add(Token.TagEnd(position)) position++ // Skip '>' readTag()
  24. readTag() val name = readName() tokens.add(Token.TagBegin(name, startPos)) while (position <

    length && input[position] != '>') { if (input[position] == '|') { position++ // Skip '|' val param = readParameter() tokens.add(Token.Parameter(param, position)) } else if (input[position].isWhitespace()) { position++ // Skip whitespace } else { throw ParsingException("Unexpected character '${input[position]}' in tag", position) } } tokens.add(Token.TagEnd(position)) position++ // Skip '>'
  25. readName() private fun readName(): String { val nameBuilder = StringBuilder()

    while (position < length && input[position].isLetterOrDigit()) { nameBuilder.append(input[position]) position++ } if (nameBuilder.isEmpty()) { throw ParsingException("Tag name cannot be empty", position) } return nameBuilder.toString() }
  26. class ParsingException(message: String, val position: Int) : Exception("$message at position

    $position”) // ParsingException: Expected '>' at the end of tag at position 26 ParsingException
  27. Result [ Content(text="Hello, ", position=0), TagBegin(name="bold", position=7), Parameter(value="param1", position=13), Parameter(value="param2",

    position=20), TagEnd(position=26), Content(text="world", position=27), TagClose(name="bold", position=32), Content(text="!", position=38) ] Hello <bold|param1| param2>word < / bold>!
  28. StringElement interface StringElement data class TextElement(val content: String) : StringElement

    data class BoldElement(val content: List<StringElement>) : StringElement
  29. API

  30. Design Considerations • Avoid code duplication between platforms ✅ •

    Make it easy to extend and flexible ✅ • Minimize API surface • Performance
  31. Walking the tree [ StyledElement(content = "Hello, ", styles =

    {}), StyledElement(content = "world", styles = {Style.BOLD}), StyledElement(content = “8:10AM”, styles = {Style.BOLD, Style.COLOR}), … ]
  32. API // commonMain fun parseStyledText(input: String): List<StyledElement> { val tokens

    = Lexer(input).lex() // Lexing val ast = Parser(tokens).parse() // Parsing val styledElements = ast. fl atMap { element -> fl attenAST(element) } // Flattening return styledElements }
  33. API // commonMain fun parseStyledText(input: String): List<StyledElement> { val tokens

    = Lexer(input).lex() // Lexing val ast = Parser(tokens).parse() // Parsing val styledElements = ast. fl atMap { element -> fl attenAST(element) } // Flattening return styledElements } // Android @Composable fun InlineStringComposable(input: String) { val styledElements = parseStyledText(input) val annotatedString = buildAnnotatedString { styledElements.forEach { element -> // render styled element } } }
  34. Design Considerations • Avoid code duplication between platforms ✅ •

    Make it easy to extend and flexible ✅ • Minimize API surface ✅ • Performance