mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
145 lines
3.6 KiB
Odin
145 lines
3.6 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(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 produces a plain-text summary of an HTML fragment.
|
|
// Blocks (paragraphs, headings, list items) are extracted, their tags
|
|
// stripped, entities decoded, and accumulated word-by-word until the
|
|
// max_words threshold is crossed — at which point the rest of the
|
|
// current block is included before stopping. Mirrors Hugo's default
|
|
// summary behavior.
|
|
generate_summary :: proc(html: string, max_words: int = 70) -> string {
|
|
separated, _ := strings.replace_all(html, "</p>", "\n\n", context.temp_allocator)
|
|
separated, _ = strings.replace_all(separated, "</h1>", "\n\n")
|
|
separated, _ = strings.replace_all(separated, "</h2>", "\n\n")
|
|
separated, _ = strings.replace_all(separated, "</h3>", "\n\n")
|
|
separated, _ = strings.replace_all(separated, "</h4>", "\n\n")
|
|
separated, _ = strings.replace_all(separated, "</h5>", "\n\n")
|
|
separated, _ = strings.replace_all(separated, "</h6>", "\n\n")
|
|
separated, _ = strings.replace_all(separated, "</li>", "\n\n")
|
|
separated, _ = strings.replace_all(separated, "</blockquote>", "\n\n")
|
|
|
|
stripped := strip_html_tags(separated, context.temp_allocator)
|
|
plain := unescape_html(stripped)
|
|
|
|
blocks := strings.split(plain, "\n\n", allocator = context.temp_allocator)
|
|
defer delete(blocks)
|
|
|
|
sb := strings.builder_make(context.temp_allocator)
|
|
defer strings.builder_destroy(&sb)
|
|
|
|
word_count := 0
|
|
first := true
|
|
for raw_block in blocks {
|
|
block := strings.trim_space(raw_block)
|
|
if len(block) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Collapse internal whitespace to single spaces.
|
|
block_sb := strings.builder_make(context.temp_allocator)
|
|
has_content := false
|
|
in_space := true
|
|
for c in block {
|
|
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
|
|
in_space = true
|
|
} else {
|
|
if in_space && has_content {
|
|
strings.write_byte(&block_sb, ' ')
|
|
}
|
|
strings.write_rune(&block_sb, c)
|
|
in_space = false
|
|
has_content = true
|
|
}
|
|
}
|
|
collapsed := strings.to_string(block_sb)
|
|
words := strings.split(collapsed, " ", allocator = context.temp_allocator)
|
|
|
|
if !first && word_count > 0 {
|
|
strings.write_byte(&sb, ' ')
|
|
}
|
|
strings.write_string(&sb, collapsed)
|
|
word_count += len(words)
|
|
first = false
|
|
|
|
delete(words)
|
|
|
|
if word_count >= max_words {
|
|
break
|
|
}
|
|
}
|
|
|
|
return strings.to_string(sb)
|
|
}
|
|
|