Skip to content

Commit 48ced61

Browse files
committed
utils: markd virtualdom renderer
1 parent 4cc82a6 commit 48ced61

File tree

1 file changed

+331
-0
lines changed

1 file changed

+331
-0
lines changed

src/utils/markd_vdom_renderer.cr

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# -----------------------------------------------------------------------
2+
# This file is part of MoonScript
3+
#
4+
# MoonSript is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# MoonSript is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with MoonSript. If not, see <https://www.gnu.org/licenses/>.
16+
#
17+
# Copyright (C) 2025 Krisna Pranav, MoonScript Developers
18+
# -----------------------------------------------------------------------
19+
20+
require "uri"
21+
22+
module MoonScript
23+
class VDOMRenderer
24+
enum Highlight
25+
MoonOnly
26+
None
27+
All
28+
end
29+
30+
def self.render(
31+
*,
32+
highlight : Highlight = Highlight::None,
33+
replacements : Array(Compiler::Compiled),
34+
document : Markd::Node,
35+
separator : String,
36+
js : Compiler::Js,
37+
) : Compiler::Compiled
38+
render(
39+
node: self.new.render(document, separator, highlight),
40+
replacements: replacements,
41+
separator: separator,
42+
js: js)
43+
end
44+
45+
def self.render_html(
46+
*,
47+
highlight : Highlight = Highlight::None,
48+
replacements : Array(String),
49+
document : Markd::Node,
50+
separator : String,
51+
)
52+
root =
53+
self.new.render(document, separator, highlight)
54+
55+
processor = uninitialized Node | String, HtmlBuilder -> Nil
56+
processor = ->(node : Node | String, builder : HtmlBuilder) do
57+
case node
58+
in String
59+
builder.text node
60+
in Node
61+
tag =
62+
case item = node.tag
63+
in Compiler::Builtin
64+
raise "WTF"
65+
in String
66+
item
67+
end
68+
69+
builder.tag(tag, node.attributes) do
70+
node.children.each do |child|
71+
processor.call(child, builder)
72+
end
73+
end
74+
end
75+
end
76+
77+
XML.build_fragment(indent: nil) do |xml|
78+
builder = HtmlBuilder.new(xml)
79+
root.children.each do |child|
80+
processor.call(child, builder)
81+
end
82+
end.strip
83+
end
84+
85+
def self.render(
86+
replacements : Array(Compiler::Compiled),
87+
node : Node | String,
88+
separator : String,
89+
js : Compiler::Js,
90+
) : Compiler::Compiled
91+
case node
92+
in String
93+
if node == separator
94+
replacements.shift
95+
else
96+
js.string(node.gsub("\\", "\\\\"))
97+
end
98+
in Node
99+
attributes =
100+
node
101+
.attributes
102+
.transform_values { |value| [%("#{value}")] of Compiler::Item }
103+
104+
children =
105+
node.children.map do |item|
106+
render(
107+
replacements: replacements,
108+
separator: separator,
109+
node: item,
110+
js: js)
111+
end
112+
113+
tag =
114+
case node.tag
115+
in Compiler::Builtin
116+
node.tag
117+
in String
118+
%('#{node.tag}')
119+
end
120+
121+
js.call(Compiler::Builtin::CreateElement, [
122+
[tag] of Compiler::Item,
123+
js.object(attributes),
124+
js.array(children),
125+
])
126+
end
127+
end
128+
129+
class Node
130+
property children : Array(Node | String)
131+
getter attributes : Hash(String, String)
132+
getter tag : Compiler::Builtin | String
133+
134+
def initialize(
135+
@tag : Compiler::Builtin | String, *,
136+
@attributes = {} of String => String,
137+
@children = [] of Node | String,
138+
)
139+
end
140+
end
141+
142+
HEADINGS = %w(h1 h2 h3 h4 h5 h6)
143+
144+
getter stack : Array(Node) = [] of Node
145+
146+
def render(
147+
document : Markd::Node,
148+
separator : String,
149+
highlight : Highlight,
150+
) : Node
151+
walker =
152+
document.walker
153+
154+
while event = walker.next
155+
node, entering = event
156+
157+
next if (grand_parent = node.parent?.try &.parent?) &&
158+
node.type == Markd::Node::Type::Paragraph &&
159+
grand_parent.type.list? &&
160+
grand_parent.data["tight"]
161+
162+
if entering
163+
item =
164+
case node.type
165+
in Markd::Node::Type::Document then Node.new(Compiler::Builtin::Fragment)
166+
in Markd::Node::Type::BlockQuote then Node.new("blockquote")
167+
in Markd::Node::Type::Strong then Node.new("strong")
168+
in Markd::Node::Type::Emphasis then Node.new("em")
169+
in Markd::Node::Type::Item then Node.new("li")
170+
in Markd::Node::Type::ThematicBreak then Node.new("hr")
171+
in Markd::Node::Type::LineBreak then Node.new("br")
172+
in Markd::Node::Type::Paragraph then Node.new("p")
173+
in Markd::Node::Type::Heading
174+
Node.new(HEADINGS[node.data["level"].as(Int32) - 1])
175+
in Markd::Node::Type::Code
176+
Node.new("code", children: [
177+
node.text.strip,
178+
] of Node | String)
179+
in Markd::Node::Type::Link
180+
Node.new("a", attributes: {
181+
"href" => node.data["destination"].as(String),
182+
"title" => node.data["title"].as(String).presence,
183+
}.compact)
184+
in Markd::Node::Type::Image
185+
Node.new("img", attributes: {
186+
"src" => node.data["destination"].as(String),
187+
"alt" => node.first_child.text,
188+
})
189+
in Markd::Node::Type::List
190+
attributes =
191+
{} of String => String
192+
193+
if start = node.data["start"].as(Int32)
194+
attributes["start"] = start.to_s unless start == 1
195+
end
196+
197+
Node.new(
198+
node.data["type"] == "bullet" ? "ul" : "ol",
199+
attributes: attributes)
200+
in Markd::Node::Type::CodeBlock
201+
languages =
202+
node.fence_language.try(&.split)
203+
204+
language =
205+
languages.try(&.first?).try(&.strip.presence)
206+
207+
attributes =
208+
{} of String => String
209+
210+
attributes["class"] =
211+
"language-#{language}" if language
212+
213+
children =
214+
if highlight != Highlight::None
215+
if (highlight == Highlight::MoonOnly && language == "moon") ||
216+
highlight == Highlight::All
217+
ast =
218+
Parser.parse_any(node.text.strip, "source.moon")
219+
220+
unless ast.nodes.empty?
221+
SemanticTokenizer
222+
.tokenize_with_lines(ast)
223+
.map do |parts|
224+
items =
225+
parts.map do |part|
226+
case part
227+
in String
228+
part
229+
in Tuple(SemanticTokenizer::TokenType, String)
230+
Node.new("span",
231+
attributes: {"class" => part[0].to_s.underscore},
232+
children: [part[1]] of Node | String)
233+
end
234+
end
235+
236+
Node.new("span",
237+
attributes: {"class" => "line"},
238+
children: items).as(Node | String)
239+
end
240+
end
241+
end || begin
242+
node.text.strip.split("\n").map do |part|
243+
Node.new("span",
244+
attributes: {"class" => "line"},
245+
children: [part] of Node | String
246+
).as(Node | String)
247+
end
248+
end
249+
else
250+
[node.text.strip] of Node | String
251+
end
252+
253+
Node.new("pre", children: [
254+
Node.new("code",
255+
attributes: attributes,
256+
children: children),
257+
] of Node | String)
258+
in Markd::Node::Type::HTMLInline,
259+
Markd::Node::Type::HTMLBlock,
260+
Markd::Node::Type::Text
261+
next if (parent = node.parent?) && parent.type.image?
262+
node.text
263+
in Markd::Node::Type::SoftBreak
264+
"\n"
265+
in Markd::Node::Type::CustomInLine,
266+
Markd::Node::Type::CustomBlock
267+
end
268+
269+
case item
270+
when String
271+
unless item.empty?
272+
stack.last?.try(&.children.concat(replace(item, separator)))
273+
end
274+
when Node
275+
stack.last?.try(&.children.concat(replace(item, separator)))
276+
end
277+
278+
case item
279+
when Node
280+
if !node.type.in?(
281+
Markd::Node::Type::ThematicBreak,
282+
Markd::Node::Type::LineBreak,
283+
Markd::Node::Type::CodeBlock,
284+
Markd::Node::Type::Code)
285+
stack.push(item)
286+
end
287+
end
288+
else
289+
case node.type
290+
when Markd::Node::Type::ThematicBreak,
291+
Markd::Node::Type::BlockQuote,
292+
Markd::Node::Type::Paragraph,
293+
Markd::Node::Type::CodeBlock,
294+
Markd::Node::Type::Emphasis,
295+
Markd::Node::Type::Document,
296+
Markd::Node::Type::Heading,
297+
Markd::Node::Type::Strong,
298+
Markd::Node::Type::Image,
299+
Markd::Node::Type::Item,
300+
Markd::Node::Type::Code,
301+
Markd::Node::Type::Link,
302+
Markd::Node::Type::List
303+
stack.pop if stack.size > 1
304+
end
305+
end
306+
end
307+
308+
stack.first
309+
end
310+
311+
def replace(item : String, separator : String) : Array(Node | String)
312+
if separator.size > 0 && item.includes?(separator)
313+
item
314+
.split(separator)
315+
.intersperse(separator)
316+
.map { |part| part.as(Node | String) }
317+
else
318+
[item] of Node | String
319+
end
320+
end
321+
322+
def replace(node : Node, separator : String) : Array(Node | String)
323+
node.children =
324+
node.children.flat_map do |item|
325+
replace(item, separator)
326+
end
327+
328+
[node] of Node | String
329+
end
330+
end
331+
end

0 commit comments

Comments
 (0)