Skip to content

Commit

Permalink
Merge pull request #3 from ygdrasil-io/feature/add-dictionary-parsing
Browse files Browse the repository at this point in the history
Add support for parsing and modeling WebIDL dictionaries
  • Loading branch information
fabmax authored Feb 16, 2025
2 parents 704d648 + c47b7d2 commit 31b605f
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 4 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Tests

on:
push:
branches: [ main, 'release/**', 'feature/**', 'fix/**' ]
paths-ignore:
- '*.md'
- '*.yml'
- '.github/workflows/**'
pull_request:
branches: [ main, 'release/**', 'feature/**', 'fix/**' ]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
tests:
strategy:
fail-fast: false
matrix:
# add macos-latest when upgrading to JDK 17
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
env:
CI: "true"
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 8
cache: 'gradle'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-gradle
- name: assemble
run: ./gradlew assemble
- name: build
run: ./gradlew build
- name: check
run: ./gradlew check
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ class IdlAttribute private constructor(builder: Builder) : IdlDecoratedElement(b

fun build() = IdlAttribute(this)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package de.fabmax.webidl.model

class IdlDictionary(builder: Builder) : IdlDecoratedElement(builder) {
val members = List(builder.members.size) { builder.members[it].build() }
val superDictionaries = builder.superDictionaries.toList()
val sourcePackage = builder.sourcePackage

fun finishModel(parentModel: IdlModel) {
this.parentModel = parentModel
members.forEach { it.finishModel(this) }
}

override fun toString(indent: String): String {
val subIndent = "$indent "
val str = StringBuilder()
str.append(decoratorsToStringOrEmpty(indent, "\n"))
str.append("${indent}dictionary $name { ")

if (members.isNotEmpty()) {
str.append("\n")
str.append(members.joinToString("\n", postfix = "\n", transform = { it.toString(subIndent) }))
}
str.append("$indent};")
superDictionaries.forEach { str.append("\n$indent$name implements $it;") }
return str.toString()
}

class Builder(name: String) : IdlDecoratedElement.Builder(name) {
val members = mutableListOf<IdlMember.Builder>()
val superDictionaries = mutableSetOf<String>()
var sourcePackage = ""

fun addAttribute(attribute: IdlMember.Builder) { members += attribute }
fun build() = IdlDictionary(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ class IdlInterface(builder: Builder) : IdlDecoratedElement(builder) {
val superInterfaces = mutableSetOf<String>()
var sourcePackage = ""

fun addAttribute(attribute: IdlAttribute.Builder) { attributes += attribute }
fun addFunction(function: IdlFunction.Builder) { functions += function }
fun addAttribute(attribute: IdlAttribute.Builder) {
attributes += attribute
}

fun addFunction(function: IdlFunction.Builder) {
functions += function
}

fun build() = IdlInterface(this)
}
}
35 changes: 35 additions & 0 deletions webidl-util/src/main/kotlin/de/fabmax/webidl/model/IdlMember.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package de.fabmax.webidl.model

class IdlMember private constructor(builder: Builder) : IdlDecoratedElement(builder) {
val type = builder.type
val isRequired = builder.isRequired
var defaultValue: String? = builder.defaultValue

var parentDictionary: IdlDictionary? = null
private set

fun finishModel(parentDictionary: IdlDictionary?) {
this.parentDictionary = parentDictionary
this.parentModel = parentDictionary?.parentModel ?: error("Parent model missing")
}

override fun toString(indent: String): String {
val str = StringBuilder(indent)
str.append(decoratorsToStringOrEmpty(postfix = " "))
if (isRequired) {
str.append("required ")
}
str.append(type).append(" ").append(name)
if (defaultValue != null) { str.append(" = $defaultValue")}
str.append(";")
return str.toString()
}

class Builder(name: String, var type: IdlType) : IdlDecoratedElement.Builder(name) {
var isRequired = false
var defaultValue: String? = null

fun build() = IdlMember(this)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import java.util.*

class IdlModel private constructor(builder: Builder) : IdlElement(builder) {
val interfaces: List<IdlInterface>
val dictionaries: List<IdlDictionary>
val enums: List<IdlEnum>

val interfacesByName: Map<String, IdlInterface>

init {
interfaces = List(builder.interfaces.size) { builder.interfaces[it].build() }
dictionaries = List(builder.dictionaries.size) { builder.dictionaries[it].build() }
interfacesByName = interfaces.associateBy { it.name }
enums = List(builder.enums.size) { builder.enums[it].build() }

dictionaries.forEach { it.finishModel(this) }
interfaces.forEach { it.finishModel(this) }
enums.forEach { it.finishModel(this) }
}
Expand Down Expand Up @@ -57,10 +60,12 @@ class IdlModel private constructor(builder: Builder) : IdlElement(builder) {

class Builder : IdlElement.Builder("root") {
val interfaces = mutableListOf<IdlInterface.Builder>()
val dictionaries = mutableListOf<IdlDictionary.Builder>()
val implements = mutableListOf<Pair<String, String>>()
val enums = mutableListOf<IdlEnum.Builder>()

fun addInterface(idlInterface: IdlInterface.Builder) { interfaces += idlInterface }
fun addDictionary(idlDictionary: IdlDictionary.Builder) { dictionaries += idlDictionary }
fun addImplements(concreteInterface: String, superInterface: String) { implements += concreteInterface to superInterface }
fun addEnum(idlEnum: IdlEnum.Builder) { enums += idlEnum }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.fabmax.webidl.parser

import de.fabmax.webidl.model.IdlDictionary

class DictionaryParser(parserState: WebIdlParser.ParserState) : ElementParser(parserState,
WebIdlParserType.Dictionary
) {
lateinit var builder: IdlDictionary.Builder

override suspend fun parse(): String {
popToken("dictionary")

val interfaceName = popUntilPattern("\\{") ?: parserException("Failed parsing dictionary name")
builder = IdlDictionary.Builder(interfaceName.first)
builder.sourcePackage = parserState.sourcePackage
parserState.popDecorators(builder)
parserState.parentParser<RootParser>().builder.addDictionary(builder)

parseChildren("}")

popToken(";")
parserState.popParser()
return interfaceName.second
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package de.fabmax.webidl.parser

import de.fabmax.webidl.model.IdlMember

class MemberParser(parserState: WebIdlParser.ParserState) : ElementParser(parserState,
WebIdlParserType.Dictionary
) {
lateinit var builder: IdlMember.Builder

override suspend fun parse(): String {
val isRequired = popIfPresent("required")
val type = parseType()
val tokens = popUntilPattern(";") ?: parserException("Failed parsing member")
builder = if (tokens.first.contains("=")) {
val name = tokens.first.substringBefore("=").trim()
IdlMember.Builder(name, type).also {
it.defaultValue = tokens.first.substringAfter("=").trim()
}
} else IdlMember.Builder(tokens.first, type)
builder.isRequired = isRequired
parserState.popDecorators(builder)
parserState.parentParser<DictionaryParser>().builder.addAttribute(builder)

parserState.popParser()
return tokens.second
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,38 @@ import de.fabmax.webidl.model.IdlType

enum class WebIdlParserType {
Root {
override fun possibleChildren() = listOf(Interface, Enum, LineComment, BlockComment, Decorators, Implements)
override fun possibleChildren() = listOf(Interface, Enum, LineComment, BlockComment, Decorators, Implements, Dictionary)
override suspend fun matches(stream: WebIdlStream) = false
override fun newParser(parserState: WebIdlParser.ParserState) = RootParser(parserState)
},

Dictionary {
override fun possibleChildren() = listOf(Decorators, LineComment, BlockComment, Member)
override suspend fun matches(stream: WebIdlStream) = stream.startsWith("dictionary")
override fun newParser(parserState: WebIdlParser.ParserState) = parserState.pushParser(
DictionaryParser(
parserState
)
)
},

Member {
override fun possibleChildren(): List<WebIdlParserType> = emptyList()
override suspend fun matches(stream: WebIdlStream): Boolean {
var line = stream.pollUntilPattern(";")?.first ?: return false
if (line.startsWith("required")) {
line = line.substring("required".length + 1)
}
return IdlType.startsWithType(line)
}

override fun newParser(parserState: WebIdlParser.ParserState) = parserState.pushParser(
MemberParser(
parserState
)
)
},

Interface {
override fun possibleChildren() = listOf(Decorators, LineComment, BlockComment, Attribute, Function)
override suspend fun matches(stream: WebIdlStream) = stream.startsWith("interface")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ class WebIdlStream {
return buffer.startsWith(prefix)
}

suspend fun endWith(prefix: String): Boolean {
readCharacters(prefix.length)
return buffer.endsWith(prefix)
}

suspend fun pollUntilPattern(searchPattern: String, abortPattern: String? = null) =
pollUntilPattern(Regex(searchPattern), abortPattern?.let { Regex(it) })

Expand Down
8 changes: 8 additions & 0 deletions webidl-util/src/test/kotlin/ParserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ class ParserTest {
parser.finish()
}

assertTrue(model.dictionaries.size == 1)
assertTrue(model.interfaces.size == 4)


assertEquals("AnDictionary", model.dictionaries[0].name)
assertEquals("someMember", model.dictionaries[0].members[0].name)
assertTrue("requiredMember", model.dictionaries[0].members[1].isRequired)
assertEquals("someMemberWithDefaultValue", model.dictionaries[0].members[2].name)
assertEquals("\"my string\"", model.dictionaries[0].members[2].defaultValue)

assertEquals("AnInterface", model.interfaces[0].name)
assertTrue(model.interfaces[0].functions.size == 1)
assertEquals("aFunction", model.interfaces[0].functions[0].name)
Expand Down
8 changes: 7 additions & 1 deletion webidl-util/src/test/resources/test.idl
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ interface JavaErrorCallback {
void JavaErrorCallback();
void reportError(ErrorCode code, DOMString message);
AnotherInterface someOtherMethod(AnInterface anInterface);
};
};

dictionary AnDictionary {
unsigned long long someMember;
required unsigned long long requiredMember;
USVString someMemberWithDefaultValue = "my string";
};

0 comments on commit 31b605f

Please sign in to comment.