]> git.phdru.name Git - phdru.name/phdru.name.git/blob - reindex_blog.py
Feat(blog): Encode tags
[phdru.name/phdru.name.git] / reindex_blog.py
1 #! /usr/bin/env python
2 # -*- coding: koi8-r -*-
3
4 __author__ = "Oleg Broytman <phd@phdru.name>"
5 __copyright__ = "Copyright (C) 2006-2024 PhiloSoft Design"
6
7 import sys, os
8 try:
9     from urllib.parse import quote
10 except ImportError:
11     from urllib import quote
12
13 from Cheetah.Template import Template
14 from Cheetah.compat import string_type
15 from blog_db import blog_root, load_blog, save_blog
16
17
18 old_blog = load_blog()
19
20 # blog is a dictionary mapping
21 # (year, month, day) => [list of (file, title, lead, tags)]
22
23 blog = {}
24 years = {}
25
26 # bodies is a dictionary mapping file => body
27
28 bodies = {}
29
30 # Walk the directory recursively
31 for dirpath, dirs, files in os.walk(blog_root):
32    d = os.path.basename(dirpath)
33    if not d.startswith("20") and not d.isdigit():
34       continue
35    for file in files:
36       if not file.endswith(".tmpl"):
37          continue
38       fullpath = os.path.join(dirpath, file)
39       template = Template(file=fullpath)
40       title_parts = template.Title.decode('utf-8').encode('koi8-r').split()
41       title = ' '.join(title_parts[6:])
42       lead = template.Lead.decode('utf-8').encode('koi8-r')
43
44       tags = template.Tag
45       if isinstance(tags, string_type):
46          tags = (tags,)
47       tags = [tag.decode('utf-8').encode('koi8-r') for tag in tags]
48
49       if title:
50          key = year, month, day = tuple(dirpath[len(blog_root):].split(os.sep)[1:])
51          if key in blog:
52             days = blog[key]
53          else:
54             days = blog[key] = []
55          days.append((file, title, lead, tags))
56
57          if year in years:
58             months = years[year]
59          else:
60             months = years[year] = {}
61
62          if month in months:
63             days = months[month]
64          else:
65             days = months[month] = []
66
67          if day not in days: days.append(day)
68
69          file = file[:-len("tmpl")] + "html"
70          key = (year, month, day, file)
71          body = template.body()
72          if isinstance(body, unicode):
73             body = body.encode('koi8-r')
74          bodies[key] = body
75
76 # Need to save the blog?
77 if blog != old_blog:
78     save_blog(blog)
79
80 # Localized month names
81
82 import locale
83 locale.setlocale(locale.LC_ALL, "ru_RU.KOI8-R")
84 from calendar import _localized_month
85
86 locale.setlocale(locale.LC_TIME, 'C')
87 months_names_en = list(_localized_month('%B'))
88 months_abbrs_en = list(_localized_month('%b'))
89
90 locale.setlocale(locale.LC_TIME, "ru_RU.KOI8-R")
91 # months_names_ru = list(_localized_month('%B'))
92
93 months_names_ru = ['', "января", "февраля", "марта", "апреля", "мая", "июня",
94    "июля", "августа", "сентября", "октября", "ноября", "декабря"
95 ]
96
97 months_names_ru0 = ['', "январь", "февраль", "март", "апрель", "май", "июнь",
98    "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"
99 ]
100
101 from news import write_if_changed
102
103 def encode_tag(tag):
104     return quote(tag.replace(' ', '_'))
105
106 def write_template(level, year, month, day, titles, tags=None):
107    path = [blog_root]
108    if level >= 1:
109       path.append(year)
110    if level >= 2:
111       path.append(month)
112    if level == 3:
113       path.append(day)
114    path.append("index.tmpl")
115    index_name = os.path.join(*path)
116
117    new_text = ["""\
118 ## THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT.
119 #encoding koi8-r
120 #extends phd_site
121 #implements respond
122 """]
123
124    if level == 0:
125       new_text.append("""\
126 #attr $Title = "Oleg Broytman's blog"
127 #attr $Description = "Broytman Russian Blog Index Document"
128 #attr $Copyright = %(cyear)s
129 #attr $alternates = (("Новости [Atom 1.0] только заголовки", "application/atom+xml", "atom_10_titles.xml"),
130                      ("Новости [Atom 1.0]", "application/atom+xml", "atom_10.xml"),
131                      ("Новости [Atom 1.0] полные тексты", "application/atom+xml", "atom_10_full.xml"),
132                      ("Новости [RSS 2.0] только заголовки",  "application/rss+xml",  "rss_20_titles.xml"),
133                      ("Новости [RSS 2.0]",  "application/rss+xml",  "rss_20.xml"),
134                      ("Новости [RSS 2.0] полные тексты",  "application/rss+xml",  "rss_20_full.xml"),
135 )
136 ##
137 #def body_html
138 <h1>Журнал</h1>
139 """ % {"cyear": year or 2005})
140
141    elif level == 1:
142       new_text.append("""\
143 #attr $Title = "Oleg Broytman's blog: %(year)s"
144 #attr $Description = "Broytman Russian Blog %(year)s Index Document"
145 #attr $Copyright = %(cyear)s
146 ##
147 #def body_html
148 <h1>Журнал: %(year)s</h1>
149 """ % {"year": year, "cyear": year or 2005})
150
151    elif level == 2:
152       imonth = int(month)
153       new_text.append("""\
154 #attr $Title = "Oleg Broytman's blog: %(month_abbr_en)s %(year)s"
155 #attr $Description = "Broytman Russian Blog %(month_name_en)s %(year)s Index Document"
156 #attr $Copyright = %(cyear)s
157 ##
158 #def body_html
159 <h1>Журнал: %(month_name_ru0)s %(year)s</h1>
160 """ % {
161       "year": year, "cyear": year or 2005,
162       "month_abbr_en": months_abbrs_en[imonth], "month_name_en": months_names_en[imonth],
163       "month_name_ru0": months_names_ru0[imonth],
164    })
165
166    elif level == 3:
167       iday = int(day)
168       imonth = int(month)
169
170       if len(titles) == 1:
171          new_text.append("""\
172 #attr $Refresh = "0; URL=%s"
173 """ % titles[0][3])
174
175       new_text.append("""\
176 #attr $Title = "Oleg Broytman's blog: %(day)d %(month_abbr_en)s %(year)s"
177 #attr $Description = "Broytman Russian Blog %(day)d %(month_name_en)s %(year)s Index Document"
178 #attr $Copyright = %(cyear)s
179 ##
180 #def body_html
181 <h1>Журнал: %(day)d %(month_name_ru)s %(year)s</h1>
182 """ % {
183       "year": year, "cyear": year or 2005,
184       "month_abbr_en": months_abbrs_en[imonth], "month_name_en": months_names_en[imonth],
185       "month_name_ru": months_names_ru[imonth],
186       "day": iday
187    })
188
189    save_titles = titles[:]
190    titles.reverse()
191
192    save_date = None
193    for year, month, day, file, title, lead in titles:
194       href = []
195       if level == 0:
196          href.append(year)
197       if level <= 1:
198          href.append(month)
199       if level <= 2:
200          href.append(day)
201       href.append(file)
202       href = '/'.join(href)
203       if day[0] == '0': day = day[1:]
204       if save_date != (year, month, day):
205          if level == 0:
206             new_text.append('\n<h2>%s %s %s</h2>' % (day, months_names_ru[int(month)], year))
207          else:
208             new_text.append('\n<h2>%s %s</h2>' % (day, months_names_ru[int(month)]))
209          save_date = year, month, day
210       new_text.append('''
211 <p class="head">
212    %s<a href="%s">%s</a>.
213 </p>
214 ''' % (lead+' ' if lead else '', href, title))
215
216    if level == 0:
217       new_text.append("""
218 <hr>
219
220 <p class="head">Новостевая лента в форматах
221 <img src="../../Graphics/atom_10.jpg" border=0>
222 <A HREF="atom_10_titles.xml">Atom 1.0 только заголовки</A> /
223 <A HREF="atom_10.xml">Atom 1.0</A> /
224 <A HREF="atom_10_full.xml">Atom 1.0 полные тексты</A>
225 и <img src="../../Graphics/rss_20.jpg" border=0>
226 <A HREF="rss_20_titles.xml">RSS 2.0 только заголовки</A> /
227 <A HREF="rss_20.xml">RSS 2.0</A> /
228 <A HREF="rss_20_full.xml">RSS 2.0 полные тексты</A>.
229 </p>
230 """)
231
232       years = {}
233       for year, month, day, file, title, lead in save_titles:
234          years[year] = True
235       new_text.append('''
236 <p class="head"><a href="tags/">Теги</a>:
237 ''')
238       first_tag = True
239       for count, tag, links in all_tags:
240          if first_tag:
241             first_tag = False
242          else:
243             new_text.append(' - ')
244          new_text.append("""<a href="tags/%s.html">%s (%d)</a>""" % (
245              encode_tag(tag), tag, count))
246       new_text.append('''
247 </p>
248 ''')
249
250       max_year = int(sorted(years.keys())[-1])
251       years = range(max_year, 2005, -1)
252
253       new_text.append('''
254 <p class="head">По годам:
255 ''')
256
257       year_counts = {}
258       for year, month, day, file, title, lead in all_titles:
259          year_counts[year] = 0
260       for year, month, day, file, title, lead in all_titles:
261          year_counts[year] += 1
262
263       first_year = True
264       for year in years:
265          if first_year:
266             first_year = False
267          else:
268             new_text.append(' - ')
269          new_text.append('<a href="%s/">%s (%d)</a>' % (year, year, year_counts[str(year)]))
270       new_text.append('''
271 </p>
272 ''')
273
274       new_text.append("""
275 <hr>
276 <p class="head"><a href="http://phd.livejournal.com/">ЖЖ</a>
277 """)
278
279    new_text.append("""\
280 #end def
281 $phd_site.respond(self)
282 """)
283
284    write_if_changed(index_name, ''.join(new_text))
285
286
287 all_tags = {}
288 all_titles = []
289 all_titles_tags = []
290
291 for year in sorted(years.keys()):
292    year_titles = []
293    months = years[year]
294    for month in sorted(months.keys()):
295       month_titles = []
296       for day in sorted(months[month]):
297          day_titles = []
298          key = year, month, day
299          if key in blog:
300             for file, title, lead, tags in blog[key]:
301                if file.endswith(".tmpl"): file = file[:-len("tmpl")] + "html"
302                value = (year, month, day, file, title, lead)
303                all_titles_tags.append((year, month, day, file, title, lead, tags))
304                all_titles.append(value)
305                year_titles.append(value)
306                month_titles.append(value)
307                day_titles.append(value)
308                for tag in tags:
309                   if tag in all_tags:
310                      tag_links = all_tags[tag]
311                   else:
312                      tag_links = all_tags[tag] = []
313                   tag_links.append(value)
314          write_template(3, year, month, day, day_titles)
315       write_template(2, year, month, day, month_titles)
316    write_template(1, year, month, day, year_titles)
317
318 def by_count_rev_tag_link(t1, t2):
319    """Sort all_tags by count in descending order,
320    and by tags and links in ascending order
321    """
322    r = cmp(t1[0], t2[0])
323    if r:
324       return -r
325    return cmp((t1[1], t1[2]), (t2[1], t2[2]))
326
327 all_tags = [(len(links), tag, links) for (tag, links) in all_tags.items()]
328 all_tags.sort(by_count_rev_tag_link)
329
330 write_template(0, year, month, day, all_titles[-20:], all_tags)
331
332 new_text = ["""\
333 ## THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT.
334 #encoding koi8-r
335 #extends phd_site
336 #implements respond
337 #attr $Title = "Oleg Broytman's blog: tags"
338 #attr $Description = "Broytman Russian Blog Tags Index Document"
339 #attr $Copyright = 2006
340 ##
341 #def body_html
342 <h1>Теги</h1>
343
344 <p class="head small">
345 Форма поиска позволяет искать сообщения в блоге, соответствующие выражению.
346 Синтаксис выражения:</p>
347 <ul class="small">
348     <li>Тег - если такой тег существует, произойдёт перенаправление на страницу тега.</li>
349     <li>Оператор '!' (NOT, not) - ищет записи, в которых нет этого тега.</li>
350     <li>Оператор '&amp;' (AND, and) - ищет записи, в которых есть оба тега.</li>
351     <li>Оператор '|' (OR, or) - ищет записи, в которых есть любые из тегов.</li>
352     <li>Приоритет операций стандартный: NOT &gt; AND &gt; OR. Скобки '()' позволяют объединять выражения.</li>
353 </ul>
354 <p class="small">
355 Примеры выражений: linux - произойдёт перенаправление
356 на страницу linux.html; linux&amp;!audio - искать записи в которых есть тег
357 linux и нет тега audio; linux and not audio - то же самое.
358 </p>
359
360 <center>
361 <form method=GET action="../../../cgi-bin/blog-ru/search-tags/search-tags.py">
362     <input type=text name=q>
363     <input type=submit name=submit value="Искать">
364 </form>
365 </center>
366
367 <dl>
368 """]
369
370 for i, (count, tag, links) in enumerate(all_tags):
371    new_text.append("""\
372    <dt><a href="%s.html">%s (%d)</a></dt>
373 """ % (encode_tag(tag), tag, count))
374
375    first = all_tags[0][1]
376    if i == 0:
377       prev = None
378    else:
379       prev = all_tags[i-1][1]
380    if i >= len(all_tags)-1:
381       next = None
382    else:
383       next = all_tags[i+1][1]
384    last = all_tags[-1][1]
385
386    tag_text = ["""\
387 ## THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT.
388 #encoding koi8-r
389 #extends phd_site
390 #implements respond
391 #attr $Title = "Oleg Broytman's blog: tag %s"
392 #attr $Description = "Broytman Russian Blog Tag %s Index Document"
393 """ % (tag, tag)]
394
395    tag_text.append("""\
396 #attr $First = "%s"
397 """ % first)
398
399    if prev:
400       tag_text.append("""\
401 #attr $Prev = "%s"
402 """ % prev)
403
404    if next:
405       tag_text.append("""\
406 #attr $Next = "%s"
407 """ % next)
408
409    tag_text.append("""\
410 #attr $Last = "%s"
411 """ % last)
412
413    tag_text.append("""\
414 #attr $Copyright = 2006
415 ##
416 #def body_html
417 <h1>%s</h1>
418
419 <ul>
420 """ % tag)
421
422    count = 0
423    for year, month, day, filename, title, lead in reversed(links):
424       link = "../%s/%s/%s/%s" % (year, month, day, filename)
425       item_text = """<li><a href="%s">%s/%s/%s: %s%s</a></li>""" % (link, year, month, day, lead+' ' if lead else '', title)
426
427       count += 1
428       if count <= 5:
429          new_text.append("      <dd>%s</dd>\n" % item_text)
430
431       tag_text.append("   %s\n" % item_text)
432
433    tag_text.append("""\
434 </ul>
435 #end def
436 $phd_site.respond(self)
437 """)
438    write_if_changed(os.path.join(blog_root, "tags",
439                                  tag.replace(' ', '_') + ".tmpl"),
440                     ''.join(tag_text))
441
442 new_text.append("""\
443 </dl>
444 #end def
445 $phd_site.respond(self)
446 """)
447 write_if_changed(os.path.join(blog_root, "tags", "index.tmpl"), ''.join(new_text))
448
449
450 from HTMLParser import HTMLParseError
451 import cgi
452 from urlparse import urljoin
453 from m_lib.net.www.html import HTMLParser as _HTMLParser
454
455 class HTMLDone(Exception): pass
456
457
458 class FirstPHTMLParser(_HTMLParser):
459    def __init__(self):
460       _HTMLParser.__init__(self)
461       self.first_p = None
462
463    def start_p(self, attrs):
464       self.accumulator = '<p>'
465
466    def end_p(self):
467       self.first_p = self.accumulator + '</p>'
468       raise HTMLDone()
469
470 def get_first_p(body):
471    parser = FirstPHTMLParser()
472
473    try:
474       parser.feed(body)
475    except (HTMLParseError, HTMLDone):
476       pass
477
478    try:
479       parser.close()
480    except (HTMLParseError, HTMLDone):
481       pass
482
483    return parser.first_p
484
485
486 class AbsURLHTMLParser(_HTMLParser):
487    def __init__(self, base):
488       _HTMLParser.__init__(self)
489       self.base = base
490
491    def start_a(self, attrs):
492       self.accumulator += '<a'
493       for attrname, value in attrs:
494          value = cgi.escape(value, True)
495          if isinstance(value, unicode):
496             value = value.encode('koi8-r')
497          if attrname == 'href':
498             self.accumulator += ' href="%s"' % urljoin(self.base, value)
499          else:
500             self.accumulator += ' %s="%s"' % (attrname, value)
501       self.accumulator += '>'
502
503    def end_a(self):
504       self.accumulator += '</a>'
505
506    def start_img(self, attrs):
507       self.accumulator += '<img'
508       for attrname, value in attrs:
509          value = cgi.escape(value, True)
510          if attrname == 'src':
511             self.accumulator += ' src="%s"' % urljoin(self.base, value)
512          else:
513             self.accumulator += ' %s="%s"' % (attrname, value)
514       self.accumulator += '>'
515
516    def end_img(self):
517        pass
518
519 def absolute_urls(body, base):
520    parser = AbsURLHTMLParser(base)
521
522    try:
523       parser.feed(body)
524    except HTMLParseError:
525       pass
526
527    try:
528       parser.close()
529    except HTMLParseError:
530       pass
531
532    return parser.accumulator
533
534
535 from atom_10 import atom_10
536 from rss_20 import rss_20
537 from news import NewsItem
538
539 if blog_root:
540    blog_root_url = blog_root[
541         blog_root.find('/htdocs/phdru.name/') + len('/htdocs/phdru.name/'):]
542    baseURL = "https://phdru.name/%s/" % blog_root_url
543 else:
544    baseURL = "https://phdru.name/"
545
546 items = []
547 for item in tuple(reversed(all_titles_tags))[:10]:
548    year, month, day, file, title, lead, tags = item
549    lead = lead.decode('koi8-r').encode('utf-8')
550    title = title.decode('koi8-r').encode('utf-8')
551    url_path = "%s/%s/%s/%s" % (year, month, day, file)
552    item = NewsItem(
553       "%s-%s-%s" % (year, month, day),
554       "%s%s" % (lead+' ' if lead else '', title),
555       url_path)
556    items.append(item)
557    item.baseURL = baseURL
558    item.categoryList = [t.decode('koi8-r').encode('utf-8') for t in tags]
559    body = bodies[(year, month, day, file)]
560    body = absolute_urls(body, baseURL + url_path)
561    try:
562        body.decode('utf-8')
563    except UnicodeDecodeError:
564        body = body.decode('koi8-r').encode('utf-8')
565    item.body = body
566    excerpt = get_first_p(body)
567    try:
568        excerpt.decode('utf-8')
569    except UnicodeDecodeError:
570        excerpt = excerpt.decode('koi8-r').encode('utf-8')
571    item.excerpt = excerpt
572
573 namespace = {
574    "title": "Oleg Broytman's blog",
575    "baseURL": baseURL,
576    "indexFile": "",
577    "description": "",
578    "lang": "ru",
579    "author": "Oleg Broytman",
580    "email": "phd@phdru.name",
581    "generator": os.path.basename(sys.argv[0]),
582    "posts": items,
583 }
584
585 # For english dates
586 locale.setlocale(locale.LC_TIME, 'C')
587
588 atom_tmpl = unicode(atom_10(searchList=[namespace])).encode('koi8-r')
589 write_if_changed(os.path.join(blog_root, "atom_10.xml"), atom_tmpl)
590 rss_tmpl = unicode(rss_20(searchList=[namespace])).encode('koi8-r')
591 write_if_changed(os.path.join(blog_root, "rss_20.xml"), rss_tmpl)
592
593 for item in items:
594     item.excerpt = None
595
596 atom_tmpl = unicode(atom_10(searchList=[namespace])).encode('koi8-r')
597 write_if_changed(os.path.join(blog_root, "atom_10_titles.xml"), atom_tmpl)
598 rss_tmpl = unicode(rss_20(searchList=[namespace])).encode('koi8-r')
599 write_if_changed(os.path.join(blog_root, "rss_20_titles.xml"), rss_tmpl)
600
601 for item in items:
602     item.content = item.body
603
604 atom_tmpl = unicode(atom_10(searchList=[namespace])).encode('koi8-r')
605 write_if_changed(os.path.join(blog_root, "atom_10_full.xml"), atom_tmpl)
606 rss_tmpl = unicode(rss_20(searchList=[namespace])).encode('koi8-r')
607 write_if_changed(os.path.join(blog_root, "rss_20_full.xml"), rss_tmpl)