mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 19:33:32 -04:00
218 lines
4.5 KiB
Odin
218 lines
4.5 KiB
Odin
package main
|
|
|
|
import "core:strings"
|
|
|
|
strip_html_tags :: proc(s: string, allocator := context.allocator) -> string {
|
|
sb := strings.builder_make(allocator)
|
|
defer strings.builder_destroy(&sb)
|
|
|
|
in_tag := false
|
|
start := 0
|
|
for i in 0 ..< len(s) {
|
|
if s[i] == '<' && !in_tag {
|
|
if i > start {
|
|
strings.write_string(&sb, s[start:i])
|
|
}
|
|
in_tag = true
|
|
} else if s[i] == '>' && in_tag {
|
|
in_tag = false
|
|
start = i + 1
|
|
}
|
|
}
|
|
if start == 0 {
|
|
return s
|
|
}
|
|
if !in_tag && start < len(s) {
|
|
strings.write_string(&sb, s[start:])
|
|
}
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
unescape_html :: proc(s: string) -> string {
|
|
sb := strings.builder_make_len_cap(0, len(s))
|
|
defer strings.builder_destroy(&sb)
|
|
|
|
start := 0
|
|
for i in 0 ..< len(s) {
|
|
if s[i] != '&' {
|
|
continue
|
|
}
|
|
semi := strings.index(s[i:], ";")
|
|
if semi < 0 {
|
|
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 {
|
|
strings.write_string(&sb, s[start:i])
|
|
}
|
|
strings.write_string(&sb, replacement)
|
|
start = i + semi + 1
|
|
}
|
|
if start == 0 {
|
|
return s
|
|
}
|
|
if start < len(s) {
|
|
strings.write_string(&sb, s[start:])
|
|
}
|
|
return strings.to_string(sb)
|
|
}
|
|
|
|
// generate_summary truncates an HTML string to the first max_words words.
|
|
// Walks forward counting whitespace→text transitions, skipping tag interiors
|
|
// so spaces inside attributes don't count. Returns a substring of the
|
|
// original — zero allocation. Mirrors Hugo's default (70 words).
|
|
generate_summary :: proc(html: string, max_words: int = 70) -> string {
|
|
if max_words <= 0 {
|
|
return ""
|
|
}
|
|
word_count := 0
|
|
in_word := false
|
|
in_tag := false
|
|
for i in 0 ..< len(html) {
|
|
c := html[i]
|
|
if in_tag {
|
|
if c == '>' {
|
|
in_tag = false
|
|
}
|
|
continue
|
|
}
|
|
if c == '<' {
|
|
in_tag = true
|
|
if in_word {
|
|
word_count += 1
|
|
if word_count >= max_words {
|
|
return html[:i]
|
|
}
|
|
in_word = false
|
|
}
|
|
continue
|
|
}
|
|
is_space := c == ' ' || c == '\n' || c == '\t' || c == '\r'
|
|
if is_space {
|
|
if in_word {
|
|
word_count += 1
|
|
if word_count >= max_words {
|
|
return html[:i]
|
|
}
|
|
in_word = false
|
|
}
|
|
} else {
|
|
in_word = true
|
|
}
|
|
}
|
|
return html
|
|
}
|
|
|
|
// generate_description converts an HTML fragment to plain text by stripping
|
|
// tags, decoding entities, and collapsing whitespace. Emits a space when
|
|
// exiting any tag so block-level boundaries aren't lost. Intended for OG
|
|
// descriptions — operate on the output of generate_summary for bounded input.
|
|
generate_description :: proc(html: string, allocator := context.temp_allocator) -> string {
|
|
sb := strings.builder_make_len_cap(0, len(html), allocator)
|
|
defer strings.builder_destroy(&sb)
|
|
|
|
in_tag := false
|
|
prev_was_space := true
|
|
run_start := 0
|
|
i := 0
|
|
for i < len(html) {
|
|
c := html[i]
|
|
if in_tag {
|
|
if c == '>' {
|
|
in_tag = false
|
|
if !prev_was_space {
|
|
strings.write_byte(&sb, ' ')
|
|
prev_was_space = true
|
|
}
|
|
}
|
|
i += 1
|
|
run_start = i
|
|
continue
|
|
}
|
|
if c == '<' {
|
|
if i > run_start {
|
|
strings.write_string(&sb, html[run_start:i])
|
|
prev_was_space = false
|
|
}
|
|
in_tag = true
|
|
i += 1
|
|
continue
|
|
}
|
|
if c == '&' {
|
|
if i > run_start {
|
|
strings.write_string(&sb, html[run_start:i])
|
|
prev_was_space = false
|
|
}
|
|
semi := strings.index(html[i:], ";")
|
|
if semi > 0 && semi <= 5 {
|
|
entity := html[i:i + semi + 1]
|
|
replacement := ""
|
|
switch entity {
|
|
case "&":
|
|
replacement = "&"
|
|
case "<":
|
|
replacement = "<"
|
|
case ">":
|
|
replacement = ">"
|
|
case """:
|
|
replacement = "\""
|
|
case "'", "'":
|
|
replacement = "'"
|
|
case:
|
|
replacement = ""
|
|
}
|
|
if replacement != "" {
|
|
strings.write_string(&sb, replacement)
|
|
prev_was_space = false
|
|
i += semi + 1
|
|
run_start = i
|
|
continue
|
|
}
|
|
}
|
|
strings.write_byte(&sb, '&')
|
|
prev_was_space = false
|
|
i += 1
|
|
run_start = i
|
|
continue
|
|
}
|
|
if c == ' ' || c == '\n' || c == '\t' || c == '\r' {
|
|
if i > run_start {
|
|
strings.write_string(&sb, html[run_start:i])
|
|
prev_was_space = false
|
|
}
|
|
if !prev_was_space {
|
|
strings.write_byte(&sb, ' ')
|
|
prev_was_space = true
|
|
}
|
|
i += 1
|
|
run_start = i
|
|
continue
|
|
}
|
|
i += 1
|
|
}
|
|
if i > run_start && !in_tag {
|
|
strings.write_string(&sb, html[run_start:i])
|
|
}
|
|
|
|
result := strings.to_string(sb)
|
|
if len(result) > 0 && result[len(result) - 1] == ' ' {
|
|
result = result[:len(result) - 1]
|
|
}
|
|
return result
|
|
}
|