mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
refactor: Moved markdown code to a separate packge.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
#+feature dynamic-literals
|
||||
package markdown
|
||||
|
||||
import "core:fmt"
|
||||
import "core:strings"
|
||||
|
||||
ALERT_STYLES: map[string]string = {
|
||||
"note" = "border-l-blue-500 bg-blue-950/30 text-slate-300",
|
||||
"tip" = "border-l-green-500 bg-green-950/30 text-slate-300",
|
||||
"important" = "border-l-purple-500 bg-purple-950/30 text-slate-300",
|
||||
"warning" = "border-l-yellow-500 bg-yellow-950/30 text-slate-300",
|
||||
"caution" = "border-l-red-500 bg-red-950/30 text-slate-300",
|
||||
}
|
||||
|
||||
ALERT_EMOJIS: map[string]string = {
|
||||
"note" = "\xe2\x84\xb9\xef\xb8\x8f",
|
||||
"tip" = "\xf0\x9f\x92\xa1",
|
||||
"important" = "\xe2\x9d\x97",
|
||||
"warning" = "\xe2\x9a\xa0\xef\xb8\x8f",
|
||||
"caution" = "\xe2\x9d\x97",
|
||||
}
|
||||
|
||||
inject_alerts :: proc(html: string) -> string {
|
||||
sb := strings.builder_make()
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
remaining := html
|
||||
|
||||
for {
|
||||
bq_start := strings.index(remaining, "<blockquote>")
|
||||
if bq_start < 0 {
|
||||
strings.write_string(&sb, remaining)
|
||||
break
|
||||
}
|
||||
|
||||
bq_close := strings.index(remaining, "</blockquote>")
|
||||
if bq_close < 0 {
|
||||
strings.write_string(&sb, remaining)
|
||||
break
|
||||
}
|
||||
|
||||
bq_end := bq_close + len("</blockquote>")
|
||||
bq := remaining[bq_start:bq_end]
|
||||
|
||||
strings.write_string(&sb, remaining[:bq_start])
|
||||
strings.write_string(&sb, transform_alert(bq))
|
||||
|
||||
remaining = remaining[bq_end:]
|
||||
}
|
||||
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
transform_alert :: proc(bq: string) -> string {
|
||||
// Skip <blockquote> tag and whitespace
|
||||
pos := len("<blockquote>")
|
||||
for pos < len(bq) && (bq[pos] == '\n' || bq[pos] == '\r' || bq[pos] == ' ' || bq[pos] == '\t') {
|
||||
pos += 1
|
||||
}
|
||||
|
||||
// Check for <p>[!
|
||||
if pos + 4 >= len(bq) || bq[pos] != '<' || bq[pos + 1] != 'p' || bq[pos + 2] != '>' ||
|
||||
bq[pos + 3] != '[' || bq[pos + 4] != '!' {
|
||||
return bq
|
||||
}
|
||||
|
||||
// Extract alert type until ]
|
||||
close := strings.index(bq[pos + 5:], "]")
|
||||
if close < 0 {
|
||||
return bq
|
||||
}
|
||||
|
||||
type_raw := bq[pos + 5 : pos + 5 + close]
|
||||
type_lower := strings.to_lower(type_raw)
|
||||
|
||||
style, has_style := ALERT_STYLES[type_lower]
|
||||
emoji, has_emoji := ALERT_EMOJIS[type_lower]
|
||||
if !has_style || !has_emoji {
|
||||
return bq
|
||||
}
|
||||
|
||||
// Position after ]
|
||||
after_type := pos + 5 + close + 1
|
||||
|
||||
// Skip optional + or -
|
||||
content_start := after_type
|
||||
if content_start < len(bq) && (bq[content_start] == '+' || bq[content_start] == '-') {
|
||||
content_start += 1
|
||||
}
|
||||
// Skip space after marker
|
||||
if content_start < len(bq) && bq[content_start] == ' ' {
|
||||
content_start += 1
|
||||
}
|
||||
|
||||
// Rebuild: styled blockquote + bold title paragraph + rest
|
||||
rest := bq[content_start:]
|
||||
|
||||
return fmt.aprintf(
|
||||
`<blockquote class="alert %s rounded-r py-2">
|
||||
<p class="font-bold mb-1">%s %s`,
|
||||
style,
|
||||
emoji,
|
||||
rest,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
#+feature dynamic-literals
|
||||
package markdown
|
||||
|
||||
import "core:strings"
|
||||
|
||||
EMOJIS: map[string]string = {
|
||||
// People
|
||||
"smile" = "\xf0\x9f\x98\x84",
|
||||
"laughing" = "\xf0\x9f\x98\x86",
|
||||
"blush" = "\xf0\x9f\x98\x8a",
|
||||
"smiley" = "\xf0\x9f\x98\x83",
|
||||
"wink" = "\xf0\x9f\x98\x89",
|
||||
"joy" = "\xf0\x9f\xa4\xa3",
|
||||
"rofl" = "\xf0\x9f\xa4\xa3",
|
||||
"relaxed" = "\xe2\x98\xba\xef\xb8\x8f",
|
||||
"thinking" = "\xf0\x9f\xa4\x94",
|
||||
"neutral_face" = "\xf0\x9f\x98\x90",
|
||||
"expressionless" = "\xf0\x9f\x98\x91",
|
||||
"no_mouth" = "\xf0\x9f\x98\xb6",
|
||||
"rolling_eyes" = "\xf0\x9f\x99\x84",
|
||||
"smirk" = "\xf0\x9f\x98\x8f",
|
||||
"persevere" = "\xf0\x9f\x98\xa3",
|
||||
"disappointed_relieved" = "\xf0\x9f\x98\xa5",
|
||||
"open_mouth" = "\xf0\x9f\x98\xae",
|
||||
"zipper_mouth_face" = "\xf0\x9f\xa4\x90",
|
||||
"hushed" = "\xf0\x9f\x98\xaf",
|
||||
"sleepy" = "\xf0\x9f\x98\xaa",
|
||||
"tired_face" = "\xf0\x9f\x98\xab",
|
||||
"sleeping" = "\xf0\x9f\x98\xb4",
|
||||
"relieved" = "\xf0\x9f\x98\x8c",
|
||||
"stuck_out_tongue" = "\xf0\x9f\x98\x9b",
|
||||
"stuck_out_tongue_winking_eye" = "\xf0\x9f\x98\x9c",
|
||||
"stuck_out_tongue_closed_eyes" = "\xf0\x9f\x98\x9d",
|
||||
"drooling_face" = "\xf0\x9f\xa4\xa4",
|
||||
"unamused" = "\xf0\x9f\x98\x92",
|
||||
"sweat" = "\xf0\x9f\x98\x85",
|
||||
"pensive" = "\xf0\x9f\x98\x94",
|
||||
"confused" = "\xf0\x9f\x98\x95",
|
||||
"upside_down_face" = "\xf0\x9f\x99\x83",
|
||||
"money_mouth_face" = "\xf0\x9f\xa4\x91",
|
||||
"astonished" = "\xf0\x9f\x98\xb2",
|
||||
"white_frowning_face" = "\xe2\x98\xb9\xef\xb8\x8f",
|
||||
"slightly_sad_face" = "\xe2\x98\xb9\xef\xb8\x8f",
|
||||
"confounded" = "\xf0\x9f\x98\x96",
|
||||
"disappointed" = "\xf0\x9f\x98\x9e",
|
||||
"worried" = "\xf0\x9f\x98\x9f",
|
||||
"triumph" = "\xf0\x9f\x98\xa4",
|
||||
"cry" = "\xf0\x9f\x98\xa2",
|
||||
"sob" = "\xf0\x9f\x98\xad",
|
||||
"frowning" = "\xf0\x9f\x98\xa6",
|
||||
"frowning_face" = "\xf0\x9f\x99\x81",
|
||||
"anguished" = "\xf0\x9f\x98\xa7",
|
||||
"fearful" = "\xf0\x9f\x98\xa8",
|
||||
"weary" = "\xf0\x9f\x98\xa9",
|
||||
"grimacing" = "\xf0\x9f\x98\xac",
|
||||
"cold_sweat" = "\xf0\x9f\x98\xb0",
|
||||
"scream" = "\xf0\x9f\x98\xb1",
|
||||
"flushed" = "\xf0\x9f\x98\xb3",
|
||||
"dizzy_face" = "\xf0\x9f\x98\xb5",
|
||||
"rage" = "\xf0\x9f\x98\xa1",
|
||||
"angry" = "\xf0\x9f\x98\xa0",
|
||||
"innocent" = "\xf0\x9f\x98\x87",
|
||||
"cowboy_hat_face" = "\xf0\x9f\xa4\xa0",
|
||||
"clown_face" = "\xf0\x9f\xa4\xa1",
|
||||
"mask" = "\xf0\x9f\x98\xb7",
|
||||
"thermometer_face" = "\xf0\x9f\xa4\x92",
|
||||
"head_bandage" = "\xf0\x9f\xa4\x95",
|
||||
"nauseated_face" = "\xf0\x9f\xa4\xa2",
|
||||
"sneezing_face" = "\xf0\x9f\xa4\xa7",
|
||||
"smiling_imp" = "\xf0\x9f\x98\x88",
|
||||
"imp" = "\xf0\x9f\x91\xbf",
|
||||
"shrug" = "\xc2\xaf\\_(\xe3\x83\x84)_/\xc2\xaf",
|
||||
"facepalm" = "\xf0\x9f\xa4\xa6",
|
||||
"facepunch" = "\xf0\x9f\x91\x8a",
|
||||
"wave" = "\xf0\x9f\x91\x8b",
|
||||
"ok_hand" = "\xf0\x9f\x91\x8c",
|
||||
"thumbsup" = "\xf0\x9f\x91\x8d",
|
||||
"thumbsdown" = "\xf0\x9f\x91\x8e",
|
||||
"clap" = "\xf0\x9f\x91\x8f",
|
||||
"pray" = "\xf0\x9f\x99\x8f",
|
||||
"point_up" = "\xe2\x98\x9d\xef\xb8\x8f",
|
||||
"point_down" = "\xf0\x9f\x91\x87",
|
||||
"point_left" = "\xf0\x9f\x91\x88",
|
||||
"point_right" = "\xf0\x9f\x89\x89",
|
||||
"v" = "\xe2\x9c\x8c\xef\xb8\x8f",
|
||||
"raised_hands" = "\xf0\x9f\x99\x8c",
|
||||
"muscle" = "\xf0\x9f\x92\xaa",
|
||||
"fist" = "\xe2\x9c\x8a",
|
||||
"hand" = "\xe2\x9c\x8b",
|
||||
|
||||
// Hearts & symbols
|
||||
"heart" = "\xe2\x9d\xa4\xef\xb8\x8f",
|
||||
"orange_heart" = "\xf0\x9f\xa7\xa1",
|
||||
"yellow_heart" = "\xf0\x9f\x92\x9b",
|
||||
"green_heart" = "\xf0\x9f\x92\x9a",
|
||||
"blue_heart" = "\xf0\x9f\x92\x99",
|
||||
"purple_heart" = "\xf0\x9f\x92\x9c",
|
||||
"broken_heart" = "\xf0\x9f\x92\x94",
|
||||
"sparkling_heart" = "\xf0\x9f\x92\x96",
|
||||
"100" = "\xf0\x9f\x92\xaf",
|
||||
"anger" = "\xf0\x9f\x92\xa2",
|
||||
"checkered_flag" = "\xf0\x9f\x8f\x81",
|
||||
"crossed_flags" = "\xf0\x9f\x9a\xa9",
|
||||
"rocket" = "\xf0\x9f\x9a\x80",
|
||||
"star" = "\xe2\xad\x90",
|
||||
"star2" = "\xf0\x9f\x8c\x9f",
|
||||
"sparkles" = "\xe2\x9c\xa8",
|
||||
"boom" = "\xf0\x9f\x92\xa5",
|
||||
"exclamation" = "\xe2\x9d\x97",
|
||||
"question" = "\xe2\x9d\x93",
|
||||
"grey_exclamation" = "\xe2\x9d\x95",
|
||||
"grey_question" = "\xe2\x9d\x94",
|
||||
"zzz" = "\xf0\x9f\x92\xa4",
|
||||
"warning" = "\xe2\x9a\xa0\xef\xb8\x8f",
|
||||
"no_entry_sign" = "\xf0\x9f\x9a\xab",
|
||||
"no_entry" = "\xe2\x9b\x94",
|
||||
"white_check_mark" = "\xe2\x9c\x85",
|
||||
"negative_squared_cross_mark" = "\xe2\x9d\x8e",
|
||||
"x" = "\xe2\x9d\x8c",
|
||||
"o" = "\xe2\xad\x95",
|
||||
"heavy_plus_sign" = "\xe2\x9e\x95",
|
||||
"heavy_minus_sign" = "\xe2\x9e\x96",
|
||||
"heavy_division_sign" = "\xe2\x9e\x97",
|
||||
"copyright" = "\xc2\xa9\xef\xb8\x8f",
|
||||
"registered" = "\xc2\xae\xef\xb8\x8f",
|
||||
"tm" = "\xe2\x84\xa2\xef\xb8\x8f",
|
||||
|
||||
// Nature & weather
|
||||
"fire" = "\xf0\x9f\x94\xa5",
|
||||
"zap" = "\xe2\x9a\xa1",
|
||||
"sunny" = "\xe2\x98\x80\xef\xb8\x8f",
|
||||
"cloud" = "\xe2\x98\x81\xef\xb8\x8f",
|
||||
"rainbow" = "\xf0\x9f\x8c\x88",
|
||||
"snowflake" = "\xe2\x9d\x84\xef\xb8\x8f",
|
||||
"dash" = "\xf0\x9f\x92\xa8",
|
||||
"tornado" = "\xf0\x9f\x8c\xaa\xef\xb8\x8f",
|
||||
"deciduous_tree" = "\xf0\x9f\x8c\xb3",
|
||||
"evergreen_tree" = "\xf0\x9f\x8c\xb2",
|
||||
"palm_tree" = "\xf0\x9f\x8c\xb4",
|
||||
"cactus" = "\xf0\x9f\x8c\xb5",
|
||||
"tulip" = "\xf0\x9f\x8c\xb7",
|
||||
"rose" = "\xf0\x9f\x8c\xb9",
|
||||
"sunflower" = "\xf0\x9f\x8c\xbb",
|
||||
"hibiscus" = "\xf0\x9f\x8c\xba",
|
||||
"earth_africa" = "\xf0\x9f\x8c\x8d",
|
||||
"earth_americas" = "\xf0\x9f\x8c\x8e",
|
||||
"earth_asia" = "\xf0\x9f\x8c\x8f",
|
||||
"new_moon" = "\xf0\x9f\x8c\x9a",
|
||||
"full_moon" = "\xf0\x9f\x8c\x95",
|
||||
|
||||
// Food & drink
|
||||
"coffee" = "\xe2\x98\x95",
|
||||
"tea" = "\xf0\x9f\x8d\xb5",
|
||||
"beer" = "\xf0\x9f\x8d\xba",
|
||||
"beers" = "\xf0\x9f\x8d\xbb",
|
||||
"wine_glass" = "\xf0\x9f\x8d\xb7",
|
||||
"tropical_drink" = "\xf0\x9f\x8d\xb9",
|
||||
"apple" = "\xf0\x9f\x8d\x8e",
|
||||
"green_apple" = "\xf0\x9f\x8d\x8f",
|
||||
"orange" = "\xf0\x9f\x8d\x8a",
|
||||
"lemon" = "\xf0\x9f\x8d\x8b",
|
||||
"banana" = "\xf0\x9f\x8d\x8c",
|
||||
"watermelon" = "\xf0\x9f\x8d\x89",
|
||||
"grapes" = "\xf0\x9f\x8d\x87",
|
||||
"strawberry" = "\xf0\x9f\x8d\x93",
|
||||
"melon" = "\xf0\x9f\x8d\x88",
|
||||
"cherries" = "\xf0\x9f\x8d\x92",
|
||||
"peach" = "\xf0\x9f\x8d\x91",
|
||||
"pineapple" = "\xf0\x9f\x8d\x8d",
|
||||
"pizza" = "\xf0\x9f\x8d\x95",
|
||||
"hamburger" = "\xf0\x9f\x8d\x94",
|
||||
"hotdog" = "\xf0\x9f\x8c\xad",
|
||||
"taco" = "\xf0\x9f\x8c\xae",
|
||||
"burrito" = "\xf0\x9f\x8c\xaf",
|
||||
"cake" = "\xf0\x9f\x8d\xb0",
|
||||
"cookie" = "\xf0\x9f\x8d\xaa",
|
||||
"chocolate_bar" = "\xf0\x9f\x8d\xab",
|
||||
"candy" = "\xf0\x9f\x8d\xac",
|
||||
"popcorn" = "\xf0\x9f\x8d\xbf",
|
||||
|
||||
// Animals
|
||||
"dog" = "\xf0\x9f\x90\xb6",
|
||||
"cat" = "\xf0\x9f\x90\xb1",
|
||||
"mouse" = "\xf0\x9f\x90\xad",
|
||||
"hamster" = "\xf0\x9f\x90\xb9",
|
||||
"rabbit" = "\xf0\x9f\x90\xb0",
|
||||
"bear" = "\xf0\x9f\x90\xbb",
|
||||
"panda_face" = "\xf0\x9f\x90\xbc",
|
||||
"koala" = "\xf0\x9f\x90\xa8",
|
||||
"tiger" = "\xf0\x9f\x90\xaf",
|
||||
"lion_face" = "\xf0\x9f\xa6\x81",
|
||||
"cow" = "\xf0\x9f\x90\xae",
|
||||
"pig" = "\xf0\x9f\x90\xb7",
|
||||
"frog" = "\xf0\x9f\x90\xb8",
|
||||
"monkey_face" = "\xf0\x9f\x90\xb5",
|
||||
"see_no_evil" = "\xf0\x9f\x99\x88",
|
||||
"hear_no_evil" = "\xf0\x9f\x99\x89",
|
||||
"speak_no_evil" = "\xf0\x9f\x99\x8a",
|
||||
"owl" = "\xf0\x9f\xa6\x89",
|
||||
"bat" = "\xf0\x9f\xa6\x87",
|
||||
"wolf" = "\xf0\x9f\x90\xba",
|
||||
"boar" = "\xf0\x9f\x90\x97",
|
||||
"horse" = "\xf0\x9f\x90\x8e",
|
||||
"unicorn" = "\xf0\x9f\xa6\x84",
|
||||
"honeybee" = "\xf0\x9f\x90\x9d",
|
||||
"bug" = "\xf0\x9f\x90\x9b",
|
||||
"snail" = "\xf0\x9f\x90\x8c",
|
||||
"beetle" = "\xf0\x9f\x90\x9e",
|
||||
"ant" = "\xf0\x9f\x90\x9c",
|
||||
"spider" = "\xf0\x9f\x95\xb7",
|
||||
"scorpion" = "\xf0\x9f\xa6\x82",
|
||||
"turtle" = "\xf0\x9f\x90\xa2",
|
||||
"snake" = "\xf0\x9f\x90\x8d",
|
||||
"octopus" = "\xf0\x9f\x90\x99",
|
||||
"shell" = "\xf0\x9f\x90\x9a",
|
||||
"whale" = "\xf0\x9f\x90\xb3",
|
||||
"dolphin" = "\xf0\x9f\x90\xac",
|
||||
"fish" = "\xf0\x9f\x90\x9f",
|
||||
"tropical_fish" = "\xf0\x9f\x90\xa0",
|
||||
"blowfish" = "\xf0\x9f\x90\xa1",
|
||||
"penguin" = "\xf0\x9f\x90\xa7",
|
||||
"chicken" = "\xf0\x9f\x90\x94",
|
||||
"bird" = "\xf0\x9f\x90\xa6",
|
||||
"eagle" = "\xf0\x9f\xa6\x85",
|
||||
|
||||
// Objects
|
||||
"computer" = "\xf0\x9f\x92\xbb",
|
||||
"iphone" = "\xf0\x9f\x93\xb1",
|
||||
"keyboard" = "\xe2\x8c\xa8\xef\xb8\x8f",
|
||||
"desktop_computer" = "\xf0\x9f\x96\xa5",
|
||||
"printer" = "\xf0\x9f\x96\xa8",
|
||||
"mouse_three_button" = "\xf0\x9f\x96\xb1",
|
||||
"joystick" = "\xf0\x9f\x95\xb9",
|
||||
"stash" = "\xf0\x9f\x92\xbe",
|
||||
"floppy_disk" = "\xf0\x9f\x92\xbe",
|
||||
"cd" = "\xf0\x9f\x92\xbf",
|
||||
"dvd" = "\xf0\x9f\x93\x80",
|
||||
"vhs" = "\xf0\x9f\x93\xbc",
|
||||
"camera" = "\xf0\x9f\x93\xb7",
|
||||
"video_camera" = "\xf0\x9f\x93\xb9",
|
||||
"tv" = "\xf0\x9f\x93\xba",
|
||||
"radio" = "\xf0\x9f\x93\xbb",
|
||||
"pager" = "\xf0\x9f\x93\x9f",
|
||||
"telephone" = "\xe2\x98\x8e\xef\xb8\x8f",
|
||||
"fax" = "\xf0\x9f\x93\xa0",
|
||||
"bulb" = "\xf0\x9f\x92\xa1",
|
||||
"candle" = "\xf0\x9f\x95\xaf",
|
||||
"bathtub" = "\xf0\x9f\x9b\x81",
|
||||
"shower" = "\xf0\x9f\x9a\xbf",
|
||||
"toilet" = "\xf0\x9f\x9a\xbd",
|
||||
"wrench" = "\xf0\x9f\x94\xa7",
|
||||
"hammer" = "\xf0\x9f\x94\xa8",
|
||||
"nut_and_bolt" = "\xf0\x9f\x94\xa9",
|
||||
"gear" = "\xe2\x9a\x99\xef\xb8\x8f",
|
||||
"link" = "\xf0\x9f\x94\x97",
|
||||
"lock" = "\xf0\x9f\x94\x92",
|
||||
"unlock" = "\xf0\x9f\x94\x93",
|
||||
"key" = "\xf0\x9f\x94\x91",
|
||||
"bell" = "\xf0\x9f\x94\x94",
|
||||
"bookmark" = "\xf0\x9f\x94\x96",
|
||||
"pushpin" = "\xf0\x9f\x93\x8c",
|
||||
"paperclip" = "\xf0\x9f\x93\x8e",
|
||||
"memo" = "\xf0\x9f\x93\x9d",
|
||||
"pencil2" = "\xe2\x9c\x8f\xef\xb8\x8f",
|
||||
"black_nib" = "\xe2\x9c\x92\xef\xb8\x8f",
|
||||
"pen" = "\xf0\x9f\x96\x8a",
|
||||
"paintbrush" = "\xf0\x9f\x96\x8c",
|
||||
"crayon" = "\xf0\x9f\x96\x8d",
|
||||
"book" = "\xf0\x9f\x93\x97",
|
||||
"books" = "\xf0\x9f\x93\x9a",
|
||||
"ledger" = "\xf0\x9f\x93\x92",
|
||||
"notebook" = "\xf0\x9f\x93\x93",
|
||||
"scroll" = "\xf0\x9f\x93\x9c",
|
||||
"page_with_curl" = "\xf0\x9f\x93\x84",
|
||||
"newspaper" = "\xf0\x9f\x93\xb0",
|
||||
"chart_with_upwards_trend" = "\xf0\x9f\x93\x88",
|
||||
"chart_with_downwards_trend" = "\xf0\x9f\x93\x89",
|
||||
"bar_chart" = "\xf0\x9f\x93\x8a",
|
||||
"calendar" = "\xf0\x9f\x93\x85",
|
||||
"date" = "\xf0\x9f\x93\x85",
|
||||
"hourglass" = "\xe2\x8f\xb3",
|
||||
"hourglass_flowing_sand" = "\xe2\x8f\xb3",
|
||||
"clock" = "\xf0\x9f\x95\x90",
|
||||
"alarm_clock" = "\xe2\x8f\xb0",
|
||||
"stopwatch" = "\xe2\x8f\xb1",
|
||||
"watch" = "\xe2\x8c\x9a",
|
||||
"moneybag" = "\xf0\x9f\x92\xb0",
|
||||
"yen" = "\xf0\x9f\x92\xb4",
|
||||
"dollar" = "\xf0\x9f\x92\xb5",
|
||||
"euro" = "\xf0\x9f\x92\xb6",
|
||||
"pound" = "\xf0\x9f\x92\xb7",
|
||||
"money_with_wings" = "\xf0\x9f\x92\xb8",
|
||||
"credit_card" = "\xf0\x9f\x92\xb3",
|
||||
"gem" = "\xf0\x9f\x92\x8e",
|
||||
"bomb" = "\xf0\x9f\x92\xa3",
|
||||
"gift" = "\xf0\x9f\x8e\x81",
|
||||
"balloon" = "\xf0\x9f\x8e\x88",
|
||||
"tada" = "\xf0\x9f\x8e\x89",
|
||||
"confetti_ball" = "\xf0\x9f\x8e\x8a",
|
||||
"package" = "\xf0\x9f\x93\xa6",
|
||||
"mailbox" = "\xf0\x9f\x93\xab",
|
||||
"inbox_tray" = "\xf0\x9f\x93\xa5",
|
||||
"outbox_tray" = "\xf0\x9f\x93\xa4",
|
||||
"email" = "\xe2\x9c\x89\xef\xb8\x8f",
|
||||
"envelope" = "\xe2\x9c\x89\xef\xb8\x8f",
|
||||
"incoming_envelope" = "\xf0\x9f\x93\xa8",
|
||||
|
||||
// Activities
|
||||
"soccer" = "\xe2\x9a\xbd",
|
||||
"basketball" = "\xf0\x9f\x8f\x80",
|
||||
"football" = "\xf0\x9f\x8f\x88",
|
||||
"baseball" = "\xe2\x9a\xbe",
|
||||
"tennis" = "\xf0\x9f\x8e\xbe",
|
||||
"8ball" = "\xf0\x9f\x8e\xb1",
|
||||
"bowling" = "\xf0\x9f\x8e\xb3",
|
||||
"video_game" = "\xf0\x9f\x8e\xae",
|
||||
"dart" = "\xf0\x9f\x8e\xaf",
|
||||
"game_die" = "\xf0\x9f\x8e\xb2",
|
||||
"slot_machine" = "\xf0\x9f\x8e\xb0",
|
||||
"cards" = "\xf0\x9f\x83\x8f",
|
||||
"black_joker" = "\xf0\x9f\x83\x9f",
|
||||
"mahjong" = "\xf0\x9f\x80\x84",
|
||||
"musical_note" = "\xf0\x9f\x8e\xb5",
|
||||
"notes" = "\xf0\x9f\x8e\xb6",
|
||||
"saxophone" = "\xf0\x9f\x8e\xb7",
|
||||
"guitar" = "\xf0\x9f\x8e\xb8",
|
||||
"musical_keyboard" = "\xf0\x9f\x8e\xb9",
|
||||
"trumpet" = "\xf0\x9f\x8e\xba",
|
||||
"violin" = "\xf0\x9f\x8e\xbb",
|
||||
"drum" = "\xf0\x9f\xa5\x81",
|
||||
"headphones" = "\xf0\x9f\x8e\xa7",
|
||||
"microphone" = "\xf0\x9f\x8e\xa4",
|
||||
"level_slider" = "\xf0\x9f\x8e\x9a",
|
||||
"control_knobs" = "\xf0\x9f\x8e\x9b",
|
||||
"ticket" = "\xf0\x9f\x8e\xab",
|
||||
"art" = "\xf0\x9f\x8e\xa8",
|
||||
"circus_tent" = "\xf0\x9f\x8e\xaa",
|
||||
"theater_masks" = "\xf0\x9f\x8e\xad",
|
||||
"clapper" = "\xf0\x9f\x8e\xac",
|
||||
|
||||
// Travel
|
||||
"car" = "\xf0\x9f\x9a\x97",
|
||||
"taxi" = "\xf0\x9f\x9a\x95",
|
||||
"bus" = "\xf0\x9f\x9a\x8d",
|
||||
"train" = "\xf0\x9f\x9a\x86",
|
||||
"metro" = "\xf0\x9f\x9a\x87",
|
||||
"light_rail" = "\xf0\x9f\x9a\x88",
|
||||
"tram" = "\xf0\x9f\x9a\x8a",
|
||||
"bike" = "\xf0\x9f\x9a\xb2",
|
||||
"motorcycle" = "\xf0\x9f\x8f\x8d",
|
||||
"airplane" = "\xe2\x9c\x88\xef\xb8\x8f",
|
||||
"helicopter" = "\xf0\x9f\x9a\x81",
|
||||
"boat" = "\xe2\x9b\xb5",
|
||||
"sailboat" = "\xe2\x9b\xb5",
|
||||
"ship" = "\xf0\x9f\x9a\xa2",
|
||||
"fuelpump" = "\xe2\x9b\xbd",
|
||||
"construction" = "\xf0\x9f\x9a\xa7",
|
||||
"house" = "\xf0\x9f\x8f\xa0",
|
||||
"house_with_garden" = "\xf0\x9f\x8f\xa1",
|
||||
"office" = "\xf0\x9f\x8f\xa2",
|
||||
"post_office" = "\xf0\x9f\x8f\xa3",
|
||||
"hospital" = "\xf0\x9f\x8f\xa5",
|
||||
"bank" = "\xf0\x9f\x8f\xa6",
|
||||
"hotel" = "\xf0\x9f\x8f\xa8",
|
||||
"school" = "\xf0\x9f\x8f\xab",
|
||||
"department_store" = "\xf0\x9f\x8f\xac",
|
||||
"church" = "\xe2\x9b\xaa",
|
||||
"castle" = "\xf0\x9f\x8f\xb0",
|
||||
"factory" = "\xf0\x9f\x8f\xad",
|
||||
"tokyo_tower" = "\xf0\x9f\x97\xbc",
|
||||
"statue_of_liberty" = "\xf0\x9f\x97\xbd",
|
||||
"fountain" = "\xe2\x9b\xb2",
|
||||
"tent" = "\xe2\x9b\xba",
|
||||
"mountain" = "\xf0\x9f\x8f\x94",
|
||||
"snow_capped_mountain" = "\xf0\x9f\x8f\x94",
|
||||
"beach" = "\xf0\x9f\x8f\x96",
|
||||
"camping" = "\xf0\x9f\x8f\x95",
|
||||
"world_map" = "\xf0\x9f\x97\xba",
|
||||
"japan" = "\xf0\x9f\x97\xbe",
|
||||
};
|
||||
|
||||
expand_emoji :: proc(text: string) -> string {
|
||||
if len(EMOJIS) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
sb := strings.builder_make()
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
remaining := text
|
||||
|
||||
for {
|
||||
colon := strings.index(remaining, ":")
|
||||
if colon < 0 {
|
||||
strings.write_string(&sb, remaining)
|
||||
break
|
||||
}
|
||||
|
||||
after := remaining[colon + 1:]
|
||||
end := strings.index(after, ":")
|
||||
if end < 0 {
|
||||
strings.write_string(&sb, remaining)
|
||||
break
|
||||
}
|
||||
|
||||
shortcode := remaining[colon + 1 : colon + 1 + end]
|
||||
|
||||
valid := true
|
||||
for c in shortcode {
|
||||
if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '_' && c != '+' && c != '-' {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid || len(shortcode) == 0 {
|
||||
strings.write_string(&sb, remaining[:colon + 1])
|
||||
remaining = remaining[colon + 1:]
|
||||
continue
|
||||
}
|
||||
|
||||
emoji, found := EMOJIS[shortcode]
|
||||
if !found {
|
||||
strings.write_string(&sb, remaining[:colon + 1])
|
||||
remaining = remaining[colon + 1:]
|
||||
continue
|
||||
}
|
||||
|
||||
strings.write_string(&sb, remaining[:colon])
|
||||
strings.write_string(&sb, emoji)
|
||||
remaining = remaining[colon + 1 + end + 1:]
|
||||
}
|
||||
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package markdown
|
||||
|
||||
import cm "vendor:commonmark"
|
||||
|
||||
import "core:fmt"
|
||||
import "core:strings"
|
||||
|
||||
Note_Kind :: enum {
|
||||
Sidenote,
|
||||
Marginnote,
|
||||
}
|
||||
|
||||
// strip_definitions scans markdown text for note definitions ([^id]: text for
|
||||
// sidenotes, [*id]: text for marginnotes), removes them, and returns the cleaned
|
||||
// text plus separate maps of id->definition for each kind.
|
||||
// Handles multi-line definitions with indented continuation lines.
|
||||
strip_definitions :: proc(
|
||||
body: string,
|
||||
) -> (
|
||||
clean_body: string,
|
||||
sn_defs, mn_defs: map[string]string,
|
||||
) {
|
||||
lines := strings.split(body, "\n")
|
||||
defer delete(lines)
|
||||
|
||||
out_sb := strings.builder_make()
|
||||
defer strings.builder_destroy(&out_sb)
|
||||
|
||||
i := 0
|
||||
for i < len(lines) {
|
||||
line := lines[i]
|
||||
|
||||
id, def_text, kind, is_def := parse_def_line(line)
|
||||
if !is_def {
|
||||
if strings.builder_len(out_sb) > 0 {
|
||||
strings.write_string(&out_sb, "\n")
|
||||
}
|
||||
strings.write_string(&out_sb, line)
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
def_sb := strings.builder_make()
|
||||
if def_text != "" {
|
||||
strings.write_string(&def_sb, def_text)
|
||||
}
|
||||
|
||||
i += 1
|
||||
for i < len(lines) {
|
||||
next := lines[i]
|
||||
if len(next) == 0 {
|
||||
break
|
||||
}
|
||||
_, _, _, is_new_def := parse_def_line(next)
|
||||
if is_new_def {
|
||||
break
|
||||
}
|
||||
if strings.builder_len(def_sb) > 0 {
|
||||
strings.write_string(&def_sb, "\n")
|
||||
}
|
||||
if is_indented(next) {
|
||||
strings.write_string(&def_sb, strings.trim_left(next, " \t"))
|
||||
} else {
|
||||
strings.write_string(&def_sb, next)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
joined := strings.clone(strings.to_string(def_sb))
|
||||
strings.builder_destroy(&def_sb)
|
||||
|
||||
if kind == .Marginnote {
|
||||
mn_defs[id] = joined
|
||||
} else {
|
||||
sn_defs[id] = joined
|
||||
}
|
||||
}
|
||||
|
||||
clean_body = strings.clone(strings.to_string(out_sb))
|
||||
return
|
||||
}
|
||||
|
||||
// parse_def_line checks if a line is a note definition: [^id]: text (sidenote)
|
||||
// or [*id]: text (marginnote).
|
||||
parse_def_line :: proc(line: string) -> (id: string, text: string, kind: Note_Kind, ok: bool) {
|
||||
if len(line) < 5 || line[0] != '[' {
|
||||
return
|
||||
}
|
||||
|
||||
if line[1] == '^' {
|
||||
kind = .Sidenote
|
||||
} else if line[1] == '*' {
|
||||
kind = .Marginnote
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
close := strings.index(line[2:], "]")
|
||||
if close < 0 {
|
||||
return
|
||||
}
|
||||
close += 2
|
||||
|
||||
if close + 1 >= len(line) || line[close + 1] != ':' {
|
||||
return
|
||||
}
|
||||
|
||||
id = line[2:close]
|
||||
text = strings.trim_left(line[close + 2:], " \t")
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
is_indented :: proc(line: string) -> bool {
|
||||
if len(line) == 0 {
|
||||
return false
|
||||
}
|
||||
return line[0] == ' ' || line[0] == '\t'
|
||||
}
|
||||
|
||||
// inject_notes finds [^id] (sidenote) and [*id] (marginnote) references in
|
||||
// rendered HTML and replaces them with the appropriate margin markup. Each
|
||||
// definition is rendered through cmark separately.
|
||||
inject_notes :: proc(html: string, sn_defs, mn_defs: map[string]string) -> string {
|
||||
if len(sn_defs) == 0 && len(mn_defs) == 0 {
|
||||
return html
|
||||
}
|
||||
|
||||
parts: strings.Builder
|
||||
strings.builder_init_len(&parts, 0) // TODO: Set a reasonable default
|
||||
defer strings.builder_destroy(&parts)
|
||||
|
||||
remaining := html
|
||||
|
||||
for {
|
||||
sn_pos := strings.index(remaining, "[^")
|
||||
mn_pos := strings.index(remaining, "[*")
|
||||
|
||||
is_margin := mn_pos >= 0 && (sn_pos < 0 || mn_pos < sn_pos)
|
||||
pos := sn_pos
|
||||
if is_margin {
|
||||
pos = mn_pos
|
||||
}
|
||||
if pos < 0 {
|
||||
strings.write_string(&parts, remaining)
|
||||
break
|
||||
}
|
||||
|
||||
// Append text before the reference
|
||||
strings.write_string(&parts, remaining[:pos])
|
||||
|
||||
close := strings.index(remaining[pos + 2:], "]")
|
||||
if close < 0 {
|
||||
strings.write_string(&parts, remaining[pos:])
|
||||
break
|
||||
}
|
||||
|
||||
id := remaining[pos + 2:pos + 2 + close]
|
||||
ref_end := pos + 2 + close + 1
|
||||
|
||||
defs := sn_defs
|
||||
if is_margin {
|
||||
defs = mn_defs
|
||||
}
|
||||
def_text, found := defs[id]
|
||||
if !found {
|
||||
// No definition found, leave as literal text
|
||||
strings.write_string(&parts, remaining[pos:ref_end])
|
||||
remaining = remaining[ref_end:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Render definition through cmark for markdown support
|
||||
def_html := cm.markdown_to_html_from_string(def_text, {.Unsafe})
|
||||
def_html = strip_p_tags(def_html)
|
||||
|
||||
note: string
|
||||
defer delete(note)
|
||||
if is_margin {
|
||||
note = fmt.aprintf(
|
||||
`<label for="mn-%s" class="margin-toggle"></label><input type="checkbox" id="mn-%s" class="margin-toggle"><span class="marginnote">%s</span>`,
|
||||
id,
|
||||
id,
|
||||
def_html,
|
||||
)
|
||||
} else {
|
||||
note = fmt.aprintf(
|
||||
`<label for="fn-%s" class="margin-toggle sidenote-number"></label><input type="checkbox" id="fn-%s" class="margin-toggle"><span class="sidenote">%s</span>`,
|
||||
id,
|
||||
id,
|
||||
def_html,
|
||||
)
|
||||
}
|
||||
strings.write_string(&parts, note)
|
||||
|
||||
remaining = remaining[ref_end:]
|
||||
}
|
||||
|
||||
return strings.to_string(parts)
|
||||
}
|
||||
|
||||
// strip_p_tags removes surrounding <p></p> if the HTML is a single paragraph.
|
||||
strip_p_tags :: proc(html: string) -> string {
|
||||
s := html
|
||||
if len(s) > 0 && s[len(s) - 1] == '\n' {
|
||||
s = s[:len(s) - 1]
|
||||
}
|
||||
if strings.has_prefix(s, "<p>") && strings.has_suffix(s, "</p>") {
|
||||
return s[3:len(s) - 4]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#+feature dynamic-literals
|
||||
#+test
|
||||
package markdown
|
||||
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_parse_def_line :: proc(t: ^testing.T) {
|
||||
// sidenote definition
|
||||
id, text, kind, ok := parse_def_line("[^foo]: bar baz")
|
||||
testing.expect(t, ok)
|
||||
testing.expect(t, id == "foo")
|
||||
testing.expect(t, text == "bar baz")
|
||||
testing.expect(t, kind == .Sidenote)
|
||||
|
||||
// marginnote definition
|
||||
id, text, kind, ok = parse_def_line("[*baz]: qux")
|
||||
testing.expect(t, ok)
|
||||
testing.expect(t, id == "baz")
|
||||
testing.expect(t, text == "qux")
|
||||
testing.expect(t, kind == .Marginnote)
|
||||
|
||||
// plain text is not a definition
|
||||
_, _, _, ok = parse_def_line("regular text")
|
||||
testing.expect(t, !ok)
|
||||
|
||||
// a bracket that is not a note (e.g. reference-link style) is ignored
|
||||
_, _, _, ok = parse_def_line("[link]: url")
|
||||
testing.expect(t, !ok)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_strip_definitions :: proc(t: ^testing.T) {
|
||||
body := "Intro[^a] and [*b].\n\n[^a]: a side\n\n[*b]: a margin\n"
|
||||
clean, sn_defs, mn_defs := strip_definitions(body)
|
||||
defer {
|
||||
delete(clean)
|
||||
delete(sn_defs["a"])
|
||||
delete(sn_defs)
|
||||
delete(mn_defs["b"])
|
||||
delete(mn_defs)
|
||||
}
|
||||
|
||||
// references stay; definitions are removed
|
||||
testing.expect(t, strings.contains(clean, "Intro[^a]"))
|
||||
testing.expect(t, strings.contains(clean, "[*b]"))
|
||||
testing.expect(t, !strings.contains(clean, "[^a]:"))
|
||||
testing.expect(t, !strings.contains(clean, "[*b]:"))
|
||||
|
||||
// routed to the correct map by sigil
|
||||
sa, sa_ok := sn_defs["a"]
|
||||
testing.expect(t, sa_ok)
|
||||
testing.expect(t, sa == "a side")
|
||||
|
||||
mb, mb_ok := mn_defs["b"]
|
||||
testing.expect(t, mb_ok)
|
||||
testing.expect(t, mb == "a margin")
|
||||
|
||||
// ids do not leak across maps
|
||||
_, leaked := sn_defs["b"]
|
||||
testing.expect(t, !leaked)
|
||||
_, leaked2 := mn_defs["a"]
|
||||
testing.expect(t, !leaked2)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_inject_notes :: proc(t: ^testing.T) {
|
||||
html := "Text[^a] more [*b] end."
|
||||
sn_defs := map[string]string {
|
||||
"a" = "side note",
|
||||
}
|
||||
defer delete(sn_defs)
|
||||
mn_defs := map[string]string {
|
||||
"b" = "margin note",
|
||||
}
|
||||
defer delete(mn_defs)
|
||||
|
||||
out := inject_notes(html, sn_defs, mn_defs)
|
||||
|
||||
// sidenote: numbered, fn- prefix, .sidenote span, rendered text
|
||||
testing.expect(
|
||||
t,
|
||||
strings.contains(out, `for="fn-a" class="margin-toggle sidenote-number"></label>`),
|
||||
)
|
||||
testing.expect(t, strings.contains(out, `class="sidenote"`))
|
||||
testing.expect(t, strings.contains(out, "side note"))
|
||||
|
||||
// marginnote: unnumbered, mn- prefix, .marginnote span, rendered text
|
||||
testing.expect(t, strings.contains(out, `for="mn-b" class="margin-toggle"></label>`))
|
||||
testing.expect(t, strings.contains(out, `class="marginnote"`))
|
||||
testing.expect(t, strings.contains(out, "margin note"))
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_inject_notes_no_defs :: proc(t: ^testing.T) {
|
||||
html := "No notes here."
|
||||
sn := make(map[string]string)
|
||||
mn := make(map[string]string)
|
||||
testing.expect(t, inject_notes(html, sn, mn) == html)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_inject_notes_missing_ref :: proc(t: ^testing.T) {
|
||||
html := "Ref[^missing] and [*missing] end."
|
||||
sn := map[string]string {
|
||||
"other" = "x",
|
||||
}
|
||||
defer delete_map(sn)
|
||||
mn := map[string]string {
|
||||
"other2" = "y",
|
||||
}
|
||||
defer delete_map(mn)
|
||||
|
||||
out := inject_notes(html, sn, mn)
|
||||
|
||||
// references with no matching definition are left as literal text
|
||||
testing.expect(t, len(out) > 2)
|
||||
testing.expect(t, strings.contains(out, "[^missing]"))
|
||||
testing.expect(t, strings.contains(out, "[*missing]"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package markdown
|
||||
|
||||
import ts "../treesitter"
|
||||
|
||||
import "core:fmt"
|
||||
import "core:log"
|
||||
import "core:strings"
|
||||
|
||||
Capture :: struct {
|
||||
start: u32,
|
||||
end: u32,
|
||||
name: string,
|
||||
}
|
||||
|
||||
find_first_error_line :: proc(root: ts.Node) -> int {
|
||||
if ts.node_is_error(root) {
|
||||
return int(ts.node_start_point(root).row) + 1
|
||||
}
|
||||
for i in 0..<ts.node_child_count(root) {
|
||||
child := ts.node_child(root, u32(i))
|
||||
if ts.node_has_error(child) {
|
||||
line := find_first_error_line(child)
|
||||
if line > 0 {
|
||||
return line
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
capture_name_to_css :: proc(name: string) -> string {
|
||||
sb := strings.builder_make()
|
||||
seg := strings.builder_make()
|
||||
first := true
|
||||
for i in 0..<len(name) {
|
||||
if name[i] == '.' {
|
||||
if !first do strings.write_byte(&sb, ' ')
|
||||
first = false
|
||||
strings.write_string(&sb, "hl-")
|
||||
strings.write_string(&sb, strings.to_string(seg))
|
||||
strings.write_byte(&seg, '-')
|
||||
} else {
|
||||
strings.write_byte(&seg, name[i])
|
||||
}
|
||||
}
|
||||
if !first do strings.write_byte(&sb, ' ')
|
||||
strings.write_string(&sb, "hl-")
|
||||
strings.write_string(&sb, strings.to_string(seg))
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
escape_html :: proc(s: string) -> string {
|
||||
sb := strings.builder_make()
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
start := 0
|
||||
for i in 0..<len(s) {
|
||||
switch s[i] {
|
||||
case '&':
|
||||
if i > start do strings.write_string(&sb, s[start:i])
|
||||
strings.write_string(&sb, "&")
|
||||
start = i + 1
|
||||
case '<':
|
||||
if i > start do strings.write_string(&sb, s[start:i])
|
||||
strings.write_string(&sb, "<")
|
||||
start = i + 1
|
||||
case '>':
|
||||
if i > start do strings.write_string(&sb, s[start:i])
|
||||
strings.write_string(&sb, ">")
|
||||
start = i + 1
|
||||
case '"':
|
||||
if i > start do strings.write_string(&sb, s[start:i])
|
||||
strings.write_string(&sb, """)
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start == 0 do return s
|
||||
if start < len(s) do strings.write_string(&sb, s[start:])
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
unescape_html :: proc(s: string) -> string {
|
||||
sb := strings.builder_make()
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
start := 0
|
||||
for i in 0..<len(s) {
|
||||
if s[i] != '&' do continue
|
||||
semi := strings.index(s[i:], ";")
|
||||
if semi < 0 do break
|
||||
entity := s[i : i + semi + 1]
|
||||
replacement := ""
|
||||
switch entity {
|
||||
case "&": replacement = "&"
|
||||
case "<": replacement = "<"
|
||||
case ">": replacement = ">"
|
||||
case """: replacement = "\""
|
||||
case "'", "'": replacement = "'"
|
||||
case: continue
|
||||
}
|
||||
if i > start do strings.write_string(&sb, s[start:i])
|
||||
strings.write_string(&sb, replacement)
|
||||
start = i + semi + 1
|
||||
}
|
||||
if start == 0 do return s
|
||||
if start < len(s) do strings.write_string(&sb, s[start:])
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
highlight_block :: proc(code: string, lang: string, file_path: string) -> string {
|
||||
gc := ts.load_grammar(lang)
|
||||
if gc == nil {
|
||||
return code
|
||||
}
|
||||
|
||||
raw_code := unescape_html(code)
|
||||
raw_c := strings.clone_to_cstring(raw_code)
|
||||
defer delete(raw_c)
|
||||
|
||||
tree := ts.parser_parse_string(gc.parser, nil, raw_c, u32(len(raw_code)))
|
||||
if tree == nil {
|
||||
return code
|
||||
}
|
||||
defer ts.tree_delete(tree)
|
||||
|
||||
root := ts.tree_root_node(tree)
|
||||
|
||||
if ts.node_has_error(root) {
|
||||
line := find_first_error_line(root)
|
||||
if line > 0 {
|
||||
log.warnf("highlight: syntax errors in %s code block at line %d (%s)", lang, line, file_path)
|
||||
} else {
|
||||
log.warnf("highlight: syntax errors in %s code block (%s)", lang, file_path)
|
||||
}
|
||||
}
|
||||
|
||||
cursor := ts.query_cursor_new()
|
||||
if cursor == nil {
|
||||
return code
|
||||
}
|
||||
defer ts.query_cursor_delete(cursor)
|
||||
|
||||
ts.query_cursor_exec(cursor, gc.query, root)
|
||||
|
||||
captures: [dynamic]Capture
|
||||
defer delete(captures)
|
||||
|
||||
match: ts.Query_Match
|
||||
capture_idx: u32
|
||||
for ts.query_cursor_next_capture(cursor, &match, &capture_idx) {
|
||||
if capture_idx >= u32(match.capture_count) {
|
||||
continue
|
||||
}
|
||||
cap := match.captures[capture_idx]
|
||||
name_len: u32
|
||||
name_c := ts.query_capture_name_for_id(gc.query, cap.index, &name_len)
|
||||
if name_c == nil {
|
||||
continue
|
||||
}
|
||||
name_full := string(name_c)
|
||||
name := name_full
|
||||
if len(name_full) > int(name_len) {
|
||||
name = name_full[:int(name_len)]
|
||||
}
|
||||
append(&captures, Capture{
|
||||
start = ts.node_start_byte(cap.node),
|
||||
end = ts.node_end_byte(cap.node),
|
||||
name = name,
|
||||
})
|
||||
}
|
||||
|
||||
if len(captures) == 0 {
|
||||
return code
|
||||
}
|
||||
|
||||
sb := strings.builder_make()
|
||||
|
||||
last_pos: u32 = 0
|
||||
stack: [dynamic]Capture
|
||||
defer delete(stack)
|
||||
|
||||
for cap in captures {
|
||||
for len(stack) > 0 {
|
||||
top := stack[len(stack) - 1]
|
||||
if top.end <= cap.start {
|
||||
if top.end > last_pos {
|
||||
strings.write_string(&sb, escape_html(raw_code[last_pos:top.end]))
|
||||
}
|
||||
strings.write_string(&sb, "</span>")
|
||||
last_pos = top.end
|
||||
pop(&stack)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cap.start > last_pos {
|
||||
strings.write_string(&sb, escape_html(raw_code[last_pos:cap.start]))
|
||||
last_pos = cap.start
|
||||
}
|
||||
|
||||
css_class := capture_name_to_css(cap.name)
|
||||
strings.write_string(&sb, fmt.tprintf("<span class=\"%s\">", css_class))
|
||||
append(&stack, cap)
|
||||
}
|
||||
|
||||
for len(stack) > 0 {
|
||||
top := pop(&stack)
|
||||
if top.end > last_pos {
|
||||
strings.write_string(&sb, escape_html(raw_code[last_pos:top.end]))
|
||||
}
|
||||
strings.write_string(&sb, "</span>")
|
||||
last_pos = top.end
|
||||
}
|
||||
|
||||
if int(last_pos) < len(raw_code) {
|
||||
strings.write_string(&sb, escape_html(raw_code[last_pos:]))
|
||||
}
|
||||
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
highlight_code :: proc(html: string, file_path: string) -> string {
|
||||
PREFIX :: `<pre><code class="language-`
|
||||
CODE_END :: `</code></pre>`
|
||||
|
||||
sb := strings.builder_make()
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
pos := 0
|
||||
found := false
|
||||
|
||||
for {
|
||||
rel := strings.index(html[pos:], PREFIX)
|
||||
if rel < 0 {
|
||||
break
|
||||
}
|
||||
found = true
|
||||
idx := pos + rel
|
||||
|
||||
if idx > pos {
|
||||
strings.write_string(&sb, html[pos:idx])
|
||||
}
|
||||
|
||||
lang_start := idx + len(PREFIX)
|
||||
lang_end_rel := strings.index(html[lang_start:], `"`)
|
||||
if lang_end_rel < 0 {
|
||||
break
|
||||
}
|
||||
lang_end := lang_start + lang_end_rel
|
||||
lang := html[lang_start:lang_end]
|
||||
|
||||
code_start := lang_end + 1
|
||||
if code_start < len(html) && html[code_start] == '>' {
|
||||
code_start += 1
|
||||
} else {
|
||||
pos = lang_end
|
||||
continue
|
||||
}
|
||||
|
||||
end_rel := strings.index(html[code_start:], CODE_END)
|
||||
if end_rel < 0 {
|
||||
break
|
||||
}
|
||||
end_idx := code_start + end_rel
|
||||
|
||||
code := html[code_start:end_idx]
|
||||
highlighted := highlight_block(code, lang, file_path)
|
||||
strings.write_string(&sb, fmt.tprintf(`<pre><code class="language-%s">%s</code></pre>`, lang, highlighted))
|
||||
|
||||
pos = end_idx + len(CODE_END)
|
||||
}
|
||||
|
||||
if pos < len(html) && found {
|
||||
strings.write_string(&sb, html[pos:])
|
||||
}
|
||||
|
||||
if !found {
|
||||
return html
|
||||
}
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package markdown
|
||||
|
||||
import cm "vendor:commonmark"
|
||||
|
||||
import "core:encoding/json"
|
||||
import "core:strings"
|
||||
|
||||
Extension :: enum {
|
||||
Emoji,
|
||||
Sidenotes,
|
||||
Alerts,
|
||||
Highlight,
|
||||
Sections,
|
||||
}
|
||||
|
||||
DEFAULT_EXTENSIONS :: bit_set[Extension]{.Emoji, .Sidenotes, .Alerts}
|
||||
|
||||
process :: proc(body: string, ext: bit_set[Extension], file_path: string) -> string {
|
||||
side_notes := make(map[string]string)
|
||||
margin_notes := make(map[string]string)
|
||||
clean_body := body
|
||||
if .Sidenotes in ext {
|
||||
clean_body, side_notes, margin_notes = strip_definitions(body)
|
||||
}
|
||||
html := cm.markdown_to_html_from_string(clean_body, {.Unsafe})
|
||||
if .Emoji in ext {
|
||||
html = expand_emoji(html)
|
||||
}
|
||||
if .Sidenotes in ext {
|
||||
html = inject_notes(html, side_notes, margin_notes)
|
||||
}
|
||||
if .Alerts in ext {
|
||||
html = inject_alerts(html)
|
||||
}
|
||||
if .Highlight in ext {
|
||||
html = highlight_code(html, file_path)
|
||||
}
|
||||
if .Sections in ext {
|
||||
html = wrap_sections(html)
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
// Convert a ',' separated list of case-insensitive extension names to a bit set.
|
||||
parse_extension_list :: proc(s: string) -> (result: bit_set[Extension]) {
|
||||
for part in strings.split(s, ",", allocator = context.temp_allocator) {
|
||||
name := strings.to_lower(strings.trim_space(part), allocator = context.temp_allocator)
|
||||
switch name {
|
||||
case "emoji":
|
||||
result += {.Emoji}
|
||||
case "sidenotes":
|
||||
result += {.Sidenotes}
|
||||
case "alerts":
|
||||
result += {.Alerts}
|
||||
case "highlight":
|
||||
result += {.Highlight}
|
||||
case "sections":
|
||||
result += {.Sections}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Given a map[Extension]bool, apply it to ext.
|
||||
apply_extension_config :: proc(ext: ^bit_set[Extension], config: json.Object) {
|
||||
for name, val in config {
|
||||
// TODO: Silently discards invalid values.
|
||||
enabled := val.(json.Boolean) or_continue
|
||||
switch name {
|
||||
case "emoji":
|
||||
if enabled {ext^ += {.Emoji}} else {ext^ -= {.Emoji}}
|
||||
case "sidenotes":
|
||||
if enabled {ext^ += {.Sidenotes}} else {ext^ -= {.Sidenotes}}
|
||||
case "alerts":
|
||||
if enabled {ext^ += {.Alerts}} else {ext^ -= {.Alerts}}
|
||||
case "highlight":
|
||||
if enabled {ext^ += {.Highlight}} else {ext^ -= {.Highlight}}
|
||||
case "sections":
|
||||
if enabled {ext^ += {.Sections}} else {ext^ -= {.Sections}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package markdown
|
||||
|
||||
import "core:strings"
|
||||
|
||||
wrap_sections :: proc(html: string) -> string {
|
||||
H2 :: "<h2"
|
||||
|
||||
sb := strings.builder_make()
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
pos := 0
|
||||
search_pos := 0
|
||||
found := false
|
||||
|
||||
for {
|
||||
rel := strings.index(html[search_pos:], H2)
|
||||
if rel < 0 {
|
||||
break
|
||||
}
|
||||
idx := search_pos + rel
|
||||
found = true
|
||||
|
||||
if idx > pos {
|
||||
strings.write_string(&sb, "<section>")
|
||||
strings.write_string(&sb, html[pos:idx])
|
||||
strings.write_string(&sb, "</section>")
|
||||
}
|
||||
|
||||
pos = idx
|
||||
search_pos = idx + len(H2)
|
||||
}
|
||||
|
||||
if pos < len(html) {
|
||||
strings.write_string(&sb, "<section>")
|
||||
strings.write_string(&sb, html[pos:])
|
||||
strings.write_string(&sb, "</section>")
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
return html
|
||||
}
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
Reference in New Issue
Block a user