Merge branch 'odin-rewrite'

This commit was merged in pull request #1.
This commit is contained in:
2026-07-13 14:58:41 +02:00
13 changed files with 528 additions and 194 deletions
-8
View File
@@ -1,8 +0,0 @@
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.v]
indent_style = tab
-8
View File
@@ -1,8 +0,0 @@
* text=auto eol=lf
*.bat eol=crlf
*.v linguist-language=V
*.vv linguist-language=V
*.vsh linguist-language=V
v.mod linguist-language=V
.vdocignore linguist-language=ignore
+1 -26
View File
@@ -1,27 +1,2 @@
# Binaries for programs and plugins
main
TypoML TypoML
*.exe src.bin
*.exe~
*.so
*.dylib
*.dll
# Ignore binary output folders
bin/
# Ignore common editor/system specific metadata
.DS_Store
.idea/
.vscode/
*.iml
# ENV
.env
# Web assets and local databases
*.db
*.js
# Ignore installed modules through `v install --local`:
modules/
+3 -3
View File
@@ -1,13 +1,13 @@
all: build all: build
build: build:
v src/ odin build src/ -out:TypoML
run: run:
v run src/ odin run src/
clean: clean:
rm -f TypoML rm -f TypoML
format: format:
v fmt -w src/ odinfmt -w src/
+1 -2
View File
@@ -1,10 +1,9 @@
#!/bin/sh #!/bin/sh
input=$(cat -) input=$(cat -)
input="${input#v run src/}" input="${input#odin run src/}"
style="<style>$(cat styles.css)</style>" style="<style>$(cat styles.css)</style>"
start="<html><head><title>TypoML Preview</title>$style</head><body>" start="<html><head><title>TypoML Preview</title>$style</head><body>"
end="</body></html>" end="</body></html>"
output="$start$input$end" output="$start$input$end"
echo "$output" > /tmp/preview.html echo "$output" > /tmp/preview.html
xdg-open /tmp/preview.html xdg-open /tmp/preview.html
+374
View File
@@ -0,0 +1,374 @@
package main
import "core:strings"
get_ext :: proc(url: string) -> string {
clean := url
if question := strings.index_byte(clean, '?'); question != -1 {
clean = clean[:question]
}
if hash := strings.index_byte(clean, '#'); hash != -1 {
clean = clean[:hash]
}
if dot := strings.last_index_byte(clean, '.'); dot != -1 {
return strings.to_lower(clean[dot:], context.temp_allocator)
}
return ""
}
detect_type :: proc(url: string) -> string {
ext := get_ext(url)
switch ext {
case ".jpg",
".jpeg",
".jpe",
".jfif",
".jif",
".pjpeg",
".pjp",
".png",
".apng",
".gif",
".webp",
".avif",
".heic",
".heif",
".hif",
".bmp",
".dib",
".tif",
".tiff",
".ico",
".cur",
".svg",
".svgz",
".raw",
".cr2",
".cr3",
".nef",
".nrw",
".orf",
".sr2",
".arw",
".srf",
".rw2",
".pef",
".raf",
".dng",
".3fr",
".ari",
".bay",
".cap",
".dcr",
".erf",
".fff",
".iiq",
".k25",
".kdc",
".mdc",
".mos",
".mrw",
".obm",
".ptx",
".pxn",
".r3d",
".rwl",
".rwz",
".srw",
".x3f",
".gpr",
".pgm",
".ppm",
".pbm",
".pnm",
".pam",
".pfm",
".tga",
".icb",
".vda",
".vst",
".pcx",
".xbm",
".xpm",
".xwd",
".sgi",
".rgb",
".rgba",
".bw",
".jp2",
".j2k",
".j2c",
".jpf",
".jpx",
".jpm",
".mj2",
".wbmp",
".emf",
".wmf",
".eps",
".epsi",
".epsf",
".psd",
".psb",
".ai",
".cdr",
".xcf",
".gbr",
".gih",
".pat",
".art",
".jxl",
".jxr",
".wdp",
".hdp",
".exr",
".hdr",
".dpx",
".cin",
".fits",
".fit",
".fts",
".icns",
".flif",
".dds",
".ktx",
".ktx2",
".astc",
".kodak",
".fpx",
".pcd",
".cmx",
".drw",
".cgm",
".plt",
".hpgl",
".iff",
".ilbm",
".lbm",
".mac",
".msp",
".pct",
".pic",
".pict",
".pntg",
".qtif",
".qti",
".ras",
".sun",
".tpic",
".wpg",
".g3",
".g4",
".dcm",
".dicom",
".ora",
".kra",
".clip":
return "image"
case ".mp3",
".mp2",
".mp1",
".mpa",
".mpga",
".ogg",
".oga",
".opus",
".spx",
".wav",
".wave",
".flac",
".aac",
".adts",
".wma",
".m4a",
".m4b",
".m4p",
".m4r",
".aif",
".aiff",
".aifc",
".au",
".snd",
".mid",
".midi",
".rmi",
".kar",
".ra",
".ram",
".mka",
".ape",
".wv",
".ac3",
".ec3",
".eac3",
".dts",
".dtshd",
".amr",
".awb",
".caf",
".gsm",
".mpc",
".shn",
".tta",
".voc",
".vox",
".8svx",
".mod",
".s3m",
".xm",
".it",
".stm",
".med",
".mtm",
".umx",
".669",
".far",
".amf",
".dsm",
".gdm",
".imf",
".okt",
".sfx",
".ult",
".uni",
".psm",
".j2b",
".dbm",
".digi",
".dtm",
".plm",
".nsf",
".nsfe",
".spc",
".gym",
".vgm",
".vgz",
".gbs",
".hes",
".ay",
".sid",
".oma",
".aa3",
".at3",
".at9",
".qcp",
".tak",
".ofr",
".ofs",
".sox",
".weba",
".dss",
".alac",
".vqf",
".vql",
".mxmf",
".ota",
".imy",
".rtttl",
".rtx",
".xmf",
".w64",
".rf64",
".bwf",
".cda",
".aup":
return "audio"
case ".mp4",
".m4v",
".mp4v",
".webm",
".mov",
".qt",
".avi",
".mkv",
".flv",
".f4v",
".f4p",
".f4a",
".f4b",
".wmv",
".mpg",
".mpeg",
".mpe",
".m1v",
".m2v",
".mpv",
".mp2v",
".mpv2",
".ogv",
".ogx",
".3gp",
".3g2",
".3gpp",
".3gpp2",
".asf",
".asx",
".vob",
".ts",
".mts",
".m2ts",
".tsv",
".tsa",
".divx",
".xvid",
".rm",
".rmvb",
".swf",
".dv",
".dif",
".gifv",
".roq",
".nsv",
".svi",
".annodex",
".axv",
".amv",
".bik",
".bk2",
".smk",
".ivf",
".mxf",
".nut",
".ogm",
".fli",
".flc",
".y4m",
".264",
".h264",
".265",
".h265",
".hevc",
".av1",
".vc1",
".vp8",
".vp9",
".wtv",
".yuv",
".str",
".pva",
".trp",
".rec",
".vid",
".rv",
".cpk",
".film":
return "video"
case:
return "iframe"
}
}
parse_type_hint :: proc(type_hint: string) -> string {
switch type_hint {
case "i", "image", "img":
return "image"
case "a", "audio":
return "audio"
case "v", "video":
return "video"
case "f", "iframe", "frame":
return "iframe"
case:
return ""
}
}
-74
View File
@@ -1,74 +0,0 @@
fn get_ext(url string) string {
mut clean := url
if question := clean.index('?') {
clean = clean[..question]
}
if hash := clean.index('#') {
clean = clean[..hash]
}
if dot := clean.last_index('.') {
return clean[dot..].to_lower()
}
return ''
}
fn detect_type(url string) string {
ext := get_ext(url)
return match ext {
'.jpg', '.jpeg', '.jpe', '.jfif', '.jif', '.pjpeg', '.pjp', '.png', '.apng', '.gif',
'.webp', '.avif', '.heic', '.heif', '.hif', '.bmp', '.dib', '.tif', '.tiff', '.ico',
'.cur', '.svg', '.svgz', '.raw', '.cr2', '.cr3', '.nef', '.nrw', '.orf', '.sr2', '.arw',
'.srf', '.rw2', '.pef', '.raf', '.dng', '.3fr', '.ari', '.bay', '.cap', '.dcr', '.erf',
'.fff', '.iiq', '.k25', '.kdc', '.mdc', '.mos', '.mrw', '.obm', '.ptx', '.pxn', '.r3d',
'.rwl', '.rwz', '.srw', '.x3f', '.gpr', '.pgm', '.ppm', '.pbm', '.pnm', '.pam', '.pfm',
'.tga', '.icb', '.vda', '.vst', '.pcx', '.xbm', '.xpm', '.xwd', '.sgi', '.rgb', '.rgba',
'.bw', '.jp2', '.j2k', '.j2c', '.jpf', '.jpx', '.jpm', '.mj2', '.wbmp', '.emf', '.wmf',
'.eps', '.epsi', '.epsf', '.psd', '.psb', '.ai', '.cdr', '.xcf', '.gbr', '.gih', '.pat',
'.art', '.jxl', '.jxr', '.wdp', '.hdp', '.exr', '.hdr', '.dpx', '.cin', '.fits', '.fit',
'.fts', '.icns', '.flif', '.dds', '.ktx', '.ktx2', '.astc', '.kodak', '.fpx', '.pcd',
'.cmx', '.drw', '.cgm', '.plt', '.hpgl', '.iff', '.ilbm', '.lbm', '.mac', '.msp', '.pct',
'.pic', '.pict', '.pntg', '.qtif', '.qti', '.ras', '.sun', '.tpic', '.wpg', '.g3', '.g4',
'.dcm', '.dicom', '.ora', '.kra', '.clip' {
'image'
}
'.mp3', '.mp2', '.mp1', '.mpa', '.mpga', '.ogg', '.oga', '.opus', '.spx', '.wav', '.wave',
'.flac', '.aac', '.adts', '.wma', '.m4a', '.m4b', '.m4p', '.m4r', '.aif', '.aiff', '.aifc',
'.au', '.snd', '.mid', '.midi', '.rmi', '.kar', '.ra', '.ram', '.mka', '.ape', '.wv',
'.ac3', '.ec3', '.eac3', '.dts', '.dtshd', '.amr', '.awb', '.caf', '.gsm', '.mpc', '.shn',
'.tta', '.voc', '.vox', '.8svx', '.mod', '.s3m', '.xm', '.it', '.stm', '.med', '.mtm',
'.umx', '.669', '.far', '.amf', '.dsm', '.gdm', '.imf', '.okt', '.sfx', '.ult', '.uni',
'.psm', '.j2b', '.dbm', '.digi', '.dtm', '.plm', '.nsf', '.nsfe', '.spc', '.gym', '.vgm',
'.vgz', '.gbs', '.hes', '.ay', '.sid', '.oma', '.aa3', '.at3', '.at9', '.qcp', '.tak',
'.ofr', '.ofs', '.sox', '.weba', '.dss', '.alac', '.vqf', '.vql', '.mxmf', '.ota', '.imy',
'.rtttl', '.rtx', '.xmf', '.w64', '.rf64', '.bwf', '.cda', '.aup' {
'audio'
}
'.mp4', '.m4v', '.mp4v', '.webm', '.mov', '.qt', '.avi', '.mkv', '.flv', '.f4v', '.f4p',
'.f4a', '.f4b', '.wmv', '.mpg', '.mpeg', '.mpe', '.m1v', '.m2v', '.mpv', '.mp2v', '.mpv2',
'.ogv', '.ogx', '.3gp', '.3g2', '.3gpp', '.3gpp2', '.asf', '.asx', '.vob', '.ts', '.mts',
'.m2ts', '.tsv', '.tsa', '.divx', '.xvid', '.rm', '.rmvb', '.swf', '.dv', '.dif', '.gifv',
'.roq', '.nsv', '.svi', '.annodex', '.axv', '.amv', '.bik', '.bk2', '.smk', '.ivf', '.mxf',
'.nut', '.ogm', '.fli', '.flc', '.y4m', '.264', '.h264', '.265', '.h265', '.hevc', '.av1',
'.vc1', '.vp8', '.vp9', '.wtv', '.yuv', '.str', '.pva', '.trp', '.rec', '.vid', '.rv',
'.cpk', '.film' {
'video'
}
else {
'iframe'
}
}
}
// will look like this:
// $[https://krzak.org/krzak-icon.png|i]
// $(https://krzak.org/krzak-icon.png|image)
// $[Krzak.org icon](https://krzak.org/krzak-icon.png|i)
fn parse_type_hint(type_hint string) string {
return match type_hint {
'i', 'image', 'img' { 'image' }
'a', 'audio' { 'audio' }
'v', 'video' { 'video' }
'f', 'iframe' { 'iframe' }
else { '' }
}
}
+58
View File
@@ -0,0 +1,58 @@
package main
import "core:fmt"
import "core:os"
import "core:strings"
read_stdin :: proc() -> []string {
data, err := os.read_entire_file_from_file(os.stdin, context.allocator)
content := string(data)
return strings.split_lines(content)
}
print_token :: proc(token: Token, depth: int = 0) {
indent := strings.repeat(" ", depth, context.temp_allocator)
type_str := ""
switch token.type {
case TokenType.Root:
type_str = "Root"
case TokenType.Text:
type_str = "Text"
case TokenType.Italics:
type_str = "Italics"
case TokenType.Bold:
type_str = "Bold"
case TokenType.BreakLine:
type_str = "BreakLine"
case TokenType.Heading:
type_str = "Heading"
case TokenType.Paragraph:
type_str = "Paragraph"
}
value_str := ""
switch v in token.value {
case string:
value_str = v
case int:
value_str = fmt.aprintf("%d", v, allocator = context.temp_allocator)
}
if value_str != "" {
fmt.printf("%s%s(%s)", indent, type_str, value_str)
} else {
fmt.printf("%s%s", indent, type_str)
}
fmt.println()
for child in token.children {
print_token(child, depth + 1)
}
}
main :: proc() {
lines := read_stdin()
parsed := parse(lines)
print_token(parsed)
}
-18
View File
@@ -1,18 +0,0 @@
import os
fn read_stdin() []string {
mut lines := []string{}
for {
line := os.get_line()
if line.len == 0 && os.stdin().eof() {
break
}
lines << line
}
return lines
}
fn main() {
lines := read_stdin()
println(parse(lines))
}
+90
View File
@@ -0,0 +1,90 @@
package main
import "core:fmt"
import "core:strings"
TokenType :: enum {
Text,
Italics,
Bold,
BreakLine,
Heading,
Paragraph,
Root,
}
TokenValue :: union {
string,
int,
}
Token :: struct {
type: TokenType,
value: TokenValue,
children: [dynamic]Token,
parent: ^Token,
}
current_token: ^Token
replace_text :: proc(text: string, start: int, len_: int, repl: string) -> string {
builder := strings.builder_make(context.temp_allocator)
strings.write_string(&builder, text[:start])
strings.write_string(&builder, repl)
strings.write_string(&builder, text[start + len_:])
return strings.to_string(builder)
}
parse_header :: proc(line: string) -> (int, string) {
if len(line) == 0 {
return 0, line
}
lvl := 0
for lvl < len(line) && line[lvl] == '!' {
lvl += 1
}
if lvl == 0 || lvl >= len(line) || line[lvl] != ' ' {
return 0, line
}
return lvl, line[lvl + 1:]
}
parse :: proc(lines: []string, allocator := context.allocator) -> Token {
root := Token{TokenType.Root, nil, nil, nil}
current_token := &root
append(&current_token.children, Token{TokenType.Paragraph, nil, nil, current_token})
current_token = &current_token.children[0]
for untrimmed_line, line_index in lines {
line := strings.trim_space(untrimmed_line)
if len(line) == 0 && line_index != len(lines) - 1 {
if current_token.parent != nil {
current_token = current_token.parent
}
append(&current_token.children, Token{TokenType.Paragraph, nil, nil, current_token})
current_token = &current_token.children[len(current_token.children) - 1]
continue
}
append(&current_token.children, Token{TokenType.Text, line, nil, current_token})
// lvl, text := parse_header(trimmed)
// is_header := lvl > 0
// if is_header {
// fmt.sbprintf(&builder, "<h%d>", lvl)
// strings.write_string(&builder, text)
// fmt.sbprintf(&builder, "</h%d>", lvl)
// } else {
// strings.write_string(&builder, text)
// strings.write_string(&builder, "<br>")
// }
// strings.write_string(&builder, "\n")
}
return root
}
-47
View File
@@ -1,47 +0,0 @@
fn replace(text string, start int, len int, repl string) string {
return text[..start] + repl + text[start + len..]
}
fn parse_header(line string) (int, string) {
if line.len == 0 {
return 0, line
}
mut lvl := 0
for lvl < line.len && line[lvl] == '!'.bytes()[0] {
lvl++
}
if lvl == 0 || lvl >= line.len || line[lvl] != ' '.bytes()[0] {
return 0, line
}
return lvl, line[lvl + 1..]
}
fn parse(lines []string) string {
mut output := '<p>'
for line in lines {
mut trimmed := line.trim_space()
if trimmed.len == 0 {
output += '<br>\n'
continue
}
lvl, mut text := parse_header(trimmed)
is_header := lvl > 0
if is_header {
output += '<h' + lvl.str() + '>'
output += text + '</h' + lvl.str() + '>'
} else {
output += text
output += '<br>'
}
output += '\n'
}
output += '</p>'
return output
}
+1 -1
View File
@@ -1 +1 @@
clear && make format && cat README.tlm | make run | ./preview clear && make format && cat README.tlm | make run # | ./preview
-7
View File
@@ -1,7 +0,0 @@
Module {
name: 'TypoML'
description: ''
version: '0.0.0'
license: 'ISC'
dependencies: []
}