diff options
Diffstat (limited to 'internal/liturgy/parse.go')
| -rw-r--r-- | internal/liturgy/parse.go | 209 |
1 files changed, 0 insertions, 209 deletions
diff --git a/internal/liturgy/parse.go b/internal/liturgy/parse.go deleted file mode 100644 index 4c1a935..0000000 --- a/internal/liturgy/parse.go +++ /dev/null @@ -1,209 +0,0 @@ -package liturgy - -import ( - "fmt" - "html" - "regexp" - "strings" -) - -// lectionaryTabs lists the two reading sets carried on the page: the -// lectionary in force since Advent 2015 ("nowy") and the one it replaced -// ("stary"). They are not interchangeable -- the acclamation can cite a -// different book entirely, and the older set contains malformed citations. -// Pick by id, never by position in the markup, or a reordering silently -// switches edition. -var lectionaryTabs = []string{"tabnowy0all", "tabstary0all"} - -var ( - brRe = regexp.MustCompile(`(?i)<br\s*/?>`) - tagRe = regexp.MustCompile(`<[^>]+>`) - wsRe = regexp.MustCompile(`[ \t]+`) - h2Re = regexp.MustCompile(`(?s)<h2>(.*?)</h2>`) - h4Re = regexp.MustCompile(`(?s)<h4>(.*?)</h4>`) - pRe = regexp.MustCompile(`(?s)<p>(.*?)</p>`) - citationRe = regexp.MustCompile(`\((.+)\)\s*$`) - - // dayNamePRe matches every classed <p><em>...</em></p> on the page; only - // the one whose class carries both "fw-bold" and a "color-" role (see - // dayNameParaMatches) is the day's celebration name -- the page also - // carries a plain fw-bold (no color-) lookalike higher up that must not - // win instead. - dayNamePRe = regexp.MustCompile(`(?s)<p class="([^"]*)">\s*<em>(.*?)</em>\s*</p>`) - - // dayColourRe matches niedziela.pl's "Kolor szat: <word>" vestment-colour - // line, tolerating the <span>/<strong> markup wrapped around the colour - // word on the page (see internal/liturgy/testdata/2026-07-22.html). - dayColourRe = regexp.MustCompile(`Kolor szat:\s*(?:<[^>]+>\s*)*([\p{L}]+)`) -) - -// modernColours maps niedziela.pl's Polish vestment-colour words to -// DayInfo's normalized colour names; anything not listed here (including a -// multi-option line like "zielony albo biały albo czerwony", which matches -// only its first word) is left for the caller to treat as "" if absent. -var modernColours = map[string]string{ - "biały": "white", - "zielony": "green", - "fioletowy": "violet", - "czerwony": "red", - "różowy": "rose", -} - -// panePattern matches the opening tag of the tab-pane div carrying the given -// tab id, e.g. `<div class="tab-pane fade " id="tabnowy0all">`. -func panePattern(tab string) *regexp.Regexp { - return regexp.MustCompile(`<div class="tab-pane[^"]*"\s+id="` + regexp.QuoteMeta(tab) + `">`) -} - -// htmlToLines turns an HTML fragment into a list of non-empty text lines. -// <br> marks a verse line break (used in psalms/acclamations); other tags -// are dropped, entities decoded, and intra-line whitespace collapsed. -func htmlToLines(fragment string) []string { - fragment = brRe.ReplaceAllString(fragment, "\n") - fragment = tagRe.ReplaceAllString(fragment, "") - text := html.UnescapeString(fragment) - var lines []string - for _, ln := range strings.Split(text, "\n") { - ln = strings.TrimSpace(wsRe.ReplaceAllString(ln, " ")) - if ln != "" { - lines = append(lines, ln) - } - } - return lines -} - -// Parse extracts every reading section from the page's preferred lectionary -// tab (falling back to the superseded one), erroring loudly if neither tab is -// present on the page, or the tab is found but carries no sections -- a -// layout change should never be mistaken for a quiet day with no readings. -func Parse(pageHTML string) ([]Section, error) { - var loc []int - for _, tab := range lectionaryTabs { - if m := panePattern(tab).FindStringIndex(pageHTML); m != nil { - loc = m - break - } - } - if loc == nil { - if strings.Contains(pageHTML, "Przykro nam") { - return nil, fmt.Errorf("no reading published for this date yet") - } - return nil, fmt.Errorf( - "no reading tab (%s) found on page -- the site layout may have changed", - strings.Join(lectionaryTabs, "/"), - ) - } - - rest := pageHTML[loc[1]:] - block := rest - if nxt := strings.Index(rest, `<div class="tab-pane`); nxt != -1 { - block = rest[:nxt] - } - - heads := h2Re.FindAllStringSubmatchIndex(block, -1) - var sections []Section - czytanieCount := 0 - for i, h := range heads { - bodyEnd := len(block) - if i+1 < len(heads) { - bodyEnd = heads[i+1][0] - } - body := block[h[1]:bodyEnd] - - heading := strings.Join(htmlToLines(block[h[2]:h[3]]), " ") - - subtitle := "" - if sub := h4Re.FindStringSubmatch(body); sub != nil { - subtitle = strings.Join(htmlToLines(sub[1]), " ") - } - - var paragraphs [][]string - for _, p := range pRe.FindAllStringSubmatch(body, -1) { - if lines := htmlToLines(p[1]); len(lines) > 0 { - paragraphs = append(paragraphs, lines) - } - } - - // A heading is expected to always carry a parenthetical citation; - // if one is somehow missing, leave Citation empty rather than - // failing the whole parse over one section. - citation, _ := ExtractCitation(heading) - - sections = append(sections, Section{ - Heading: heading, - Subtitle: subtitle, - Citation: citation, - PartID: partID(heading, &czytanieCount), - Paragraphs: paragraphs, - }) - } - - // A layout change can leave the tab findable but empty; say so rather - // than returning nothing and looking like a quiet day. - if len(sections) == 0 { - return nil, fmt.Errorf("reading tab found but no sections in it -- the site layout may have changed") - } - return sections, nil -} - -// ParseDayInfo extracts the day's celebration name and liturgical colour -// from a niedziela.pl page: Name is the inner text of the <p class="... -// fw-bold color-XXX"><em>NAME</em></p> paragraph (there is also an earlier, -// plain fw-bold-but-no-color- lookalike on the page -- see dayNamePRe -- -// which must not match instead), and Colour comes from the page's "Kolor -// szat: <word>" line, mapped via modernColours (case-insensitive; unknown -// word -> ""). Season is always "" -- the modern lectionary folds its -// temporal context into Name on temporal days rather than carrying it -// separately. A page whose markup doesn't match either pattern (a layout -// change, or a fixture with neither) yields a zero DayInfo, never an error: -// the readings are the load-bearing content, the header is a nice-to-have. -func ParseDayInfo(pageHTML string) DayInfo { - var info DayInfo - - for _, m := range dayNamePRe.FindAllStringSubmatch(pageHTML, -1) { - class := m[1] - if strings.Contains(class, "fw-bold") && strings.Contains(class, "color-") { - info.Name = strings.Join(htmlToLines(m[2]), " ") - break - } - } - - if m := dayColourRe.FindStringSubmatch(pageHTML); m != nil { - info.Colour = modernColours[strings.ToLower(m[1])] - } - - return info -} - -// partID assigns the stable liturgical-part identifier for a section heading. -// A second "1. czytanie" heading on the same day (a split feast offering two -// alternative first readings) becomes "drugie_czytanie" instead of colliding -// with the first ("pierwsze_czytanie"). Anything unrecognised is "". -func partID(heading string, czytanieCount *int) string { - switch { - case strings.HasPrefix(heading, "1. czytanie"): - *czytanieCount++ - if *czytanieCount == 1 { - return "pierwsze_czytanie" - } - return "drugie_czytanie" - case strings.HasPrefix(heading, "Psalm"): - return "psalm" - case strings.HasPrefix(heading, "Aklamacja"): - return "aklamacja" - case strings.HasPrefix(heading, "Ewangelia"): - return "ewangelia" - default: - return "" - } -} - -// ExtractCitation pulls the citation from a section heading: -// "Ewangelia (Mt 7, 1-5)" -> "Mt 7, 1-5". -func ExtractCitation(heading string) (string, error) { - m := citationRe.FindStringSubmatch(heading) - if m == nil { - return "", fmt.Errorf("no reference found in heading: %q", heading) - } - return strings.TrimSpace(m[1]), nil -} |
