mirror of
https://github.com/sbrow/thor.git
synced 2026-10-25 02:01:56 -04:00
refactor: Moved diagnostic code into its own package.
This commit is contained in:
@@ -32,6 +32,7 @@ thor/
|
|||||||
├── treesitter/ # FFI types + grammar management (standalone package)
|
├── treesitter/ # FFI types + grammar management (standalone package)
|
||||||
├── markdown/ # Content transformation pipeline (imports ../treesitter)
|
├── markdown/ # Content transformation pipeline (imports ../treesitter)
|
||||||
├── mustache/ # Template engine with lambdas + pipe filters + diagnostics
|
├── mustache/ # Template engine with lambdas + pipe filters + diagnostics
|
||||||
|
├── diagnostics/ # Shared diagnostic utilities (suggest_correction)
|
||||||
├── content.odin # Page struct, Pending_File, scan_content_files, collect_languages, load_page
|
├── content.odin # Page struct, Pending_File, scan_content_files, collect_languages, load_page
|
||||||
├── render.odin # Template rendering, Template_Context, sort_pages, RSS, sitemap
|
├── render.odin # Template rendering, Template_Context, sort_pages, RSS, sitemap
|
||||||
├── menus.odin # Menu_Entry, DEFAULT_WEIGHT, build_menus, collect_auto_menus, merge_page_menus, parse_page_menus, parse_config_menus
|
├── menus.odin # Menu_Entry, DEFAULT_WEIGHT, build_menus, collect_auto_menus, merge_page_menus, parse_page_menus, parse_config_menus
|
||||||
@@ -82,6 +83,7 @@ thor/
|
|||||||
| | `deflists.odin` | `convert_deflists` — pre-cmark pass. Scans for definition list patterns (`term\n\n: definition`) and converts to `<dl><dt><dd>` HTML blocks. Terms and definitions rendered through cmark individually for inline markdown. Consecutive pairs grouped into single `<dl>`. |
|
| | `deflists.odin` | `convert_deflists` — pre-cmark pass. Scans for definition list patterns (`term\n\n: definition`) and converts to `<dl><dt><dd>` HTML blocks. Terms and definitions rendered through cmark individually for inline markdown. Consecutive pairs grouped into single `<dl>`. |
|
||||||
| | `toc.odin` | `generate_toc(html, allocator)` — page-level feature (not a pipeline extension). Scans `<h1>`-`<h6>` for IDs (after `inject_heading_ids`), builds nested `<ul>` with `<a href="#id">` links. Called from `load_page` when frontmatter `"toc": true`. Depends on `.HeadingIDs` being enabled. |
|
| | `toc.odin` | `generate_toc(html, allocator)` — page-level feature (not a pipeline extension). Scans `<h1>`-`<h6>` for IDs (after `inject_heading_ids`), builds nested `<ul>` with `<a href="#id">` links. Called from `load_page` when frontmatter `"toc": true`. Depends on `.HeadingIDs` being enabled. |
|
||||||
| `mustache/` | See [Mustache engine](#mustache-engine) below | Template engine |
|
| `mustache/` | See [Mustache engine](#mustache-engine) below | Template engine |
|
||||||
|
| `diagnostics/` | `diagnostics.odin` | Shared diagnostic utilities: `suggest_correction` (Levenshtein "did you mean?"), `format_error` (rust-style multi-line error formatter with `caret_start`/`caret_end` params), `line_col`, `line_text`, `count_lines`, `digit_count`, `display_width`, `should_colorize`, `write_gutter`. Used by `mustache` (tag-aware errors via `format_tag_error` wrapper), `markdown` (extension name suggestions), and `render.odin` (parse/render error display). |
|
||||||
| `bench/` | `bench.odin` + `templates/` | Standalone template rendering benchmark. Generates 500 posts + 100 comments, renders with indented partials + inheritance + pipes. `--dump <path>` for output validation, positional arg for iteration count (default 250). |
|
| `bench/` | `bench.odin` + `templates/` | Standalone template rendering benchmark. Generates 500 posts + 100 comments, renders with indented partials + inheritance + pipes. `--dump <path>` for output validation, positional arg for iteration count (default 250). |
|
||||||
|
|
||||||
Icon SVGs live as HTML partials in `layouts/partials/icons/` (home, github, rss, chevron_up, star).
|
Icon SVGs live as HTML partials in `layouts/partials/icons/` (home, github, rss, chevron_up, star).
|
||||||
@@ -419,7 +421,7 @@ Spec-compliant implementation at `mustache/`. See `mustache/SPEC.md` for the imp
|
|||||||
| `tokenizer.odin` | Tokenizer (template string → `[]Token`), standalone whitespace detection |
|
| `tokenizer.odin` | Tokenizer (template string → `[]Token`), standalone whitespace detection |
|
||||||
| `data.odin` | Reflection-based data model: `base_value` (peels union/any/nested-any layers), `lookup_in` (structs + maps, handles `Type_Info_Any` value kind in maps), `resolve_name`, `is_truthy`, `any_to_string`, `write_value`, `list_info`, `extract_list_element`, `collect_map_keys` |
|
| `data.odin` | Reflection-based data model: `base_value` (peels union/any/nested-any layers), `lookup_in` (structs + maps, handles `Type_Info_Any` value kind in maps), `resolve_name`, `is_truthy`, `any_to_string`, `write_value`, `list_info`, `extract_list_element`, `collect_map_keys` |
|
||||||
| `pipes.odin` | Pipes extension: `Pipe_Op` enum (`.Format`, `.Group_By`), `pipe_op_from_string`/`pipe_op_candidates` (reflection-based enum name lookup), `Pipe_Filter` AST (with `op_pos` for diagnostics), `parse_pipeline` (tracks byte offsets via `strings.index`), `apply_pipeline`, `apply_filter` (exhaustive enum switch), `apply_group_by`, `apply_format`. Misspelled pipe op suggestions via `suggest_correction`. Stored on `Node.filters`; render-scoped results in temp allocator. |
|
| `pipes.odin` | Pipes extension: `Pipe_Op` enum (`.Format`, `.Group_By`), `pipe_op_from_string`/`pipe_op_candidates` (reflection-based enum name lookup), `Pipe_Filter` AST (with `op_pos` for diagnostics), `parse_pipeline` (tracks byte offsets via `strings.index`), `apply_pipeline`, `apply_filter` (exhaustive enum switch), `apply_group_by`, `apply_format`. Misspelled pipe op suggestions via `suggest_correction`. Stored on `Node.filters`; render-scoped results in temp allocator. |
|
||||||
| `diagnostic.odin` | Rust-style error formatter: `format_error` (multi-line context, ANSI colors via `core:terminal/ansi`, `colorize` param), `format_render_error` (formats `Error`), `line_col`, `line_text`, `context_extent`, `count_lines`, `digit_count`, `should_colorize`. |
|
| `diagnostic.odin` | Mustache-specific diagnostics: `context_extent` (scans `{{ }}` tags for caret underline), `format_tag_error` (wraps `diags.format_error` + `context_extent`), `format_render_error` (formats `Error` using `Template` source/path). Generic error formatting lives in `diagnostics/` package. |
|
||||||
| `suggest.odin` | Strict-warning helpers: `validate_key_path` (walks dotted path, crosses maps silently), `suggest_correction` (Levenshtein via `core:strings/levenshtein_distance`), `collect_struct_keys` (via reflection, recurses into `using`), `struct_has_field` (distinguishes missing field from nil value — needed for `Maybe(bool)`), `collect_partial_names`, `collect_block_names`. |
|
| `suggest.odin` | Strict-warning helpers: `validate_key_path` (walks dotted path, crosses maps silently), `suggest_correction` (Levenshtein via `core:strings/levenshtein_distance`), `collect_struct_keys` (via reflection, recurses into `using`), `struct_has_field` (distinguishes missing field from nil value — needed for `Maybe(bool)`), `collect_partial_names`, `collect_block_names`. |
|
||||||
| `spec_test.odin` | JSON spec test runner — loads `spec/specs/*.json`, runs each test case. Uses `log.nil_logger()` to suppress expected warnings. |
|
| `spec_test.odin` | JSON spec test runner — loads `spec/specs/*.json`, runs each test case. Uses `log.nil_logger()` to suppress expected warnings. |
|
||||||
| `pipes_test.odin` | Pipe filter tests (`group_by` + `format`) |
|
| `pipes_test.odin` | Pipe filter tests (`group_by` + `format`) |
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package diagnostics
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:terminal/ansi"
|
||||||
|
import "core:unicode/utf8"
|
||||||
|
|
||||||
|
// suggest_correction returns the closest match from `available` to `missing`
|
||||||
|
// using Levenshtein distance, or "" if no good match exists. The threshold
|
||||||
|
// scales with the length of the missing key.
|
||||||
|
suggest_correction :: proc(available: []string, missing: string) -> string {
|
||||||
|
if len(available) == 0 || len(missing) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
threshold := max(2, len(missing) / 3)
|
||||||
|
|
||||||
|
best: string
|
||||||
|
best_dist := threshold + 1
|
||||||
|
for candidate in available {
|
||||||
|
if abs(len(candidate) - len(missing)) > threshold {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d := strings.levenshtein_distance(missing, candidate)
|
||||||
|
if d <= threshold && d < best_dist {
|
||||||
|
best = candidate
|
||||||
|
best_dist = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
// line_col returns the 1-indexed line and column for a byte offset in source.
|
||||||
|
line_col :: proc(source: string, pos_in: int) -> (line: int, col: int) {
|
||||||
|
pos := pos_in
|
||||||
|
if pos < 0 {
|
||||||
|
return 1, 1
|
||||||
|
}
|
||||||
|
if pos > len(source) {
|
||||||
|
pos = len(source)
|
||||||
|
}
|
||||||
|
line = 1
|
||||||
|
col = 1
|
||||||
|
for i := 0; i < pos; i += 1 {
|
||||||
|
if source[i] == '\n' {
|
||||||
|
line += 1
|
||||||
|
col = 1
|
||||||
|
} else {
|
||||||
|
col += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// line_text returns the Nth (1-indexed) line of source, without the trailing
|
||||||
|
// newline. Returns "" if line is out of range.
|
||||||
|
line_text :: proc(source: string, line: int) -> string {
|
||||||
|
if line < 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
current := 1
|
||||||
|
start := 0
|
||||||
|
for i := 0; i < len(source); i += 1 {
|
||||||
|
if current == line {
|
||||||
|
end := i
|
||||||
|
for end < len(source) && source[end] != '\n' {
|
||||||
|
end += 1
|
||||||
|
}
|
||||||
|
return source[start:end]
|
||||||
|
}
|
||||||
|
if source[i] == '\n' {
|
||||||
|
current += 1
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current == line {
|
||||||
|
return source[start:]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// should_colorize returns true if stderr is a TTY and color output is wanted.
|
||||||
|
should_colorize :: proc() -> bool {
|
||||||
|
return os.is_tty(os.stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// count_lines returns the number of '\n'-separated lines in source.
|
||||||
|
count_lines :: proc(source: string) -> int {
|
||||||
|
if len(source) == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
n := 1
|
||||||
|
for c in source {
|
||||||
|
if c == '\n' {
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(source) > 0 && source[len(source) - 1] == '\n' {
|
||||||
|
n -= 1
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// digit_count returns the number of decimal digits in n (min 1).
|
||||||
|
digit_count :: proc(n: int) -> int {
|
||||||
|
if n <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
c := 0
|
||||||
|
x := n
|
||||||
|
for x > 0 {
|
||||||
|
c += 1
|
||||||
|
x /= 10
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// display_width returns the number of terminal cells `s` occupies.
|
||||||
|
display_width :: proc(s: string) -> int {
|
||||||
|
return utf8.rune_count_in_string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// line_start_at returns the byte offset of the start of the line containing pos.
|
||||||
|
line_start_at :: proc(source: string, pos_in: int) -> int {
|
||||||
|
pos := pos_in
|
||||||
|
if pos < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if pos >= len(source) {
|
||||||
|
pos = len(source) - 1
|
||||||
|
}
|
||||||
|
for pos > 0 && source[pos - 1] != '\n' {
|
||||||
|
pos -= 1
|
||||||
|
}
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// format_error produces a rust-style multi-line diagnostic string.
|
||||||
|
//
|
||||||
|
// <msg>
|
||||||
|
// --> <path>:<line>:<col>
|
||||||
|
// |
|
||||||
|
// N | <source line N-2>
|
||||||
|
// N | <source line N-1>
|
||||||
|
// N | <source line N — the error line>
|
||||||
|
// | ^^^^^^^^^^^ <hint>
|
||||||
|
// N | <source line N+1>
|
||||||
|
// N | <source line N+2>
|
||||||
|
// |
|
||||||
|
//
|
||||||
|
// `caret_start`/`caret_end` control the byte range of the caret underline.
|
||||||
|
// If not provided (or negative), defaults to a single-rune underline at `pos`.
|
||||||
|
format_error :: proc(
|
||||||
|
path: string,
|
||||||
|
source: string,
|
||||||
|
pos: int,
|
||||||
|
msg: string,
|
||||||
|
hint: string = "",
|
||||||
|
context_before: int = 2,
|
||||||
|
context_after: int = 2,
|
||||||
|
colorize: bool = false,
|
||||||
|
caret_start: int = -1,
|
||||||
|
caret_end: int = -1,
|
||||||
|
) -> string {
|
||||||
|
line, col := line_col(source, pos)
|
||||||
|
total_lines := count_lines(source)
|
||||||
|
|
||||||
|
start_line := line - context_before
|
||||||
|
if start_line < 1 {
|
||||||
|
start_line = 1
|
||||||
|
}
|
||||||
|
end_line := line + context_after
|
||||||
|
if end_line > total_lines {
|
||||||
|
end_line = total_lines
|
||||||
|
}
|
||||||
|
|
||||||
|
width := digit_count(end_line)
|
||||||
|
if width < 1 {
|
||||||
|
width = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := caret_start >= 0 ? caret_start : pos
|
||||||
|
ce := caret_end > cs ? caret_end : cs + 1
|
||||||
|
|
||||||
|
ls := line_start_at(source, pos)
|
||||||
|
caret_start_col := cs - ls + 1
|
||||||
|
caret_end_col := ce - ls + 1
|
||||||
|
|
||||||
|
sb := strings.builder_make(context.temp_allocator)
|
||||||
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
|
red, faint, reset := "", "", ""
|
||||||
|
if colorize {
|
||||||
|
red = ansi.CSI + ansi.FG_RED + ansi.SGR
|
||||||
|
faint = ansi.CSI + ansi.FAINT + ansi.SGR
|
||||||
|
reset = ansi.CSI + ansi.RESET + ansi.SGR
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
strings.write_string(&sb, msg)
|
||||||
|
strings.write_byte(&sb, '\n')
|
||||||
|
|
||||||
|
// Location line
|
||||||
|
strings.write_string(&sb, faint)
|
||||||
|
for _ in 0 ..< width {
|
||||||
|
strings.write_byte(&sb, ' ')
|
||||||
|
}
|
||||||
|
strings.write_string(&sb, "--> ")
|
||||||
|
strings.write_string(&sb, reset)
|
||||||
|
fmt.sbprintf(&sb, "%s:%d:%d\n", path, line, col)
|
||||||
|
|
||||||
|
// Top gutter
|
||||||
|
write_gutter(&sb, width, faint, reset)
|
||||||
|
|
||||||
|
// Context lines
|
||||||
|
for n in start_line ..= end_line {
|
||||||
|
num_str := fmt.tprintf("%d", n)
|
||||||
|
strings.write_string(&sb, faint)
|
||||||
|
for _ in 0 ..< width - len(num_str) {
|
||||||
|
strings.write_byte(&sb, ' ')
|
||||||
|
}
|
||||||
|
strings.write_string(&sb, num_str)
|
||||||
|
strings.write_string(&sb, " | ")
|
||||||
|
strings.write_string(&sb, reset)
|
||||||
|
|
||||||
|
strings.write_string(&sb, line_text(source, n))
|
||||||
|
strings.write_byte(&sb, '\n')
|
||||||
|
|
||||||
|
// Caret row after error line
|
||||||
|
if n == line {
|
||||||
|
strings.write_string(&sb, faint)
|
||||||
|
for _ in 0 ..< width + 1 {
|
||||||
|
strings.write_byte(&sb, ' ')
|
||||||
|
}
|
||||||
|
strings.write_string(&sb, "| ")
|
||||||
|
strings.write_string(&sb, reset)
|
||||||
|
|
||||||
|
for _ in 1 ..< caret_start_col {
|
||||||
|
strings.write_byte(&sb, ' ')
|
||||||
|
}
|
||||||
|
if colorize {
|
||||||
|
strings.write_string(&sb, red)
|
||||||
|
}
|
||||||
|
for _ in 0 ..< caret_end_col - caret_start_col {
|
||||||
|
strings.write_byte(&sb, '^')
|
||||||
|
}
|
||||||
|
if colorize {
|
||||||
|
strings.write_string(&sb, reset)
|
||||||
|
}
|
||||||
|
if hint != "" {
|
||||||
|
strings.write_byte(&sb, ' ')
|
||||||
|
if colorize {
|
||||||
|
strings.write_string(&sb, faint)
|
||||||
|
}
|
||||||
|
strings.write_string(&sb, hint)
|
||||||
|
if colorize {
|
||||||
|
strings.write_string(&sb, reset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strings.write_byte(&sb, '\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing gutter
|
||||||
|
write_gutter(&sb, width, faint, reset)
|
||||||
|
|
||||||
|
return strings.to_string(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write_gutter emits a faint pipe-only gutter line: `<width+1 spaces> |`.
|
||||||
|
write_gutter :: proc(sb: ^strings.Builder, width: int, faint: string, reset: string) {
|
||||||
|
strings.write_string(sb, faint)
|
||||||
|
for _ in 0 ..< width + 1 {
|
||||||
|
strings.write_byte(sb, ' ')
|
||||||
|
}
|
||||||
|
strings.write_string(sb, "|")
|
||||||
|
strings.write_string(sb, reset)
|
||||||
|
strings.write_byte(sb, '\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#+test
|
||||||
|
package diagnostics
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:testing"
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_suggest_correction_exact :: proc(t: ^testing.T) {
|
||||||
|
available := []string{"title", "page.title", "body"}
|
||||||
|
testing.expect_value(t, suggest_correction(available, "page_titel"), "page.title")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_suggest_correction_close :: proc(t: ^testing.T) {
|
||||||
|
available := []string{"title", "body", "now"}
|
||||||
|
testing.expect_value(t, suggest_correction(available, "titel"), "title")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_suggest_correction_no_match :: proc(t: ^testing.T) {
|
||||||
|
available := []string{"completely_different", "unrelated"}
|
||||||
|
testing.expect_value(t, suggest_correction(available, "page_titel"), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_suggest_correction_empty :: proc(t: ^testing.T) {
|
||||||
|
testing.expect_value(t, suggest_correction([]string{}, "anything"), "")
|
||||||
|
testing.expect_value(t, suggest_correction([]string{"a"}, ""), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// line_col / line_text / count_lines / digit_count — primitive helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_line_col_basic :: proc(t: ^testing.T) {
|
||||||
|
src := "abc\ndef\nghi"
|
||||||
|
cases := [?]struct {
|
||||||
|
pos: int,
|
||||||
|
line: int,
|
||||||
|
col: int,
|
||||||
|
}{{0, 1, 1}, {2, 1, 3}, {3, 1, 4}, {4, 2, 1}, {6, 2, 3}, {7, 2, 4}, {8, 3, 1}}
|
||||||
|
for c in cases {
|
||||||
|
l, col := line_col(src, c.pos)
|
||||||
|
testing.expect(t, l == c.line, fmt.tprintf("pos %d: line %d, want %d", c.pos, l, c.line))
|
||||||
|
testing.expect(t, col == c.col, fmt.tprintf("pos %d: col %d, want %d", c.pos, col, c.col))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_line_col_empty :: proc(t: ^testing.T) {
|
||||||
|
l, col := line_col("", 0)
|
||||||
|
testing.expect_value(t, l, 1)
|
||||||
|
testing.expect_value(t, col, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_line_col_negative :: proc(t: ^testing.T) {
|
||||||
|
l, col := line_col("abc", -1)
|
||||||
|
testing.expect_value(t, l, 1)
|
||||||
|
testing.expect_value(t, col, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_line_col_past_end :: proc(t: ^testing.T) {
|
||||||
|
l, col := line_col("abc", 100)
|
||||||
|
testing.expect_value(t, l, 1)
|
||||||
|
testing.expect_value(t, col, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_line_text_first :: proc(t: ^testing.T) {
|
||||||
|
src := "first\nsecond\nthird"
|
||||||
|
testing.expect_value(t, line_text(src, 1), "first")
|
||||||
|
testing.expect_value(t, line_text(src, 2), "second")
|
||||||
|
testing.expect_value(t, line_text(src, 3), "third")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_line_text_trailing_newline :: proc(t: ^testing.T) {
|
||||||
|
src := "first\nsecond\n"
|
||||||
|
testing.expect_value(t, line_text(src, 1), "first")
|
||||||
|
testing.expect_value(t, line_text(src, 2), "second")
|
||||||
|
testing.expect_value(t, line_text(src, 3), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_line_text_out_of_range :: proc(t: ^testing.T) {
|
||||||
|
testing.expect_value(t, line_text("abc", 5), "")
|
||||||
|
testing.expect_value(t, line_text("abc", 0), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_count_lines :: proc(t: ^testing.T) {
|
||||||
|
testing.expect_value(t, count_lines(""), 1)
|
||||||
|
testing.expect_value(t, count_lines("abc"), 1)
|
||||||
|
testing.expect_value(t, count_lines("a\nb"), 2)
|
||||||
|
testing.expect_value(t, count_lines("a\nb\n"), 2)
|
||||||
|
testing.expect_value(t, count_lines("a\nb\nc"), 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_digit_count :: proc(t: ^testing.T) {
|
||||||
|
testing.expect_value(t, digit_count(0), 1)
|
||||||
|
testing.expect_value(t, digit_count(1), 1)
|
||||||
|
testing.expect_value(t, digit_count(9), 1)
|
||||||
|
testing.expect_value(t, digit_count(10), 2)
|
||||||
|
testing.expect_value(t, digit_count(99), 2)
|
||||||
|
testing.expect_value(t, digit_count(100), 3)
|
||||||
|
testing.expect_value(t, digit_count(-5), 1)
|
||||||
|
}
|
||||||
+37
-242
@@ -1,61 +1,6 @@
|
|||||||
package mustache
|
package mustache
|
||||||
|
|
||||||
import "core:fmt"
|
import diags "../diagnostics"
|
||||||
import "core:os"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:terminal/ansi"
|
|
||||||
import "core:unicode/utf8"
|
|
||||||
|
|
||||||
// line_col returns the 1-indexed line and column for a byte offset in source.
|
|
||||||
// Newlines ('\n') separate lines; '\r' is treated as part of '\r\n'. Column is
|
|
||||||
// counted in bytes from the start of the line.
|
|
||||||
line_col :: proc(source: string, pos_in: int) -> (line: int, col: int) {
|
|
||||||
pos := pos_in
|
|
||||||
if pos < 0 {
|
|
||||||
return 1, 1
|
|
||||||
}
|
|
||||||
if pos > len(source) {
|
|
||||||
pos = len(source)
|
|
||||||
}
|
|
||||||
line = 1
|
|
||||||
col = 1
|
|
||||||
for i := 0; i < pos; i += 1 {
|
|
||||||
if source[i] == '\n' {
|
|
||||||
line += 1
|
|
||||||
col = 1
|
|
||||||
} else {
|
|
||||||
col += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// line_text returns the Nth (1-indexed) line of source, without the trailing
|
|
||||||
// newline. Returns "" if line is out of range.
|
|
||||||
line_text :: proc(source: string, line: int) -> string {
|
|
||||||
if line < 1 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
current := 1
|
|
||||||
start := 0
|
|
||||||
for i := 0; i < len(source); i += 1 {
|
|
||||||
if current == line {
|
|
||||||
end := i
|
|
||||||
for end < len(source) && source[end] != '\n' {
|
|
||||||
end += 1
|
|
||||||
}
|
|
||||||
return source[start:end]
|
|
||||||
}
|
|
||||||
if source[i] == '\n' {
|
|
||||||
current += 1
|
|
||||||
start = i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if current == line {
|
|
||||||
return source[start:]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// context_extent returns the byte offset of the start of the line containing
|
// context_extent returns the byte offset of the start of the line containing
|
||||||
// pos, plus the byte offsets of the start and end of the mustache tag at pos.
|
// pos, plus the byte offsets of the start and end of the mustache tag at pos.
|
||||||
@@ -115,68 +60,11 @@ context_extent :: proc(
|
|||||||
return line_start, pos, pos + 1
|
return line_start, pos, pos + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// should_colorize returns true if stderr is a TTY and color output is wanted.
|
// format_tag_error is a mustache-aware wrapper around diags.format_error.
|
||||||
should_colorize :: proc() -> bool {
|
// It uses context_extent to compute the caret underline for `{{ }}` tags,
|
||||||
return os.is_tty(os.stderr)
|
// falling back to a single-rune underline when pos is not inside a tag.
|
||||||
}
|
// When `span > 0`, the caret extends from pos to pos + span.
|
||||||
|
format_tag_error :: proc(
|
||||||
// count_lines returns the number of '\n'-separated lines in source.
|
|
||||||
// A trailing newline does not add an extra line.
|
|
||||||
count_lines :: proc(source: string) -> int {
|
|
||||||
if len(source) == 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
n := 1
|
|
||||||
for c in source {
|
|
||||||
if c == '\n' {
|
|
||||||
n += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Drop phantom last line if source ends with '\n'.
|
|
||||||
if len(source) > 0 && source[len(source) - 1] == '\n' {
|
|
||||||
n -= 1
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// digit_count returns the number of decimal digits in n (min 1).
|
|
||||||
digit_count :: proc(n: int) -> int {
|
|
||||||
if n <= 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
c := 0
|
|
||||||
x := n
|
|
||||||
for x > 0 {
|
|
||||||
c += 1
|
|
||||||
x /= 10
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// display_width returns the number of terminal cells `s` occupies.
|
|
||||||
// For ASCII this is byte length; for UTF-8 we count runes (combining
|
|
||||||
// marks and wide CJK chars are still approximate).
|
|
||||||
display_width :: proc(s: string) -> int {
|
|
||||||
return utf8.rune_count_in_string(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// format_error produces a rust-style multi-line diagnostic string.
|
|
||||||
//
|
|
||||||
// <msg>
|
|
||||||
// --> <path>:<line>:<col>
|
|
||||||
// |
|
|
||||||
// N | <source line N-2>
|
|
||||||
// N | <source line N-1>
|
|
||||||
// N | <source line N — the error line>
|
|
||||||
// | ^^^^^^^^^^^ <hint>
|
|
||||||
// N | <source line N+1>
|
|
||||||
// N | <source line N+2>
|
|
||||||
// |
|
|
||||||
//
|
|
||||||
// `context_before`/`context_after` lines of context are shown around the
|
|
||||||
// error line. Line numbers are right-aligned to the width of the largest
|
|
||||||
// line number shown.
|
|
||||||
format_error :: proc(
|
|
||||||
path: string,
|
path: string,
|
||||||
source: string,
|
source: string,
|
||||||
pos: int,
|
pos: int,
|
||||||
@@ -187,135 +75,25 @@ format_error :: proc(
|
|||||||
colorize: bool = false,
|
colorize: bool = false,
|
||||||
span: int = 0,
|
span: int = 0,
|
||||||
) -> string {
|
) -> string {
|
||||||
line, col := line_col(source, pos)
|
caret_start := pos
|
||||||
total_lines := count_lines(source)
|
caret_end := pos + 1
|
||||||
|
|
||||||
start_line := line - context_before
|
|
||||||
if start_line < 1 {
|
|
||||||
start_line = 1
|
|
||||||
}
|
|
||||||
end_line := line + context_after
|
|
||||||
if end_line > total_lines {
|
|
||||||
end_line = total_lines
|
|
||||||
}
|
|
||||||
|
|
||||||
// Width of the line-number column (right-align).
|
|
||||||
width := digit_count(end_line)
|
|
||||||
if width < 1 {
|
|
||||||
width = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
sb := strings.builder_make(context.temp_allocator)
|
|
||||||
defer strings.builder_destroy(&sb)
|
|
||||||
|
|
||||||
color := colorize
|
|
||||||
red, faint, reset := "", "", ""
|
|
||||||
if color {
|
|
||||||
red = ansi.CSI + ansi.FG_RED + ansi.SGR
|
|
||||||
faint = ansi.CSI + ansi.FAINT + ansi.SGR
|
|
||||||
reset = ansi.CSI + ansi.RESET + ansi.SGR
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header line: message.
|
|
||||||
strings.write_string(&sb, msg)
|
|
||||||
strings.write_byte(&sb, '\n')
|
|
||||||
|
|
||||||
// Location line: " --> path:line:col" (width spaces + arrow).
|
|
||||||
strings.write_string(&sb, faint)
|
|
||||||
for _ in 0 ..< width {
|
|
||||||
strings.write_byte(&sb, ' ')
|
|
||||||
}
|
|
||||||
strings.write_string(&sb, "--> ")
|
|
||||||
strings.write_string(&sb, reset)
|
|
||||||
fmt.sbprintf(&sb, "%s:%d:%d\n", path, line, col)
|
|
||||||
|
|
||||||
// Top gutter line.
|
|
||||||
write_gutter(&sb, width, faint, reset)
|
|
||||||
|
|
||||||
// Caret extent for the error line.
|
|
||||||
line_start, token_start, token_end := context_extent(source, pos)
|
|
||||||
if span > 0 {
|
if span > 0 {
|
||||||
token_start = pos
|
caret_end = pos + span
|
||||||
token_end = pos + span
|
} else {
|
||||||
|
_, ts, te := context_extent(source, pos)
|
||||||
|
caret_start = ts
|
||||||
|
caret_end = te
|
||||||
}
|
}
|
||||||
caret_start_col := token_start - line_start + 1
|
return diags.format_error(
|
||||||
caret_end_col := token_end - line_start + 1
|
path, source, pos, msg, hint,
|
||||||
if caret_end_col <= caret_start_col {
|
context_before, context_after,
|
||||||
caret_end_col = caret_start_col + 1
|
colorize,
|
||||||
}
|
caret_start, caret_end,
|
||||||
|
)
|
||||||
// Context lines.
|
|
||||||
for n in start_line ..= end_line {
|
|
||||||
// Line number (right-aligned, faint).
|
|
||||||
num_str := fmt.tprintf("%d", n)
|
|
||||||
strings.write_string(&sb, faint)
|
|
||||||
for _ in 0 ..< width - len(num_str) {
|
|
||||||
strings.write_byte(&sb, ' ')
|
|
||||||
}
|
|
||||||
strings.write_string(&sb, num_str)
|
|
||||||
strings.write_string(&sb, " | ")
|
|
||||||
strings.write_string(&sb, reset)
|
|
||||||
|
|
||||||
strings.write_string(&sb, line_text(source, n))
|
|
||||||
strings.write_byte(&sb, '\n')
|
|
||||||
|
|
||||||
// After the error line, emit the caret row.
|
|
||||||
if n == line {
|
|
||||||
strings.write_string(&sb, faint)
|
|
||||||
for _ in 0 ..< width + 1 {
|
|
||||||
strings.write_byte(&sb, ' ')
|
|
||||||
}
|
|
||||||
strings.write_string(&sb, "| ")
|
|
||||||
strings.write_string(&sb, reset)
|
|
||||||
|
|
||||||
for _ in 1 ..< caret_start_col {
|
|
||||||
strings.write_byte(&sb, ' ')
|
|
||||||
}
|
|
||||||
if color {
|
|
||||||
strings.write_string(&sb, red)
|
|
||||||
}
|
|
||||||
for _ in 0 ..< caret_end_col - caret_start_col {
|
|
||||||
strings.write_byte(&sb, '^')
|
|
||||||
}
|
|
||||||
if color {
|
|
||||||
strings.write_string(&sb, reset)
|
|
||||||
}
|
|
||||||
if hint != "" {
|
|
||||||
strings.write_byte(&sb, ' ')
|
|
||||||
if color {
|
|
||||||
strings.write_string(&sb, faint)
|
|
||||||
}
|
|
||||||
strings.write_string(&sb, hint)
|
|
||||||
if color {
|
|
||||||
strings.write_string(&sb, reset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strings.write_byte(&sb, '\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trailing gutter line for visual closure.
|
|
||||||
write_gutter(&sb, width, faint, reset)
|
|
||||||
|
|
||||||
return strings.to_string(sb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// write_gutter emits a faint pipe-only gutter line: `<width+1 spaces> |`.
|
|
||||||
write_gutter :: proc(sb: ^strings.Builder, width: int, faint: string, reset: string) {
|
|
||||||
strings.write_string(sb, faint)
|
|
||||||
for _ in 0 ..< width + 1 {
|
|
||||||
strings.write_byte(sb, ' ')
|
|
||||||
}
|
|
||||||
strings.write_string(sb, "|")
|
|
||||||
strings.write_string(sb, reset)
|
|
||||||
strings.write_byte(sb, '\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// format_render_error produces a diagnostic for an Error value using the
|
// format_render_error produces a diagnostic for an Error value using the
|
||||||
// template's path and source for context. Returns "" for nil errors.
|
// template's path and source for context. Returns "" for nil errors.
|
||||||
// If the error carries its own source/path (from tag_error), those are used
|
|
||||||
// instead of the passed-in template — this ensures errors inside partials
|
|
||||||
// point at the correct file.
|
|
||||||
format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false) -> string {
|
format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false) -> string {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -326,5 +104,22 @@ format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false)
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "<input>"
|
path = "<input>"
|
||||||
}
|
}
|
||||||
return format_error(path, source, b.pos, b.msg, hint = b.hint, colorize = colorize, span = b.span)
|
|
||||||
|
caret_start := b.pos
|
||||||
|
caret_end := b.pos + 1
|
||||||
|
if b.span > 0 {
|
||||||
|
caret_end = b.pos + b.span
|
||||||
|
} else {
|
||||||
|
_, ts, te := context_extent(source, b.pos)
|
||||||
|
caret_start = ts
|
||||||
|
caret_end = te
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags.format_error(
|
||||||
|
path, source, b.pos, b.msg,
|
||||||
|
hint = b.hint,
|
||||||
|
colorize = colorize,
|
||||||
|
caret_start = caret_start,
|
||||||
|
caret_end = caret_end,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-101
@@ -6,95 +6,13 @@ import "core:strings"
|
|||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// line_col / line_text / count_lines / digit_count — primitive helpers
|
// format_tag_error — golden output tests
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_line_col_basic :: proc(t: ^testing.T) {
|
|
||||||
src := "abc\ndef\nghi"
|
|
||||||
cases := [?]struct {
|
|
||||||
pos: int,
|
|
||||||
line: int,
|
|
||||||
col: int,
|
|
||||||
}{{0, 1, 1}, {2, 1, 3}, {3, 1, 4}, {4, 2, 1}, {6, 2, 3}, {7, 2, 4}, {8, 3, 1}}
|
|
||||||
for c in cases {
|
|
||||||
l, col := line_col(src, c.pos)
|
|
||||||
testing.expect(t, l == c.line, fmt.tprintf("pos %d: line %d, want %d", c.pos, l, c.line))
|
|
||||||
testing.expect(t, col == c.col, fmt.tprintf("pos %d: col %d, want %d", c.pos, col, c.col))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_line_col_empty :: proc(t: ^testing.T) {
|
|
||||||
l, col := line_col("", 0)
|
|
||||||
testing.expect_value(t, l, 1)
|
|
||||||
testing.expect_value(t, col, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_line_col_negative :: proc(t: ^testing.T) {
|
|
||||||
l, col := line_col("abc", -1)
|
|
||||||
testing.expect_value(t, l, 1)
|
|
||||||
testing.expect_value(t, col, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_line_col_past_end :: proc(t: ^testing.T) {
|
|
||||||
l, col := line_col("abc", 100)
|
|
||||||
testing.expect_value(t, l, 1)
|
|
||||||
testing.expect_value(t, col, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_line_text_first :: proc(t: ^testing.T) {
|
|
||||||
src := "first\nsecond\nthird"
|
|
||||||
testing.expect_value(t, line_text(src, 1), "first")
|
|
||||||
testing.expect_value(t, line_text(src, 2), "second")
|
|
||||||
testing.expect_value(t, line_text(src, 3), "third")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_line_text_trailing_newline :: proc(t: ^testing.T) {
|
|
||||||
src := "first\nsecond\n"
|
|
||||||
testing.expect_value(t, line_text(src, 1), "first")
|
|
||||||
testing.expect_value(t, line_text(src, 2), "second")
|
|
||||||
testing.expect_value(t, line_text(src, 3), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_line_text_out_of_range :: proc(t: ^testing.T) {
|
|
||||||
testing.expect_value(t, line_text("abc", 5), "")
|
|
||||||
testing.expect_value(t, line_text("abc", 0), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_count_lines :: proc(t: ^testing.T) {
|
|
||||||
testing.expect_value(t, count_lines(""), 1)
|
|
||||||
testing.expect_value(t, count_lines("abc"), 1)
|
|
||||||
testing.expect_value(t, count_lines("a\nb"), 2)
|
|
||||||
testing.expect_value(t, count_lines("a\nb\n"), 2)
|
|
||||||
testing.expect_value(t, count_lines("a\nb\nc"), 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_digit_count :: proc(t: ^testing.T) {
|
|
||||||
testing.expect_value(t, digit_count(0), 1)
|
|
||||||
testing.expect_value(t, digit_count(1), 1)
|
|
||||||
testing.expect_value(t, digit_count(9), 1)
|
|
||||||
testing.expect_value(t, digit_count(10), 2)
|
|
||||||
testing.expect_value(t, digit_count(99), 2)
|
|
||||||
testing.expect_value(t, digit_count(100), 3)
|
|
||||||
testing.expect_value(t, digit_count(-5), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// format_error — golden output tests
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_format_error_basic :: proc(t: ^testing.T) {
|
test_format_error_basic :: proc(t: ^testing.T) {
|
||||||
src := "line 1\nline 2\n{{bad}}\nline 4\nline 5"
|
src := "line 1\nline 2\n{{bad}}\nline 4\nline 5"
|
||||||
out := format_error("p.html", src, 14, "unknown key 'bad'", "", colorize = false)
|
out := format_tag_error("p.html", src, 14, "unknown key 'bad'", "", colorize = false)
|
||||||
expected := `unknown key 'bad'
|
expected := `unknown key 'bad'
|
||||||
--> p.html:3:1
|
--> p.html:3:1
|
||||||
|
|
|
|
||||||
@@ -112,7 +30,7 @@ test_format_error_basic :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_format_error_with_hint :: proc(t: ^testing.T) {
|
test_format_error_with_hint :: proc(t: ^testing.T) {
|
||||||
src := "line 1\nline 2\n{{titel}}\nline 4\nline 5"
|
src := "line 1\nline 2\n{{titel}}\nline 4\nline 5"
|
||||||
out := format_error(
|
out := format_tag_error(
|
||||||
"post.html",
|
"post.html",
|
||||||
src,
|
src,
|
||||||
14,
|
14,
|
||||||
@@ -137,7 +55,7 @@ test_format_error_with_hint :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_format_error_no_hint_omits_trailing_space :: proc(t: ^testing.T) {
|
test_format_error_no_hint_omits_trailing_space :: proc(t: ^testing.T) {
|
||||||
src := "{{bad}}"
|
src := "{{bad}}"
|
||||||
out := format_error("p.html", src, 0, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
|
||||||
// Caret line ends immediately after the carets — no trailing space.
|
// Caret line ends immediately after the carets — no trailing space.
|
||||||
testing.expect(t, strings.contains(out, "^^^^^^^\n"), out)
|
testing.expect(t, strings.contains(out, "^^^^^^^\n"), out)
|
||||||
testing.expect(t, !strings.contains(out, "^^^^^^^ \n"), out)
|
testing.expect(t, !strings.contains(out, "^^^^^^^ \n"), out)
|
||||||
@@ -150,7 +68,7 @@ test_format_error_no_hint_omits_trailing_space :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_format_error_first_line_only_after_context :: proc(t: ^testing.T) {
|
test_format_error_first_line_only_after_context :: proc(t: ^testing.T) {
|
||||||
src := "{{bad}}\nline 2\nline 3\nline 4\nline 5"
|
src := "{{bad}}\nline 2\nline 3\nline 4\nline 5"
|
||||||
out := format_error("p.html", src, 0, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
|
||||||
expected := `msg
|
expected := `msg
|
||||||
--> p.html:1:1
|
--> p.html:1:1
|
||||||
|
|
|
|
||||||
@@ -166,7 +84,7 @@ test_format_error_first_line_only_after_context :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_format_error_last_line_only_before_context :: proc(t: ^testing.T) {
|
test_format_error_last_line_only_before_context :: proc(t: ^testing.T) {
|
||||||
src := "line 1\nline 2\nline 3\nline 4\n{{bad}}"
|
src := "line 1\nline 2\nline 3\nline 4\n{{bad}}"
|
||||||
out := format_error("p.html", src, 28, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 28, "msg", "", colorize = false)
|
||||||
expected := `msg
|
expected := `msg
|
||||||
--> p.html:5:1
|
--> p.html:5:1
|
||||||
|
|
|
|
||||||
@@ -182,7 +100,7 @@ test_format_error_last_line_only_before_context :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_format_error_short_source_clamped :: proc(t: ^testing.T) {
|
test_format_error_short_source_clamped :: proc(t: ^testing.T) {
|
||||||
src := "x\n{{bad}}\ny"
|
src := "x\n{{bad}}\ny"
|
||||||
out := format_error("p.html", src, 2, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 2, "msg", "", colorize = false)
|
||||||
expected := `msg
|
expected := `msg
|
||||||
--> p.html:2:1
|
--> p.html:2:1
|
||||||
|
|
|
|
||||||
@@ -198,7 +116,7 @@ test_format_error_short_source_clamped :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_format_error_single_line_source :: proc(t: ^testing.T) {
|
test_format_error_single_line_source :: proc(t: ^testing.T) {
|
||||||
src := "{{bad}}"
|
src := "{{bad}}"
|
||||||
out := format_error("p.html", src, 0, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
|
||||||
expected := `msg
|
expected := `msg
|
||||||
--> p.html:1:1
|
--> p.html:1:1
|
||||||
|
|
|
|
||||||
@@ -214,7 +132,7 @@ test_format_error_two_digit_line_numbers :: proc(t: ^testing.T) {
|
|||||||
// 12-line source; error on line 9. end_line=11 → width=2.
|
// 12-line source; error on line 9. end_line=11 → width=2.
|
||||||
src := "l01\nl02\nl03\nl04\nl05\nl06\nl07\nl08\n{{bad}}\nl10\nl11\nl12"
|
src := "l01\nl02\nl03\nl04\nl05\nl06\nl07\nl08\n{{bad}}\nl10\nl11\nl12"
|
||||||
// Position of `{{bad}}`: 8 lines of "l0N\n" = 8*4 = 32 bytes.
|
// Position of `{{bad}}`: 8 lines of "l0N\n" = 8*4 = 32 bytes.
|
||||||
out := format_error("p.html", src, 32, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 32, "msg", "", colorize = false)
|
||||||
testing.expect(t, strings.contains(out, " --> p.html:9:1\n"), out)
|
testing.expect(t, strings.contains(out, " --> p.html:9:1\n"), out)
|
||||||
testing.expect(t, strings.contains(out, " |\n"), out)
|
testing.expect(t, strings.contains(out, " |\n"), out)
|
||||||
testing.expect(t, strings.contains(out, " 7 | l07\n"), out)
|
testing.expect(t, strings.contains(out, " 7 | l07\n"), out)
|
||||||
@@ -241,7 +159,7 @@ test_format_error_three_digit_line_numbers :: proc(t: ^testing.T) {
|
|||||||
pos += len(parts[i - 1]) + 1
|
pos += len(parts[i - 1]) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
out := format_error("p.html", src, pos, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, pos, "msg", "", colorize = false)
|
||||||
testing.expect(t, strings.contains(out, " --> p.html:100:1\n"), out)
|
testing.expect(t, strings.contains(out, " --> p.html:100:1\n"), out)
|
||||||
testing.expect(t, strings.contains(out, " |\n"), out)
|
testing.expect(t, strings.contains(out, " |\n"), out)
|
||||||
testing.expect(t, strings.contains(out, " 98 | l098\n"), out)
|
testing.expect(t, strings.contains(out, " 98 | l098\n"), out)
|
||||||
@@ -256,7 +174,7 @@ test_format_error_three_digit_line_numbers :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_caret_at_column_1 :: proc(t: ^testing.T) {
|
test_caret_at_column_1 :: proc(t: ^testing.T) {
|
||||||
src := "{{bad}} at start"
|
src := "{{bad}} at start"
|
||||||
out := format_error("p.html", src, 0, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
|
||||||
// Caret line should start with "^" right after "| " (no leading spaces).
|
// Caret line should start with "^" right after "| " (no leading spaces).
|
||||||
testing.expect(t, strings.contains(out, " | ^^^^^^^\n"), out)
|
testing.expect(t, strings.contains(out, " | ^^^^^^^\n"), out)
|
||||||
}
|
}
|
||||||
@@ -265,7 +183,7 @@ test_caret_at_column_1 :: proc(t: ^testing.T) {
|
|||||||
test_caret_at_column_N :: proc(t: ^testing.T) {
|
test_caret_at_column_N :: proc(t: ^testing.T) {
|
||||||
src := " {{bad}}"
|
src := " {{bad}}"
|
||||||
// pos=4 is the first '{'. Line 1, col 5.
|
// pos=4 is the first '{'. Line 1, col 5.
|
||||||
out := format_error("p.html", src, 4, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 4, "msg", "", colorize = false)
|
||||||
// 4 leading spaces, then 7 carets.
|
// 4 leading spaces, then 7 carets.
|
||||||
testing.expect(t, strings.contains(out, " | ^^^^^^^\n"), out)
|
testing.expect(t, strings.contains(out, " | ^^^^^^^\n"), out)
|
||||||
}
|
}
|
||||||
@@ -273,7 +191,7 @@ test_caret_at_column_N :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_caret_width_matches_token :: proc(t: ^testing.T) {
|
test_caret_width_matches_token :: proc(t: ^testing.T) {
|
||||||
src := "{{x}}"
|
src := "{{x}}"
|
||||||
out := format_error("p.html", src, 0, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
|
||||||
// {{x}} is 5 chars wide.
|
// {{x}} is 5 chars wide.
|
||||||
testing.expect(t, strings.contains(out, " | ^^^^^\n"), out)
|
testing.expect(t, strings.contains(out, " | ^^^^^\n"), out)
|
||||||
}
|
}
|
||||||
@@ -285,7 +203,7 @@ test_caret_width_matches_token :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_context_before_zero :: proc(t: ^testing.T) {
|
test_context_before_zero :: proc(t: ^testing.T) {
|
||||||
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
|
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
|
||||||
out := format_error(
|
out := format_tag_error(
|
||||||
"p.html",
|
"p.html",
|
||||||
src,
|
src,
|
||||||
9,
|
9,
|
||||||
@@ -309,7 +227,7 @@ test_context_before_zero :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_context_after_zero :: proc(t: ^testing.T) {
|
test_context_after_zero :: proc(t: ^testing.T) {
|
||||||
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
|
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
|
||||||
out := format_error(
|
out := format_tag_error(
|
||||||
"p.html",
|
"p.html",
|
||||||
src,
|
src,
|
||||||
9,
|
9,
|
||||||
@@ -333,7 +251,7 @@ test_context_after_zero :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_context_both_zero :: proc(t: ^testing.T) {
|
test_context_both_zero :: proc(t: ^testing.T) {
|
||||||
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
|
src := "l1\nl2\nl3\n{{bad}}\nl5\nl6"
|
||||||
out := format_error(
|
out := format_tag_error(
|
||||||
"p.html",
|
"p.html",
|
||||||
src,
|
src,
|
||||||
9,
|
9,
|
||||||
@@ -360,7 +278,7 @@ test_context_both_zero :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_gutter_pipes_align_with_source_pipe :: proc(t: ^testing.T) {
|
test_gutter_pipes_align_with_source_pipe :: proc(t: ^testing.T) {
|
||||||
src := "l1\n{{bad}}\nl3"
|
src := "l1\n{{bad}}\nl3"
|
||||||
out := format_error("p.html", src, 3, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 3, "msg", "", colorize = false)
|
||||||
// All "|" characters should appear at the same column.
|
// All "|" characters should appear at the same column.
|
||||||
// For width=1: source line is "N | ...", so "|" at col 2.
|
// For width=1: source line is "N | ...", so "|" at col 2.
|
||||||
// Empty gutter is " |" (width+1 spaces + "|"), so "|" at col 2.
|
// Empty gutter is " |" (width+1 spaces + "|"), so "|" at col 2.
|
||||||
@@ -382,7 +300,7 @@ test_gutter_pipes_align_with_source_pipe :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_arrow_points_at_pipe :: proc(t: ^testing.T) {
|
test_arrow_points_at_pipe :: proc(t: ^testing.T) {
|
||||||
src := "{{bad}}"
|
src := "{{bad}}"
|
||||||
out := format_error("p.html", src, 0, "msg", "", colorize = false)
|
out := format_tag_error("p.html", src, 0, "msg", "", colorize = false)
|
||||||
// For width=1: arrow line is " --> ..." so ">" at col 3.
|
// For width=1: arrow line is " --> ..." so ">" at col 3.
|
||||||
// Pipe lines are " |" so "|" at col 2.
|
// Pipe lines are " |" so "|" at col 2.
|
||||||
lines := strings.split(out, "\n", context.temp_allocator)
|
lines := strings.split(out, "\n", context.temp_allocator)
|
||||||
@@ -424,7 +342,7 @@ test_format_render_error_dispatch :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b := body(parse_err)
|
b := body(parse_err)
|
||||||
out := format_error("test.html", src, b.pos, b.msg, colorize = false)
|
out := format_tag_error("test.html", src, b.pos, b.msg, colorize = false)
|
||||||
testing.expect(t, strings.contains(out, "unclosed section"), out)
|
testing.expect(t, strings.contains(out, "unclosed section"), out)
|
||||||
testing.expect(t, strings.contains(out, "test.html:"), out)
|
testing.expect(t, strings.contains(out, "test.html:"), out)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-8
@@ -5,6 +5,8 @@ import "core:fmt"
|
|||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
|
import diags "../diagnostics"
|
||||||
|
|
||||||
// A hypothetical maximum context depth. Trying to pass more than this many items
|
// A hypothetical maximum context depth. Trying to pass more than this many items
|
||||||
// to render(tpl, data), or nesting templates further than this depth would be
|
// to render(tpl, data), or nesting templates further than this depth would be
|
||||||
// an error.
|
// an error.
|
||||||
@@ -842,7 +844,7 @@ warn_unknown_key :: proc(current: Template, ctx: []any, node: Node) {
|
|||||||
|
|
||||||
hint := ""
|
hint := ""
|
||||||
if len(available) > 0 {
|
if len(available) > 0 {
|
||||||
suggestion := suggest_correction(available, missing)
|
suggestion := diags.suggest_correction(available, missing)
|
||||||
if suggestion != "" {
|
if suggestion != "" {
|
||||||
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
||||||
}
|
}
|
||||||
@@ -852,7 +854,7 @@ warn_unknown_key :: proc(current: Template, ctx: []any, node: Node) {
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "<input>"
|
path = "<input>"
|
||||||
}
|
}
|
||||||
diag := format_error(path, current.source, node.pos, msg, hint, colorize = should_colorize())
|
diag := format_tag_error(path, current.source, node.pos, msg, hint, colorize = diags.should_colorize())
|
||||||
log.warnf("%s", diag)
|
log.warnf("%s", diag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -866,7 +868,7 @@ warn_missing_partial :: proc(
|
|||||||
) {
|
) {
|
||||||
hint := ""
|
hint := ""
|
||||||
available := collect_partial_names(partials)
|
available := collect_partial_names(partials)
|
||||||
suggestion := suggest_correction(available, name)
|
suggestion := diags.suggest_correction(available, name)
|
||||||
if suggestion != "" {
|
if suggestion != "" {
|
||||||
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
||||||
}
|
}
|
||||||
@@ -875,7 +877,7 @@ warn_missing_partial :: proc(
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "<input>"
|
path = "<input>"
|
||||||
}
|
}
|
||||||
diag := format_error(path, current.source, node.pos, msg, hint, colorize = should_colorize())
|
diag := format_tag_error(path, current.source, node.pos, msg, hint, colorize = diags.should_colorize())
|
||||||
log.warnf("%s", diag)
|
log.warnf("%s", diag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -911,7 +913,7 @@ warn_unmatched_block_overrides :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
hint := ""
|
hint := ""
|
||||||
suggestion := suggest_correction(available, child.key)
|
suggestion := diags.suggest_correction(available, child.key)
|
||||||
if suggestion != "" {
|
if suggestion != "" {
|
||||||
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
||||||
}
|
}
|
||||||
@@ -924,13 +926,13 @@ warn_unmatched_block_overrides :: proc(
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "<input>"
|
path = "<input>"
|
||||||
}
|
}
|
||||||
diag := format_error(
|
diag := format_tag_error(
|
||||||
path,
|
path,
|
||||||
current.source,
|
current.source,
|
||||||
child.pos,
|
child.pos,
|
||||||
msg,
|
msg,
|
||||||
hint,
|
hint,
|
||||||
colorize = should_colorize(),
|
colorize = diags.should_colorize(),
|
||||||
)
|
)
|
||||||
log.warnf("%s", diag)
|
log.warnf("%s", diag)
|
||||||
}
|
}
|
||||||
@@ -954,6 +956,6 @@ warn_context_depth :: proc(current: Template, node: Node) {
|
|||||||
if path == "" {
|
if path == "" {
|
||||||
path = "<input>"
|
path = "<input>"
|
||||||
}
|
}
|
||||||
diag := format_error(path, current.source, node.pos, msg, "", colorize = should_colorize())
|
diag := format_tag_error(path, current.source, node.pos, msg, "", colorize = diags.should_colorize())
|
||||||
log.warnf("%s", diag)
|
log.warnf("%s", diag)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,8 @@ import "core:log"
|
|||||||
import "core:reflect"
|
import "core:reflect"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
|
||||||
|
import diags "../diagnostics"
|
||||||
import "core:time/datetime"
|
import "core:time/datetime"
|
||||||
|
|
||||||
// MAX_PIPES was chosen arbitrarily. It holds no performance or logical
|
// MAX_PIPES was chosen arbitrarily. It holds no performance or logical
|
||||||
@@ -260,7 +262,7 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) ->
|
|||||||
op, ok := pipe_op_from_string(filter.op)
|
op, ok := pipe_op_from_string(filter.op)
|
||||||
if !ok {
|
if !ok {
|
||||||
hint := ""
|
hint := ""
|
||||||
suggestion := suggest_correction(pipe_op_candidates(), filter.op)
|
suggestion := diags.suggest_correction(pipe_op_candidates(), filter.op)
|
||||||
if suggestion != "" {
|
if suggestion != "" {
|
||||||
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
hint = fmt.tprintf("did you mean '%s'?", suggestion)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-25
@@ -3,6 +3,8 @@ package mustache
|
|||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
|
import diags "../diagnostics"
|
||||||
|
|
||||||
// collect_struct_keys enumerates the visible field names of a struct value,
|
// collect_struct_keys enumerates the visible field names of a struct value,
|
||||||
// including fields promoted via `using`-embedded structs.
|
// including fields promoted via `using`-embedded structs.
|
||||||
collect_struct_keys :: proc(val: any, allocator := context.temp_allocator) -> []string {
|
collect_struct_keys :: proc(val: any, allocator := context.temp_allocator) -> []string {
|
||||||
@@ -130,7 +132,7 @@ validate_key_path :: proc(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
available := collect_map_keys(current, allocator)
|
available := collect_map_keys(current, allocator)
|
||||||
if suggest_correction(available, parts[i]) != "" {
|
if diags.suggest_correction(available, parts[i]) != "" {
|
||||||
return false, parts[i], available
|
return false, parts[i], available
|
||||||
}
|
}
|
||||||
return true, "", nil
|
return true, "", nil
|
||||||
@@ -155,30 +157,6 @@ validate_key_path :: proc(
|
|||||||
return true, "", nil
|
return true, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// suggest_correction returns the closest match from `available` to `missing`
|
|
||||||
// using Levenshtein distance, or "" if no good match exists. The threshold
|
|
||||||
// scales with the length of the missing key.
|
|
||||||
suggest_correction :: proc(available: []string, missing: string) -> string {
|
|
||||||
if len(available) == 0 || len(missing) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
threshold := max(2, len(missing) / 3)
|
|
||||||
|
|
||||||
best: string
|
|
||||||
best_dist := threshold + 1
|
|
||||||
for candidate in available {
|
|
||||||
if abs(len(candidate) - len(missing)) > threshold {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
d := strings.levenshtein_distance(missing, candidate)
|
|
||||||
if d <= threshold && d < best_dist {
|
|
||||||
best = candidate
|
|
||||||
best_dist = d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect_partial_names enumerates the keys of the partials map.
|
// collect_partial_names enumerates the keys of the partials map.
|
||||||
collect_partial_names :: proc(
|
collect_partial_names :: proc(
|
||||||
partials: map[string]Template,
|
partials: map[string]Template,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import "core:encoding/json"
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
|
import diags "../diagnostics"
|
||||||
|
|
||||||
Inner :: struct {
|
Inner :: struct {
|
||||||
foo: string,
|
foo: string,
|
||||||
bar: int,
|
bar: int,
|
||||||
@@ -153,30 +155,6 @@ test_validate_map_path_silent :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, ok, true)
|
testing.expect_value(t, ok, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_suggest_correction_exact :: proc(t: ^testing.T) {
|
|
||||||
available := []string{"title", "page.title", "body"}
|
|
||||||
testing.expect_value(t, suggest_correction(available, "page_titel"), "page.title")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_suggest_correction_close :: proc(t: ^testing.T) {
|
|
||||||
available := []string{"title", "body", "now"}
|
|
||||||
testing.expect_value(t, suggest_correction(available, "titel"), "title")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_suggest_correction_no_match :: proc(t: ^testing.T) {
|
|
||||||
available := []string{"completely_different", "unrelated"}
|
|
||||||
testing.expect_value(t, suggest_correction(available, "page_titel"), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_suggest_correction_empty :: proc(t: ^testing.T) {
|
|
||||||
testing.expect_value(t, suggest_correction([]string{}, "anything"), "")
|
|
||||||
testing.expect_value(t, suggest_correction([]string{"a"}, ""), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_warn_no_false_positive_for_valid_keys :: proc(t: ^testing.T) {
|
test_warn_no_false_positive_for_valid_keys :: proc(t: ^testing.T) {
|
||||||
Data :: struct {
|
Data :: struct {
|
||||||
@@ -244,7 +222,7 @@ test_validate_key_path_map_typo :: proc(t: ^testing.T) {
|
|||||||
ok, missing, available := validate_key_path(ctx[:], "params.authr")
|
ok, missing, available := validate_key_path(ctx[:], "params.authr")
|
||||||
testing.expect(t, !ok, "typo of map key should not be ok")
|
testing.expect(t, !ok, "typo of map key should not be ok")
|
||||||
testing.expect_value(t, missing, "authr")
|
testing.expect_value(t, missing, "authr")
|
||||||
suggestion := suggest_correction(available, "authr")
|
suggestion := diags.suggest_correction(available, "authr")
|
||||||
testing.expect_value(t, suggestion, "author")
|
testing.expect_value(t, suggestion, "author")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import "mustache"
|
import "mustache"
|
||||||
|
|
||||||
|
import diags "diagnostics"
|
||||||
|
|
||||||
import "core:encoding/json"
|
import "core:encoding/json"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
@@ -40,12 +42,12 @@ load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
|
|||||||
b := mustache.body(err)
|
b := mustache.body(err)
|
||||||
log.errorf(
|
log.errorf(
|
||||||
"%s",
|
"%s",
|
||||||
mustache.format_error(
|
mustache.format_tag_error(
|
||||||
entry.fs_path,
|
entry.fs_path,
|
||||||
source,
|
source,
|
||||||
b.pos,
|
b.pos,
|
||||||
b.msg,
|
b.msg,
|
||||||
colorize = mustache.should_colorize(),
|
colorize = diags.should_colorize(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
os.exit(1)
|
os.exit(1)
|
||||||
@@ -142,7 +144,7 @@ render_template :: proc(
|
|||||||
formatted := mustache.format_render_error(
|
formatted := mustache.format_render_error(
|
||||||
err,
|
err,
|
||||||
content_tpl,
|
content_tpl,
|
||||||
colorize = mustache.should_colorize(),
|
colorize = diags.should_colorize(),
|
||||||
)
|
)
|
||||||
if formatted in reported_errors^ {
|
if formatted in reported_errors^ {
|
||||||
return ""
|
return ""
|
||||||
@@ -377,12 +379,12 @@ load_partials :: proc(vfs: ^VFS) -> map[string]mustache.Template {
|
|||||||
b := mustache.body(err)
|
b := mustache.body(err)
|
||||||
log.errorf(
|
log.errorf(
|
||||||
"%s",
|
"%s",
|
||||||
mustache.format_error(
|
mustache.format_tag_error(
|
||||||
entry.fs_path,
|
entry.fs_path,
|
||||||
source,
|
source,
|
||||||
b.pos,
|
b.pos,
|
||||||
b.msg,
|
b.msg,
|
||||||
colorize = mustache.should_colorize(),
|
colorize = diags.should_colorize(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
os.exit(1)
|
os.exit(1)
|
||||||
|
|||||||
+34
-8
@@ -346,14 +346,40 @@ TODO: Should this be a subsection of a "Data Types" section?
|
|||||||
|
|
||||||
Dates are strings in one of the following formats:
|
Dates are strings in one of the following formats:
|
||||||
|
|
||||||
| Format | Time zone |
|
<table>
|
||||||
| --------------------------- | ---------------------------- |
|
<thead>
|
||||||
| `2023-10-15T13:18:50-07:00` | `America/Los_Angeles` |
|
<tr>
|
||||||
| `2023-10-15T13:18:50-0700` | `America/Los_Angeles` |
|
<th>Format</th>
|
||||||
| `2023-10-15T13:18:50Z` | `Etc/UTC` |
|
<th>Time zone</th>
|
||||||
| `2023-10-15T13:18:50` | Default is local system time |
|
</tr>
|
||||||
| `2023-10-15` | Default is local system time |
|
</thead>
|
||||||
| `15 Oct 2023` | Default is local system time |
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code class="inline-code">2023-10-15T13:18:50-07:00</code></td>
|
||||||
|
<td><code class="inline-code">America/Los_Angeles</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code class="inline-code">2023-10-15T13:18:50-0700</code></td>
|
||||||
|
<td><code class="inline-code">America/Los_Angeles</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code class="inline-code">2023-10-15T13:18:50Z</code></td>
|
||||||
|
<td><code class="inline-code">Etc/UTC</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code class="inline-code">2023-10-15T13:18:50</code></td>
|
||||||
|
<td>Default is local system time</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code class="inline-code">2023-10-15</code></td>
|
||||||
|
<td>Default is local system time</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code class="inline-code">15 Oct 2023</code></td>
|
||||||
|
<td>Default is local system time</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
If you want to display a date in a different format, you can use the `| format` Filter. With no argument, it will default to formatting the date with `site.date_format`.
|
If you want to display a date in a different format, you can use the `| format` Filter. With no argument, it will default to formatting the date with `site.date_format`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user