initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.dSYM
|
||||||
|
*.sublime-workspace
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# comb
|
||||||
|
|
||||||
|
This is a project to enable literate programming in Markdown files.
|
||||||
42
comb.odin
Normal file
42
comb.odin
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package comb
|
||||||
|
|
||||||
|
import "core:log"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:flags"
|
||||||
|
import "core:os/os2"
|
||||||
|
|
||||||
|
example_1 :: string(#load("example_1.md"))
|
||||||
|
|
||||||
|
CLI_Options :: struct {
|
||||||
|
file_path: string `args:"pos=0,required" usage:"Input file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: proc() {
|
||||||
|
context.logger = log.create_console_logger()
|
||||||
|
opt: CLI_Options
|
||||||
|
flags.parse_or_exit(&opt, os2.args)
|
||||||
|
// TODO: handle error
|
||||||
|
data, _ := os2.read_entire_file(opt.file_path, context.allocator)
|
||||||
|
defer delete(data)
|
||||||
|
content := string(data)
|
||||||
|
// TODO: handle error
|
||||||
|
blocks, _ := parse_code_blocks(content)
|
||||||
|
defer for b in blocks do destroy_code_block(b)
|
||||||
|
if len(blocks) == 0 {
|
||||||
|
log.info("No code blocks detected.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for block, i in blocks {
|
||||||
|
log.infof("Executing block #%v (%v), starting on line %v...", i, block.language, block.line_start)
|
||||||
|
stdout, stderr, err := eval_code_block(block)
|
||||||
|
defer delete(stdout)
|
||||||
|
defer delete(stderr)
|
||||||
|
if err != nil {
|
||||||
|
log.errorf("Failed to execute code block: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.infof("Stdout: %v", stdout)
|
||||||
|
log.infof("Stderr: %v", stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
comb.sublime-project
Normal file
82
comb.sublime-project
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"build_systems": [
|
||||||
|
{
|
||||||
|
"cmd": [
|
||||||
|
"odin",
|
||||||
|
"build",
|
||||||
|
".",
|
||||||
|
"-debug",
|
||||||
|
"-o:none"
|
||||||
|
],
|
||||||
|
"name": "build comb",
|
||||||
|
"selector": "source.odin",
|
||||||
|
"working_dir": "${project_path}",
|
||||||
|
"file_regex": "^\\s*(\\S[^\\(]+)\\((\\d+):(\\d+)\\) (.*)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shell_cmd": "odin build . -debug -o:none && ./comb",
|
||||||
|
"name": "build and run",
|
||||||
|
"selector": "source.odin",
|
||||||
|
"working_dir": "${project_path}",
|
||||||
|
"file_regex": "^\\s*(\\S[^\\(]+)\\((\\d+):(\\d+)\\) (.*)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": [
|
||||||
|
"odin",
|
||||||
|
"run",
|
||||||
|
".",
|
||||||
|
"-debug",
|
||||||
|
"-o:none",
|
||||||
|
],
|
||||||
|
"name": "run comb",
|
||||||
|
"selector": "source.odin",
|
||||||
|
"working_dir": "${project_path}",
|
||||||
|
"file_regex": "^\\s*(\\S[^\\(]+)\\((\\d+):(\\d+)\\) (.*)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": [
|
||||||
|
"odin",
|
||||||
|
"test",
|
||||||
|
".",
|
||||||
|
"-debug",
|
||||||
|
"-o:none",
|
||||||
|
"-define:ODIN_TEST_GO_TO_ERROR=true"
|
||||||
|
],
|
||||||
|
"name": "test comb",
|
||||||
|
"selector": "source.odin",
|
||||||
|
"working_dir": "${project_path}",
|
||||||
|
"file_regex": "^\\s*(\\S[^\\(]+)\\((\\d+):(\\d+)\\):(.*)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".",
|
||||||
|
"folder_exclude_patterns": [
|
||||||
|
"*bin*",
|
||||||
|
"*.dSYM"
|
||||||
|
],
|
||||||
|
"binary_file_patterns": [
|
||||||
|
"*.bin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/opt/homebrew/Cellar/odin/2025-12a/libexec/core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/opt/homebrew/Cellar/odin/2025-12a/libexec/base",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/opt/homebrew/Cellar/odin/2025-12a/libexec/vendor/raylib",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"debugger_configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch",
|
||||||
|
"program": "${project_path}/comb",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${project_path}"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
92
eval_code_blocks.odin
Normal file
92
eval_code_blocks.odin
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#+feature dynamic-literals
|
||||||
|
package comb
|
||||||
|
|
||||||
|
import os "core:os/os2"
|
||||||
|
import "core:strings"
|
||||||
|
import tt "core:testing"
|
||||||
|
|
||||||
|
LangInfo :: struct {
|
||||||
|
command: []string,
|
||||||
|
file_extension: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
lang_info_map := map[string]LangInfo {
|
||||||
|
"python" = {command = {"python3", "-c"}, file_extension = ".py"},
|
||||||
|
"javascript" = {command = {"node", "-e"}, file_extension = ".js"},
|
||||||
|
"bash" = {command = {"bash", "-c"}, file_extension = ".sh"},
|
||||||
|
}
|
||||||
|
|
||||||
|
EvalError :: union #shared_nil {
|
||||||
|
enum {
|
||||||
|
None = 0,
|
||||||
|
Unknown_Language,
|
||||||
|
},
|
||||||
|
os.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
eval_code_block :: proc(
|
||||||
|
cb: Code_Block,
|
||||||
|
allocator := context.allocator,
|
||||||
|
temp_allocator := context.temp_allocator,
|
||||||
|
) -> (
|
||||||
|
stdout: string,
|
||||||
|
stderr: string,
|
||||||
|
err: EvalError,
|
||||||
|
) {
|
||||||
|
info, ok := lang_info_map[cb.language]
|
||||||
|
if !ok {
|
||||||
|
err = .Unknown_Language
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
command := make([dynamic]string, allocator = temp_allocator)
|
||||||
|
reserve(&command, len(info.command) + 1)
|
||||||
|
for c in info.command {
|
||||||
|
append(&command, c)
|
||||||
|
}
|
||||||
|
append(&command, cb.text)
|
||||||
|
|
||||||
|
env, _ := os.environ(temp_allocator)
|
||||||
|
process_desc := os.Process_Desc {
|
||||||
|
command = command[:],
|
||||||
|
env = env,
|
||||||
|
}
|
||||||
|
state, byte_stdout, byte_stderr, os_err := os.process_exec(process_desc, allocator)
|
||||||
|
if os_err != nil {
|
||||||
|
delete(byte_stdout)
|
||||||
|
delete(byte_stderr)
|
||||||
|
err = os_err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stdout = string(byte_stdout)
|
||||||
|
stderr = string(byte_stderr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
can_eval_code_blocks_python :: proc(t: ^tt.T) {
|
||||||
|
cb := Code_Block{
|
||||||
|
language="python",
|
||||||
|
text="print('hello world')"
|
||||||
|
}
|
||||||
|
defer free_all(context.temp_allocator)
|
||||||
|
stdout, stderr := eval_code_block(cb) or_else tt.fail_now(t, "should be able to execute code block")
|
||||||
|
defer delete(stdout)
|
||||||
|
defer delete(stderr)
|
||||||
|
tt.expect_value(t, strings.trim_space(stdout), "hello world")
|
||||||
|
tt.expect_value(t, strings.trim_space(stderr), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
can_eval_code_blocks_javascript :: proc(t: ^tt.T) {
|
||||||
|
cb := Code_Block{
|
||||||
|
language="javascript",
|
||||||
|
text="console.log('hello world')"
|
||||||
|
}
|
||||||
|
defer free_all(context.temp_allocator)
|
||||||
|
stdout, stderr := eval_code_block(cb) or_else tt.fail_now(t, "should be able to execute code block")
|
||||||
|
defer delete(stdout)
|
||||||
|
defer delete(stderr)
|
||||||
|
tt.expect_value(t, strings.trim_space(stdout), "hello world")
|
||||||
|
tt.expect_value(t, strings.trim_space(stderr), "")
|
||||||
|
}
|
||||||
13
example_1.md
Normal file
13
example_1.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# hello
|
||||||
|
|
||||||
|
```python
|
||||||
|
print("Hello world")
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript {Foo=bar}
|
||||||
|
console.log("hello")
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash {Foo=bar, Bazz=quq}
|
||||||
|
echo "hi"
|
||||||
|
```
|
||||||
137
parse_code_blocks.odin
Normal file
137
parse_code_blocks.odin
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package comb
|
||||||
|
|
||||||
|
import "core:strings"
|
||||||
|
import tt "core:testing"
|
||||||
|
|
||||||
|
Code_Block :: struct {
|
||||||
|
line_start: int,
|
||||||
|
line_end: int,
|
||||||
|
char_start: int,
|
||||||
|
char_end: int,
|
||||||
|
language: string,
|
||||||
|
tags: map[string]string,
|
||||||
|
text: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_code_block :: proc(cb: Code_Block) {
|
||||||
|
delete(cb.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseError :: enum {
|
||||||
|
None = 0,
|
||||||
|
Incomplete_Code_Block,
|
||||||
|
Invalid_Tag,
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_code_blocks :: proc(s: string) -> (blocks: [dynamic]Code_Block, err: ParseError) {
|
||||||
|
it := s
|
||||||
|
in_code_block := false
|
||||||
|
current_block := Code_Block{}
|
||||||
|
line_index := 0
|
||||||
|
char_index := 0
|
||||||
|
for line in strings.split_lines_iterator(&it) {
|
||||||
|
defer line_index += 1
|
||||||
|
// Add +1 for \n character
|
||||||
|
defer char_index += len(line) + 1
|
||||||
|
trimmed_line := strings.trim_space(line)
|
||||||
|
strings.starts_with(trimmed_line, "```") or_continue
|
||||||
|
in_code_block = !in_code_block
|
||||||
|
// We just finished parsing a code block
|
||||||
|
if !in_code_block {
|
||||||
|
current_block.line_end = line_index
|
||||||
|
current_block.char_end = char_index - 1
|
||||||
|
current_block.text = s[current_block.char_start:current_block.char_end]
|
||||||
|
append(&blocks, current_block)
|
||||||
|
current_block = {}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// It looks like we're starting a code block, but there's no language specified
|
||||||
|
// That means we shouldn't try to extract it
|
||||||
|
if len(trimmed_line) == 3 {
|
||||||
|
in_code_block = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
current_block.line_start = line_index
|
||||||
|
current_block.char_start = char_index + len(line) + 1
|
||||||
|
remaining := strings.trim_space(trimmed_line[3:])
|
||||||
|
first_space := strings.index_rune(remaining, ' ')
|
||||||
|
// There are no keys after the language name
|
||||||
|
if first_space == -1 {
|
||||||
|
current_block.language = remaining
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
current_block.language = remaining[:first_space]
|
||||||
|
// The +1 is safe because we know there are characters after the space (or else it would have been trimmed)
|
||||||
|
tag_str := remaining[first_space + 1:]
|
||||||
|
opening_curly := strings.index_rune(tag_str, '{')
|
||||||
|
closing_curly := strings.last_index_byte(tag_str, '}')
|
||||||
|
if closing_curly == opening_curly + 1 do continue
|
||||||
|
|
||||||
|
// TODO: allow curlies if they're in strings
|
||||||
|
if opening_curly == -1 || closing_curly == -1 || closing_curly < opening_curly {
|
||||||
|
// TODO: improve error reporting
|
||||||
|
return blocks, .Invalid_Tag
|
||||||
|
}
|
||||||
|
tag_content_str := strings.trim_space(tag_str[opening_curly + 1:closing_curly])
|
||||||
|
if tag_content_str == "" do continue
|
||||||
|
|
||||||
|
tag_content_str_it := tag_content_str
|
||||||
|
for pair in strings.split_iterator(&tag_content_str_it, ",") {
|
||||||
|
pair := strings.trim_space(pair)
|
||||||
|
key_start, key_end, value_start := 0, 0, 0
|
||||||
|
value_end := len(pair)
|
||||||
|
pair_loop: for c, i in pair {
|
||||||
|
if c == '=' {
|
||||||
|
key_end = i
|
||||||
|
value_start = i + 1
|
||||||
|
current_block.tags[string(pair[key_start:key_end])] = string(
|
||||||
|
pair[value_start:value_end],
|
||||||
|
)
|
||||||
|
break pair_loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: improve error reporting
|
||||||
|
if in_code_block do return blocks, .Incomplete_Code_Block
|
||||||
|
|
||||||
|
return blocks, .None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
parse_blocks_correctly :: proc(t: ^tt.T) {
|
||||||
|
blocks, err := parse_code_blocks(example_1)
|
||||||
|
defer delete(blocks)
|
||||||
|
defer for cb in blocks do destroy_code_block(cb)
|
||||||
|
tt.expect(t, err == nil, "parse_blocks_correctly should not error")
|
||||||
|
tt.expect_value(t, len(blocks), 3)
|
||||||
|
{
|
||||||
|
b := blocks[0]
|
||||||
|
tt.expect_value(t, b.language, "python")
|
||||||
|
tt.expect_value(t, b.text, `print("Hello world")`)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
b := blocks[1]
|
||||||
|
tt.expect_value(t, len(b.tags), 1)
|
||||||
|
val :=
|
||||||
|
b.tags["Foo"] or_else tt.fail_now(t, "the second code block should have a `Foo` key")
|
||||||
|
tt.expect_value(t, val, "bar")
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
b := blocks[2]
|
||||||
|
tt.expect_value(t, len(b.tags), 2)
|
||||||
|
val :=
|
||||||
|
b.tags["Foo"] or_else tt.fail_now(t, "the second code block should have a `Foo` key")
|
||||||
|
tt.expect_value(t, val, "bar")
|
||||||
|
val =
|
||||||
|
b.tags["Bazz"] or_else tt.fail_now(t, "the second code block should have a `Bazz` key")
|
||||||
|
tt.expect_value(t, val, "quq")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user