Skip to content

Commit 0b1e704

Browse files
respencer-nclclaude
andcommitted
Add getOutline and getTree methods to RiddlAPI
Implement OutlinePass and TreePass as HierarchyPass subclasses for extracting definition structure from parsed RIDDL models. OutlinePass produces a flat list with depth info; TreePass produces a recursive tree. Both are exposed via @JSExport in RiddlAPI with TypeScript declarations, for use in the ossum.ai node hierarchy panel. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 756d7d3 commit 0b1e704

4 files changed

Lines changed: 421 additions & 1 deletion

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2019-2026 Ossum, Inc.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package com.ossuminc.riddl.passes
8+
9+
import com.ossuminc.riddl.language.AST.*
10+
import com.ossuminc.riddl.language.Messages
11+
import com.ossuminc.riddl.utils.PlatformContext
12+
13+
import scala.collection.mutable
14+
15+
object OutlinePass extends PassInfo[PassOptions] {
16+
val name: String = "Outline"
17+
def creator(
18+
options: PassOptions = PassOptions.empty
19+
)(using PlatformContext): PassCreator = {
20+
(in: PassInput, out: PassesOutput) => OutlinePass(in, out)
21+
}
22+
}
23+
24+
case class OutlineEntry(
25+
kind: String,
26+
id: String,
27+
depth: Int,
28+
line: Int,
29+
col: Int,
30+
offset: Int
31+
)
32+
33+
case class OutlineOutput(
34+
root: PassRoot,
35+
messages: Messages.Messages,
36+
entries: Seq[OutlineEntry]
37+
) extends PassOutput
38+
39+
case class OutlinePass(
40+
input: PassInput,
41+
outputs: PassesOutput
42+
)(using PlatformContext)
43+
extends HierarchyPass(input, outputs) {
44+
45+
def name: String = OutlinePass.name
46+
47+
private val buffer: mutable.ListBuffer[OutlineEntry] =
48+
mutable.ListBuffer.empty
49+
50+
protected def openContainer(
51+
definition: Definition,
52+
parents: Parents
53+
): Unit = {
54+
if definition.id.nonEmpty then
55+
buffer.append(
56+
OutlineEntry(
57+
kind = definition.kind,
58+
id = definition.id.value,
59+
depth = parents.size,
60+
line = definition.loc.line,
61+
col = definition.loc.col,
62+
offset = definition.loc.offset
63+
)
64+
)
65+
end if
66+
}
67+
68+
protected def processLeaf(
69+
definition: Leaf,
70+
parents: Parents
71+
): Unit = {
72+
if definition.id.nonEmpty then
73+
buffer.append(
74+
OutlineEntry(
75+
kind = definition.kind,
76+
id = definition.id.value,
77+
depth = parents.size,
78+
line = definition.loc.line,
79+
col = definition.loc.col,
80+
offset = definition.loc.offset
81+
)
82+
)
83+
end if
84+
}
85+
86+
protected def processValue(
87+
value: RiddlValue,
88+
parents: Parents
89+
): Unit = ()
90+
91+
protected def closeContainer(
92+
definition: Definition,
93+
parents: Parents
94+
): Unit = ()
95+
96+
def result(root: PassRoot): OutlineOutput = {
97+
OutlineOutput(root, Messages.empty, buffer.toSeq)
98+
}
99+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2019-2026 Ossum, Inc.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package com.ossuminc.riddl.passes
8+
9+
import com.ossuminc.riddl.language.AST.*
10+
import com.ossuminc.riddl.language.Messages
11+
import com.ossuminc.riddl.utils.PlatformContext
12+
13+
import scala.collection.mutable
14+
15+
object TreePass extends PassInfo[PassOptions] {
16+
val name: String = "Tree"
17+
def creator(
18+
options: PassOptions = PassOptions.empty
19+
)(using PlatformContext): PassCreator = {
20+
(in: PassInput, out: PassesOutput) => TreePass(in, out)
21+
}
22+
}
23+
24+
case class TreeNode(
25+
kind: String,
26+
id: String,
27+
line: Int,
28+
col: Int,
29+
offset: Int,
30+
children: Seq[TreeNode]
31+
)
32+
33+
case class TreeOutput(
34+
root: PassRoot,
35+
messages: Messages.Messages,
36+
tree: Seq[TreeNode]
37+
) extends PassOutput
38+
39+
case class TreePass(
40+
input: PassInput,
41+
outputs: PassesOutput
42+
)(using PlatformContext)
43+
extends HierarchyPass(input, outputs) {
44+
45+
def name: String = TreePass.name
46+
47+
private val childrenMap: mutable.Map[Definition, mutable.ListBuffer[TreeNode]] =
48+
mutable.Map.empty
49+
50+
private val topLevel: mutable.ListBuffer[TreeNode] =
51+
mutable.ListBuffer.empty
52+
53+
protected def openContainer(
54+
definition: Definition,
55+
parents: Parents
56+
): Unit = {
57+
childrenMap(definition) = mutable.ListBuffer.empty
58+
}
59+
60+
protected def processLeaf(
61+
definition: Leaf,
62+
parents: Parents
63+
): Unit = {
64+
if definition.id.nonEmpty then
65+
val leaf = TreeNode(
66+
kind = definition.kind,
67+
id = definition.id.value,
68+
line = definition.loc.line,
69+
col = definition.loc.col,
70+
offset = definition.loc.offset,
71+
children = Seq.empty
72+
)
73+
if parents.nonEmpty then
74+
childrenMap.get(parents.head.asInstanceOf[Definition]) match {
75+
case Some(buf) => buf.append(leaf)
76+
case None => topLevel.append(leaf)
77+
}
78+
else topLevel.append(leaf)
79+
end if
80+
end if
81+
}
82+
83+
protected def processValue(
84+
value: RiddlValue,
85+
parents: Parents
86+
): Unit = ()
87+
88+
protected def closeContainer(
89+
definition: Definition,
90+
parents: Parents
91+
): Unit = {
92+
val children = childrenMap.remove(definition)
93+
.map(_.toSeq).getOrElse(Seq.empty)
94+
if definition.id.nonEmpty then
95+
val node = TreeNode(
96+
kind = definition.kind,
97+
id = definition.id.value,
98+
line = definition.loc.line,
99+
col = definition.loc.col,
100+
offset = definition.loc.offset,
101+
children = children
102+
)
103+
if parents.nonEmpty then
104+
childrenMap.get(parents.head.asInstanceOf[Definition]) match {
105+
case Some(buf) => buf.append(node)
106+
case None => topLevel.append(node)
107+
}
108+
else topLevel.append(node)
109+
end if
110+
end if
111+
}
112+
113+
def result(root: PassRoot): TreeOutput = {
114+
TreeOutput(root, Messages.empty, topLevel.toSeq)
115+
}
116+
}

riddlLib/js/src/main/scala/com/ossuminc/riddl/RiddlAPI.scala

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.ossuminc.riddl.language.AST.{Nebula, Root, Token}
1010
import com.ossuminc.riddl.language.{Contents, *}
1111
import com.ossuminc.riddl.language.Messages.Messages
1212
import com.ossuminc.riddl.language.parsing.{RiddlParserInput, TopLevelParser}
13-
import com.ossuminc.riddl.passes.Pass
13+
import com.ossuminc.riddl.passes.{Pass, PassInput, PassesOutput, OutlinePass, OutlineOutput, OutlineEntry, TreePass, TreeOutput, TreeNode}
1414
import com.ossuminc.riddl.utils.{CommonOptions, DOMPlatformContext, PlatformContext, URL}
1515

1616
import scala.scalajs.js
@@ -461,4 +461,123 @@ object RiddlAPI {
461461
import com.ossuminc.riddl.utils.InfoFormatter
462462
InfoFormatter.formatInfo
463463
}
464+
465+
/** Get a flat outline of all named definitions in RIDDL source.
466+
*
467+
* Returns a flat array of entries, each with kind, id, depth, and location.
468+
* Useful for building outline/table-of-contents views.
469+
*
470+
* @param source The RIDDL source code to outline
471+
* @param origin Optional origin identifier for error messages
472+
* @return Result object with { succeeded: boolean, value?: OutlineEntry[], errors?: Array }
473+
*/
474+
@JSExport("getOutline")
475+
def getOutline(
476+
source: String,
477+
origin: String = "string"
478+
): js.Dynamic = {
479+
val rpi = RiddlParserInput(source, originToURL(origin))
480+
val parseResult = TopLevelParser.parseInput(rpi)(using defaultContext)
481+
parseResult match {
482+
case Right(root) =>
483+
val passInput = PassInput(root)
484+
val passesResult = Pass.runThesePasses(
485+
passInput,
486+
Seq(OutlinePass.creator()(using defaultContext))
487+
)(using defaultContext)
488+
passesResult.outputs.outputOf[OutlineOutput](OutlinePass.name) match {
489+
case Some(outlineOutput) =>
490+
val entries = outlineOutput.entries.map { e =>
491+
js.Dynamic.literal(
492+
kind = e.kind,
493+
id = e.id,
494+
depth = e.depth,
495+
line = e.line,
496+
col = e.col,
497+
offset = e.offset
498+
)
499+
}.toJSArray
500+
js.Dynamic.literal(succeeded = true, value = entries)
501+
case None =>
502+
js.Dynamic.literal(
503+
succeeded = false,
504+
errors = js.Array(
505+
js.Dynamic.literal(
506+
kind = "Error",
507+
message = "OutlinePass produced no output",
508+
location = js.Dynamic.literal(
509+
line = 1, col = 1, offset = 0, source = origin
510+
)
511+
)
512+
)
513+
)
514+
}
515+
case Left(messages) =>
516+
js.Dynamic.literal(
517+
succeeded = false,
518+
errors = formatMessagesAsArray(messages)
519+
)
520+
}
521+
}
522+
523+
/** Get a recursive tree of all named definitions in RIDDL source.
524+
*
525+
* Returns a nested tree structure mirroring the RIDDL definition hierarchy.
526+
* Useful for building tree views or navigation panels.
527+
*
528+
* @param source The RIDDL source code to process
529+
* @param origin Optional origin identifier for error messages
530+
* @return Result object with { succeeded: boolean, value?: TreeNode[], errors?: Array }
531+
*/
532+
@JSExport("getTree")
533+
def getTree(
534+
source: String,
535+
origin: String = "string"
536+
): js.Dynamic = {
537+
val rpi = RiddlParserInput(source, originToURL(origin))
538+
val parseResult = TopLevelParser.parseInput(rpi)(using defaultContext)
539+
parseResult match {
540+
case Right(root) =>
541+
val passInput = PassInput(root)
542+
val passesResult = Pass.runThesePasses(
543+
passInput,
544+
Seq(TreePass.creator()(using defaultContext))
545+
)(using defaultContext)
546+
passesResult.outputs.outputOf[TreeOutput](TreePass.name) match {
547+
case Some(treeOutput) =>
548+
val nodes = treeOutput.tree.map(treeNodeToJs).toJSArray
549+
js.Dynamic.literal(succeeded = true, value = nodes)
550+
case None =>
551+
js.Dynamic.literal(
552+
succeeded = false,
553+
errors = js.Array(
554+
js.Dynamic.literal(
555+
kind = "Error",
556+
message = "TreePass produced no output",
557+
location = js.Dynamic.literal(
558+
line = 1, col = 1, offset = 0, source = origin
559+
)
560+
)
561+
)
562+
)
563+
}
564+
case Left(messages) =>
565+
js.Dynamic.literal(
566+
succeeded = false,
567+
errors = formatMessagesAsArray(messages)
568+
)
569+
}
570+
}
571+
572+
/** Convert a TreeNode to a JavaScript object recursively */
573+
private def treeNodeToJs(node: TreeNode): js.Dynamic = {
574+
js.Dynamic.literal(
575+
kind = node.kind,
576+
id = node.id,
577+
line = node.line,
578+
col = node.col,
579+
offset = node.offset,
580+
children = node.children.map(treeNodeToJs).toJSArray
581+
)
582+
}
464583
}

0 commit comments

Comments
 (0)