taler-merchant-demos

Python-based Frontends for the Demonstration Web site
Log | Files | Refs | README | LICENSE

articles.go (6022B)


      1 package web
      2 
      3 import (
      4 	"bytes"
      5 	"fmt"
      6 	"html"
      7 	"html/template"
      8 	"io/fs"
      9 	"net/http"
     10 	"net/url"
     11 	"path"
     12 	"regexp"
     13 	"sort"
     14 	"strings"
     15 	"time"
     16 )
     17 
     18 type Article struct {
     19 	Slug       string
     20 	Title      string
     21 	Teaser     template.HTML
     22 	Summary    string
     23 	Contents   template.HTML
     24 	Lang       string
     25 	ExtraFiles map[string]string
     26 }
     27 
     28 type articleLibrary map[string]map[string]Article
     29 
     30 var (
     31 	h2Pattern                 = regexp.MustCompile(`(?is)<h2(?:\s[^>]*)?>(.*?)</h2>`)
     32 	teaserPattern             = regexp.MustCompile(`(?is)<p(?:\s[^>]*)?\bid\s*=\s*["']teaser["'][^>]*>.*?</p>`)
     33 	paragraph                 = regexp.MustCompile(`(?is)<p(?:\s[^>]*)?>.*?</p>`)
     34 	tagPattern                = regexp.MustCompile(`(?s)<[^>]*>`)
     35 	spacePattern              = regexp.MustCompile(`\s+`)
     36 	localFileReferencePattern = regexp.MustCompile(`(?i)(?:src|href)\s*=\s*["'](/[^"'?#]+)["']`)
     37 )
     38 
     39 var articleLocales = map[string]string{
     40 	"en": "en", "fr": "fr", "it": "it", "ru": "ru", "tr": "tr",
     41 	"uk": "uk", "de": "de", "es": "es", "br": "pt", "pt": "pt",
     42 }
     43 
     44 var selectedArticleSlugs = map[string]bool{
     45 	"free-sw": true, "thegnuproject": true, "initial-announcement": true,
     46 	"manifesto": true, "why-free": true, "free-doc": true, "selling": true,
     47 	"categories": true, "open-source-misses-the-point": true, "not-ipr": true,
     48 	"words-to-avoid": true, "right-to-read": true, "misinterpreting-copyright": true,
     49 	"why-copyleft": true, "pragmatic": true, "trivial-patent": true,
     50 	"can-you-trust": true, "javascript-trap": true,
     51 	"who-does-that-server-really-serve": true, "compromise": true,
     52 }
     53 
     54 func loadArticles() (articleLibrary, error) {
     55 	library := make(articleLibrary)
     56 	extraFiles, err := availableArticleData()
     57 	if err != nil {
     58 		return nil, err
     59 	}
     60 	languages, err := fs.ReadDir(Assets, "assets/articles")
     61 	if err != nil {
     62 		return nil, fmt.Errorf("read articles: %w", err)
     63 	}
     64 	for _, language := range languages {
     65 		sourceLang := language.Name()
     66 		locale, supported := articleLocales[sourceLang]
     67 		if !language.IsDir() || !supported {
     68 			continue
     69 		}
     70 		files, err := fs.ReadDir(Assets, path.Join("assets/articles", sourceLang))
     71 		if err != nil {
     72 			return nil, err
     73 		}
     74 		library[locale] = make(map[string]Article)
     75 		for _, file := range files {
     76 			if file.IsDir() {
     77 				continue
     78 			}
     79 			contents, err := Assets.ReadFile(path.Join("assets/articles", sourceLang, file.Name()))
     80 			if err != nil {
     81 				return nil, err
     82 			}
     83 			slug := strings.TrimSuffix(file.Name(), path.Ext(file.Name()))
     84 			if !selectedArticleSlugs[slug] {
     85 				continue
     86 			}
     87 			article, ok := parseArticle(locale, slug, string(contents))
     88 			if !ok {
     89 				continue
     90 			}
     91 			article.Contents, article.ExtraFiles = rewriteArticleData(article.Slug, article.Contents, extraFiles)
     92 			article.Teaser, _ = rewriteArticleData(article.Slug, article.Teaser, extraFiles)
     93 			library[locale][article.Slug] = article
     94 		}
     95 	}
     96 	return library, nil
     97 }
     98 
     99 func availableArticleData() (map[string]string, error) {
    100 	entries, err := fs.ReadDir(Assets, "assets/data")
    101 	if err != nil {
    102 		return nil, fmt.Errorf("read article data: %w", err)
    103 	}
    104 	files := make(map[string]string)
    105 	for _, entry := range entries {
    106 		if !entry.IsDir() {
    107 			files[entry.Name()] = path.Join("assets/data", entry.Name())
    108 		}
    109 	}
    110 	return files, nil
    111 }
    112 
    113 func rewriteArticleData(slug string, contents template.HTML, available map[string]string) (template.HTML, map[string]string) {
    114 	raw := string(contents)
    115 	matches := localFileReferencePattern.FindAllStringSubmatchIndex(raw, -1)
    116 	linked := make(map[string]string)
    117 	var rewritten strings.Builder
    118 	last := 0
    119 	for _, match := range matches {
    120 		name := path.Base(raw[match[2]:match[3]])
    121 		assetPath, ok := available[name]
    122 		if !ok {
    123 			continue
    124 		}
    125 		rewritten.WriteString(raw[last:match[2]])
    126 		rewritten.WriteString(url.PathEscape(slug) + "/data/" + url.PathEscape(name))
    127 		last = match[3]
    128 		linked[name] = assetPath
    129 	}
    130 	if last == 0 {
    131 		return contents, linked
    132 	}
    133 	rewritten.WriteString(raw[last:])
    134 	return template.HTML(rewritten.String()), linked
    135 }
    136 
    137 func (a *App) articleData(w http.ResponseWriter, r *http.Request) {
    138 	lang, ok := a.blogLanguage(w, r)
    139 	if !ok {
    140 		return
    141 	}
    142 	article, exists := a.articles[lang][r.PathValue("article")]
    143 	if !exists {
    144 		a.renderError(w, r, http.StatusNotFound, lang, "Page not found", nil)
    145 		return
    146 	}
    147 	name := r.PathValue("file")
    148 	assetPath, exists := article.ExtraFiles[name]
    149 	if !exists {
    150 		a.renderError(w, r, http.StatusNotFound, lang, "Supplemental file not found", nil)
    151 		return
    152 	}
    153 	contents, err := Assets.ReadFile(assetPath)
    154 	if err != nil {
    155 		a.renderError(w, r, http.StatusInternalServerError, lang, "Internal error", err)
    156 		return
    157 	}
    158 	w.Header().Set("Cache-Control", "public, max-age=3600")
    159 	w.Header().Set("X-Content-Type-Options", "nosniff")
    160 	http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(contents))
    161 }
    162 
    163 func parseArticle(lang, slug, contents string) (Article, bool) {
    164 	titleMatch := h2Pattern.FindStringSubmatch(contents)
    165 	if len(titleMatch) != 2 {
    166 		return Article{}, false
    167 	}
    168 	title := strings.TrimSpace(html.UnescapeString(spacePattern.ReplaceAllString(tagPattern.ReplaceAllString(titleMatch[1], ""), " ")))
    169 	if title == "" {
    170 		return Article{}, false
    171 	}
    172 	teaser := teaserPattern.FindString(contents)
    173 	if teaser == "" {
    174 		for _, candidate := range paragraph.FindAllString(contents, -1) {
    175 			if len(strings.TrimSpace(tagPattern.ReplaceAllString(candidate, ""))) >= 100 {
    176 				teaser = candidate
    177 				break
    178 			}
    179 		}
    180 	}
    181 	if teaser == "" {
    182 		return Article{}, false
    183 	}
    184 	summary := strings.TrimSpace(html.UnescapeString(spacePattern.ReplaceAllString(tagPattern.ReplaceAllString(teaser, ""), " ")))
    185 	return Article{
    186 		Slug: slug, Title: title, Teaser: template.HTML(teaser), Summary: summary,
    187 		Contents: template.HTML(contents), Lang: lang,
    188 	}, true
    189 }
    190 
    191 func sortedArticles(articles map[string]Article) []Article {
    192 	result := make([]Article, 0, len(articles))
    193 	for _, article := range articles {
    194 		result = append(result, article)
    195 	}
    196 	sort.Slice(result, func(i, j int) bool { return result[i].Title < result[j].Title })
    197 	return result
    198 }