My static website generator using poole https://www.xythobuz.de
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. # -*- coding: utf-8 -*-
  2. from __future__ import print_function
  3. import sys
  4. import re
  5. import itertools
  6. import email.utils
  7. import os.path
  8. import time
  9. import codecs
  10. from datetime import datetime
  11. # -----------------------------------------------------------------------------
  12. # Python 2/3 hacks
  13. # -----------------------------------------------------------------------------
  14. PY3 = sys.version_info[0] == 3
  15. if PY3:
  16. import urllib
  17. import urllib.request
  18. def urlparse_foo(link):
  19. return urllib.parse.parse_qs(urllib.parse.urlparse(link).query)['v'][0]
  20. else:
  21. import urllib
  22. import urlparse
  23. def urlparse_foo(link):
  24. return urlparse.parse_qs(urlparse.urlparse(link).query)['v'][0]
  25. # -----------------------------------------------------------------------------
  26. # config "system"
  27. # -----------------------------------------------------------------------------
  28. conf = {
  29. "default_lang": "en",
  30. "base_url": "https://www.xythobuz.de",
  31. "birthday": datetime(1994, 1, 22, 0, 0),
  32. "blog_years_back": 6,
  33. }
  34. def get_conf(name):
  35. return conf[name]
  36. # -----------------------------------------------------------------------------
  37. # local vars for compatibility
  38. # -----------------------------------------------------------------------------
  39. DEFAULT_LANG = get_conf("default_lang")
  40. BASE_URL = get_conf("base_url")
  41. # -----------------------------------------------------------------------------
  42. # birthday calculation
  43. # -----------------------------------------------------------------------------
  44. from datetime import timedelta
  45. from calendar import isleap
  46. size_of_day = 1. / 366.
  47. size_of_second = size_of_day / (24. * 60. * 60.)
  48. def date_as_float(dt):
  49. days_from_jan1 = dt - datetime(dt.year, 1, 1)
  50. if not isleap(dt.year) and days_from_jan1.days >= 31+28:
  51. days_from_jan1 += timedelta(1)
  52. return dt.year + days_from_jan1.days * size_of_day + days_from_jan1.seconds * size_of_second
  53. def difference_in_years(start_date, end_date):
  54. return int(date_as_float(end_date) - date_as_float(start_date))
  55. def own_age():
  56. return difference_in_years(get_conf("birthday"), datetime.now())
  57. # -----------------------------------------------------------------------------
  58. # sub page helper macro
  59. # -----------------------------------------------------------------------------
  60. def backToParent():
  61. # check for special parent cases
  62. posts = []
  63. if page.get("show_in_quadcopters", "false") == "true":
  64. posts = [p for p in pages if p.url == "quadcopters.html"]
  65. # if not, check for actual parent
  66. if len(posts) == 0:
  67. url = page.get("parent", "") + ".html"
  68. posts = [p for p in pages if p.url == url]
  69. # print if any parent link found
  70. if len(posts) > 0:
  71. p = posts[0]
  72. print('<span class="listdesc">[...back to ' + p.title + ' overview](' + p.url + ')</span>')
  73. # -----------------------------------------------------------------------------
  74. # table helper macro
  75. # -----------------------------------------------------------------------------
  76. def tableHelper(style, header, content):
  77. print("<table>")
  78. if (header != None) and (len(header) == len(style)):
  79. print("<tr>")
  80. for h in header:
  81. print("<th>" + h + "</th>")
  82. print("</tr>")
  83. for ci in range(0, len(content)):
  84. if len(content[ci]) != len(style):
  85. # invalid call of table helper!
  86. continue
  87. print("<tr>")
  88. for i in range(0, len(style)):
  89. s = style[i]
  90. td_style = ""
  91. if "monospaced" in s:
  92. td_style += " font-family: monospace;"
  93. if "align-last-right" in s:
  94. if ci == (len(content) - 1):
  95. td_style += " text-align: right;"
  96. else:
  97. if "align-center" in s:
  98. td_style += " text-align: center;"
  99. elif "align-right" in s:
  100. td_style += " text-align: right;"
  101. elif "align-center" in s:
  102. td_style += " text-align: center;"
  103. td_args = ""
  104. if td_style != "":
  105. td_args = " style=\"" + td_style + "\""
  106. print("<td" + td_args + ">")
  107. if isinstance(content[ci][i], tuple):
  108. text, link = content[ci][i]
  109. print("<a href=\"" + link + "\">" + text + "</a>")
  110. else:
  111. text = content[ci][i]
  112. print(text)
  113. print("</td>")
  114. print("</tr>")
  115. print("</table>")
  116. # -----------------------------------------------------------------------------
  117. # menu helper macro
  118. # -----------------------------------------------------------------------------
  119. def githubCommitBadge(p, showInline = False):
  120. ret = ""
  121. if p.get("github", "") != "":
  122. link = p.get("git", p.github)
  123. linkParts = p.github.split("/")
  124. if len(linkParts) >= 5:
  125. ret += "<a href=\"" + link + "\"><img "
  126. if showInline:
  127. ret += "style =\"vertical-align: middle; padding-bottom: 0.25em;\" "
  128. ret += "src=\"https://img.shields.io/github/last-commit/"
  129. ret += linkParts[3] + "/" + linkParts[4]
  130. ret += ".svg?logo=git&style=flat\" /></a>"
  131. return ret
  132. def printMenuItem(p, yearsAsHeading = False, showDateSpan = False, showOnlyStartDate = False, nicelyFormatFullDate = False, lastyear = "0", lang = "", showLastCommit = True, hide_description = False, indent_count = 0, updates_as_heading = False):
  133. title = p.title
  134. if lang != "":
  135. if p.get("title_" + lang, "") != "":
  136. title = p.get("title_" + lang, "")
  137. if title == "Blog":
  138. title = p.post
  139. if updates_as_heading:
  140. year = p.get("update", p.get("date", ""))[0:4]
  141. else:
  142. year = p.get("date", "")[0:4]
  143. if year != lastyear:
  144. lastyear = year
  145. if yearsAsHeading:
  146. print("\n\n#### %s\n" % (year))
  147. dateto = ""
  148. if p.get("date", "" != ""):
  149. year = p.get("date", "")[0:4]
  150. if showOnlyStartDate:
  151. dateto = " (%s)" % (year)
  152. if p.get("update", "") != "" and p.get("update", "")[0:4] != year:
  153. if showDateSpan:
  154. dateto = " (%s - %s)" % (year, p.get("update", "")[0:4])
  155. if nicelyFormatFullDate:
  156. dateto = " - " + datetime.strptime(p.get("update", p.date), "%Y-%m-%d").strftime("%B %d, %Y")
  157. indent = " " * (indent_count + 1)
  158. print(indent + "* **[%s](%s)**%s" % (title, p.url, dateto))
  159. if hide_description == False:
  160. if p.get("description", "") != "":
  161. description = p.get("description", "")
  162. if lang != "":
  163. if p.get("description_" + lang, "") != "":
  164. description = p.get("description_" + lang, "")
  165. print("<br><span class=\"listdesc\">" + description + "</span>")
  166. if showLastCommit:
  167. link = githubCommitBadge(p)
  168. if len(link) > 0:
  169. print("<br>" + link)
  170. return lastyear
  171. def printRecentMenu(count = 5):
  172. posts = [p for p in pages if "date" in p and p.lang == "en"]
  173. posts.sort(key=lambda p: p.get("update", p.get("date")), reverse=True)
  174. if count > 0:
  175. posts = posts[0:count]
  176. lastyear = "0"
  177. for p in posts:
  178. lastyear = printMenuItem(p, count == 0, False, False, True, lastyear, "", False, False, 0, True)
  179. def printBlogMenu(year_min=None, year_max=None):
  180. posts = [p for p in pages if "post" in p and p.lang == "en"]
  181. posts.sort(key=lambda p: p.get("date", "9999-01-01"), reverse=True)
  182. if year_min != None:
  183. posts = [p for p in posts if int(p.get("date", "9999-01-01")[0:4]) >= int(year_min)]
  184. if year_max != None:
  185. posts = [p for p in posts if int(p.get("date", "9999-01-01")[0:4]) <= int(year_max)]
  186. lastyear = "0"
  187. for p in posts:
  188. lastyear = printMenuItem(p, True, False, False, True, lastyear)
  189. def printProjectsMenu():
  190. # prints all pages with parent 'projects' or 'stuff'.
  191. # first the ones without date, sorted by position.
  192. # this first section includes sub-headings for children
  193. # then afterwards those with date, split by year.
  194. # also supports blog posts with parent.
  195. enpages = [p for p in pages if p.lang == "en"]
  196. # select pages without date
  197. dpages = [p for p in enpages if p.get("date", "") == ""]
  198. # only those that have a parent in ['projects', 'stuff']
  199. mpages = [p for p in dpages if any(x in p.get("parent", "") for x in [ 'projects', 'stuff' ])]
  200. # sort by position
  201. mpages.sort(key=lambda p: [int(p.get("position", "999"))])
  202. # print all pages
  203. for p in mpages:
  204. printMenuItem(p)
  205. # print subpages for these top-level items
  206. subpages = [sub for sub in enpages if sub.get("parent", "none") == p.get("child-id", "unknown")]
  207. for sp in subpages:
  208. printMenuItem(sp, False, True, True, False, "0", "", False, True, 1)
  209. # slect pages with a date
  210. dpages = [p for p in enpages if p.get("date", "") != ""]
  211. # only those that have a parent in ['projects', 'stuff']
  212. mpages = [p for p in dpages if any(x in p.get("parent", "") for x in [ 'projects', 'stuff' ])]
  213. # sort by date
  214. mpages.sort(key=lambda p: [p.get("date", "9999-01-01")], reverse = True)
  215. # print all pages
  216. lastyear = "0"
  217. for p in mpages:
  218. lastyear = printMenuItem(p, True, True, False, False, lastyear)
  219. # print subpages for these top-level items
  220. subpages = [sub for sub in enpages if sub.get("parent", "none") == p.get("child-id", "unknown")]
  221. subpages.sort(key=lambda p: [p.get("date", "9999-01-01")], reverse = True)
  222. for sp in subpages:
  223. printMenuItem(sp, False, True, True, False, "0", "", False, True, 1)
  224. def print3DPrintingMenu():
  225. mpages = [p for p in pages if p.get("parent", "") == "3d-printing" and p.lang == "en"]
  226. mpages.sort(key=lambda p: int(p["position"]))
  227. for p in mpages:
  228. printMenuItem(p, False, True, True)
  229. def printInputDevicesMenu():
  230. mpages = [p for p in pages if p.get("parent", "") == "input_devices" and p.lang == "en"]
  231. mpages.sort(key=lambda p: [p.get("date", "9999-01-01")], reverse = True)
  232. for p in mpages:
  233. printMenuItem(p, False, True, True)
  234. def printInputDevicesRelatedMenu():
  235. mpages = [p for p in pages if p.get("show_in_input_devices", "false") == "true"]
  236. mpages.sort(key=lambda p: [p.get("date", "9999-01-01")], reverse = True)
  237. for p in mpages:
  238. printMenuItem(p, False, True, True)
  239. def printSmarthomeMenu():
  240. mpages = [p for p in pages if p.get("parent", "") == "smarthome" and p.lang == "en"]
  241. mpages.sort(key=lambda p: int(p["position"]))
  242. for p in mpages:
  243. printMenuItem(p, False, True, True)
  244. def printQuadcopterMenu():
  245. mpages = [p for p in pages if p.get("parent", "") == "quadcopters" and p.lang == "en"]
  246. mpages.sort(key=lambda p: int(p["position"]))
  247. for p in mpages:
  248. printMenuItem(p, False, True, True)
  249. def printQuadcopterRelatedMenu():
  250. mpages = [p for p in pages if p.get("show_in_quadcopters", "false") == "true"]
  251. mpages.sort(key=lambda p: [p.get("date", "9999-01-01")], reverse = True)
  252. for p in mpages:
  253. printMenuItem(p, False, True, True)
  254. def printRobotMenuEnglish():
  255. mpages = [p for p in pages if p.get("parent", "") == "xyrobot" and p.lang == "en"]
  256. mpages.sort(key=lambda p: int(p["position"]))
  257. for p in mpages:
  258. printMenuItem(p)
  259. def printRobotMenuDeutsch():
  260. mpages = [p for p in pages if p.get("parent", "") == "xyrobot" and p.lang == "de"]
  261. mpages.sort(key=lambda p: int(p["position"]))
  262. for p in mpages:
  263. printMenuItem(p, False, False, False, False, "0", "de")
  264. def printSteamMenuEnglish():
  265. mpages = [p for p in pages if p.get("parent", "") == "steam" and p.lang == "en"]
  266. mpages.sort(key=lambda p: [p.get("date", "9999-01-01")], reverse = True)
  267. for p in mpages:
  268. printMenuItem(p, False, False, False, True)
  269. def printSteamMenuDeutsch():
  270. # TODO show german pages, or english pages when german not available
  271. printSteamMenuEnglish()
  272. # -----------------------------------------------------------------------------
  273. # lightgallery helper macro
  274. # -----------------------------------------------------------------------------
  275. # call this macro like this:
  276. # lightgallery([
  277. # [ "image-link", "description" ],
  278. # [ "image-link", "thumbnail-link", "description" ],
  279. # [ "youtube-link", "thumbnail-link", "description" ],
  280. # [ "video-link", "mime", "thumbnail-link", "image-link", "description" ],
  281. # [ "video-link", "mime", "", "", "description" ],
  282. # ])
  283. # it will also auto-generate thumbnails and resize and strip EXIF from images
  284. # using the included web-image-resize script.
  285. # and it can generate video thumbnails and posters with the video-thumb script.
  286. def lightgallery_check_thumbnail(link, thumb):
  287. # only check local image links
  288. if not link.startswith('img/'):
  289. return
  290. # generate thumbnail filename web-image-resize will create
  291. x = link.rfind('.')
  292. img = link[:x] + '_small' + link[x:]
  293. # only run when desired thumb path matches calculated ones
  294. if thumb != img:
  295. return
  296. # generate fs path to images
  297. path = os.path.join(os.getcwd(), 'static', link)
  298. img = os.path.join(os.getcwd(), 'static', thumb)
  299. # no need to generate thumb again
  300. if os.path.exists(img):
  301. return
  302. # run web-image-resize to generate thumbnail
  303. script = os.path.join(os.getcwd(), 'web-image-resize')
  304. os.system(script + ' ' + path)
  305. def lightgallery_check_thumbnail_video(link, thumb, poster):
  306. # only check local image links
  307. if not link.startswith('img/'):
  308. return
  309. # generate thumbnail filenames video-thumb will create
  310. x = link.rfind('.')
  311. thumb_l = link[:x] + '_thumb.png'
  312. poster_l = link[:x] + '_poster.png'
  313. # only run when desired thumb path matches calculated ones
  314. if (thumb_l != thumb) or (poster_l != poster):
  315. return
  316. # generate fs path to images
  317. path = os.path.join(os.getcwd(), 'static', link)
  318. thumb_p = os.path.join(os.getcwd(), 'static', thumb)
  319. poster_p = os.path.join(os.getcwd(), 'static', poster)
  320. # no need to generate thumb again
  321. if os.path.exists(thumb_p) or os.path.exists(poster_p):
  322. return
  323. # run video-thumb to generate thumbnail
  324. script = os.path.join(os.getcwd(), 'video-thumb')
  325. os.system(script + ' ' + path)
  326. def lightgallery(links):
  327. global v_ii
  328. try:
  329. v_ii += 1
  330. except NameError:
  331. v_ii = 0
  332. videos = [l for l in links if len(l) == 5]
  333. v_i = -1
  334. for v in videos:
  335. link, mime, thumb, poster, alt = v
  336. v_i += 1
  337. print('<div style="display:none;" id="video' + str(v_i) + '_' + str(v_ii) + '">')
  338. print('<video class="lg-video-object lg-html5" controls preload="none">')
  339. print('<source src="' + link + '" type="' + mime + '">')
  340. print('<a href="' + link + '">' + alt + '</a>')
  341. print('</video>')
  342. print('</div>')
  343. print('<div class="lightgallery">')
  344. v_i = -1
  345. for l in links:
  346. if (len(l) == 3) or (len(l) == 2):
  347. link = img = alt = ""
  348. style = img2 = ""
  349. if len(l) == 3:
  350. link, img, alt = l
  351. else:
  352. link, alt = l
  353. if "youtube.com" in link:
  354. img = "https://img.youtube.com/vi/"
  355. img += urlparse_foo(link)
  356. img += "/0.jpg" # full size preview
  357. #img += "/default.jpg" # default thumbnail
  358. style = ' style="width:300px;"'
  359. img2 = '<img src="lg/video-play.png" class="picthumb">'
  360. else:
  361. x = link.rfind('.')
  362. img = link[:x] + '_small' + link[x:]
  363. lightgallery_check_thumbnail(link, img)
  364. print('<div class="border" style="position:relative;" data-src="' + link + '"><a href="' + link + '"><img class="pic" src="' + img + '" alt="' + alt + '"' + style + '>' + img2 + '</a></div>')
  365. elif len(l) == 5:
  366. v_i += 1
  367. link, mime, thumb, poster, alt = videos[v_i]
  368. if len(thumb) <= 0:
  369. x = link.rfind('.')
  370. thumb = link[:x] + '_thumb.png'
  371. if len(poster) <= 0:
  372. x = link.rfind('.')
  373. poster = link[:x] + '_poster.png'
  374. lightgallery_check_thumbnail_video(link, thumb, poster)
  375. print('<div class="border" data-poster="' + poster + '" data-sub-html="' + alt + '" data-html="#video' + str(v_i) + '_' + str(v_ii) + '"><a href="' + link + '"><img class="pic" src="' + thumb + '"></a></div>')
  376. else:
  377. raise NameError('Invalid number of arguments for lightgallery')
  378. print('</div>')
  379. # -----------------------------------------------------------------------------
  380. # github helper macros
  381. # -----------------------------------------------------------------------------
  382. import json, sys
  383. def restRequest(url):
  384. response = urllib.request.urlopen(url) if PY3 else urllib.urlopen(url)
  385. if response.getcode() != 200:
  386. sys.stderr.write("\n")
  387. sys.stderr.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")
  388. sys.stderr.write("!!!!!!! WARNING !!!!!\n")
  389. sys.stderr.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")
  390. sys.stderr.write("invalid response code: " + str(response.getcode()) + "\n")
  391. sys.stderr.write("url: \"" + url + "\"\n")
  392. sys.stderr.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")
  393. sys.stderr.write("!!!!!!! WARNING !!!!!\n")
  394. sys.stderr.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")
  395. sys.stderr.write("\n")
  396. return ""
  397. data = json.loads(response.read().decode("utf-8"))
  398. return data
  399. def restReleases(user, repo):
  400. s = "https://api.github.com/repos/"
  401. s += user
  402. s += "/"
  403. s += repo
  404. s += "/releases"
  405. return restRequest(s)
  406. def printLatestRelease(user, repo):
  407. repo_url = "https://github.com/" + user + "/" + repo
  408. print("<div class=\"releasecard\">")
  409. print("Release builds for " + repo + " are <a href=\"" + repo_url + "/releases\">available on GitHub</a>.<br>\n")
  410. releases = restReleases(user, repo)
  411. if len(releases) <= 0:
  412. print("No release has been published on GitHub yet.")
  413. print("</div>")
  414. return
  415. releases.sort(key=lambda x: x["published_at"], reverse=True)
  416. r = releases[0]
  417. release_url = r["html_url"]
  418. print("Latest release of <a href=\"" + repo_url + "\">" + repo + "</a>, at the time of this writing: <a href=\"" + release_url + "\">" + r["name"] + "</a> (" + datetime.strptime(r["published_at"], "%Y-%m-%dT%H:%M:%SZ").strftime("%Y-%m-%d %H:%M:%S") + ")\n")
  419. if len(r["assets"]) <= 0:
  420. print("<br>No release assets have been published on GitHub for that.")
  421. print("</div>")
  422. return
  423. print("<ul>")
  424. print("Release Assets:")
  425. for a in r["assets"]:
  426. size = int(a["size"])
  427. ss = " "
  428. if size >= (1024 * 1024):
  429. ss += "(%.1f MiB)" % (size / (1024.0 * 1024.0))
  430. elif size >= 1024:
  431. ss += "(%d KiB)" % (size // 1024)
  432. else:
  433. ss += "(%d Byte)" % (size)
  434. print("<li><a href=\"" + a["browser_download_url"] + "\">" + a["name"] + "</a>" + ss)
  435. print("</ul></div>")
  436. def include_url(url):
  437. response = urllib.request.urlopen(url) if PY3 else urllib.urlopen(url)
  438. if response.getcode() != 200:
  439. raise Exception("invalid response code", response.getcode())
  440. data = response.read().decode("utf-8")
  441. print(data, end="")
  442. # -----------------------------------------------------------------------------
  443. # preconvert hooks
  444. # -----------------------------------------------------------------------------
  445. # -----------------------------------------------------------------------------
  446. # multi language support
  447. # -----------------------------------------------------------------------------
  448. def hook_preconvert_anotherlang():
  449. MKD_PATT = r'\.(?:md|mkd|mdown|markdown)$'
  450. _re_lang = re.compile(r'^[\s+]?lang[\s+]?[:=]((?:.|\n )*)', re.MULTILINE)
  451. vpages = [] # Set of all virtual pages
  452. for p in pages:
  453. current_lang = DEFAULT_LANG # Default language
  454. langs = [] # List of languages for the current page
  455. page_vpages = {} # Set of virtual pages for the current page
  456. text_lang = re.split(_re_lang, p.source)
  457. text_grouped = dict(zip([current_lang,] + \
  458. [lang.strip() for lang in text_lang[1::2]], \
  459. text_lang[::2]))
  460. for lang, text in (iter(text_grouped.items()) if PY3 else text_grouped.iteritems()):
  461. spath = p.fname.split(os.path.sep)
  462. langs.append(lang)
  463. if lang == "en":
  464. filename = re.sub(MKD_PATT, r"%s\g<0>" % "", p.fname).split(os.path.sep)[-1]
  465. else:
  466. filename = re.sub(MKD_PATT, r".%s\g<0>" % lang, p.fname).split(os.path.sep)[-1]
  467. vp = Page(filename, virtual=text)
  468. # Copy real page attributes to the virtual page
  469. for attr in p:
  470. if not ((attr in vp) if PY3 else vp.has_key(attr)):
  471. vp[attr] = p[attr]
  472. # Define a title in the proper language
  473. vp["title"] = p["title_%s" % lang] \
  474. if ((("title_%s" % lang) in p) if PY3 else p.has_key("title_%s" % lang)) \
  475. else p["title"]
  476. # Keep track of the current lang of the virtual page
  477. vp["lang"] = lang
  478. page_vpages[lang] = vp
  479. # Each virtual page has to know about its sister vpages
  480. for lang, vpage in (iter(page_vpages.items()) if PY3 else page_vpages.iteritems()):
  481. vpage["lang_links"] = dict([(l, v["url"]) for l, v in (iter(page_vpages.items()) if PY3 else page_vpages.iteritems())])
  482. vpage["other_lang"] = langs # set other langs and link
  483. vpages += page_vpages.values()
  484. pages[:] = vpages
  485. # -----------------------------------------------------------------------------
  486. # compatibility redirect for old website URLs
  487. # -----------------------------------------------------------------------------
  488. _COMPAT = """ case "%s":
  489. $loc = "%s/%s";
  490. break;
  491. """
  492. _COMPAT_404 = """ default:
  493. $loc = "%s";
  494. break;
  495. """
  496. def hook_preconvert_compat():
  497. fp = open(os.path.join(options.project, "output", "index.php"), 'w')
  498. fp.write("<?\n")
  499. fp.write("// Auto generated xyCMS compatibility index.php\n")
  500. fp.write("$loc = 'https://www.xythobuz.de/index.de.html';\n")
  501. fp.write("if (isset($_GET['p'])) {\n")
  502. fp.write(" if (isset($_GET['lang'])) {\n")
  503. fp.write(" $_GET['p'] .= 'EN';\n")
  504. fp.write(" }\n")
  505. fp.write(" switch($_GET['p']) {\n")
  506. for p in pages:
  507. if p.get("compat", "") != "":
  508. tmp = p["compat"]
  509. if p.get("lang", DEFAULT_LANG) == DEFAULT_LANG:
  510. tmp = tmp + "EN"
  511. fp.write(_COMPAT % (tmp, "https://www.xythobuz.de", p.url))
  512. fp.write("\n")
  513. fp.write(_COMPAT_404 % "/404.html")
  514. fp.write(" }\n")
  515. fp.write("}\n")
  516. fp.write("if ($_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.1') {\n")
  517. fp.write(" if (php_sapi_name() == 'cgi') {\n")
  518. fp.write(" header('Status: 301 Moved Permanently');\n")
  519. fp.write(" } else {\n")
  520. fp.write(" header('HTTP/1.1 301 Moved Permanently');\n")
  521. fp.write(" }\n")
  522. fp.write("}\n");
  523. fp.write("header('Location: '.$loc);\n")
  524. fp.write("?>")
  525. fp.close()
  526. # -----------------------------------------------------------------------------
  527. # sitemap generation
  528. # -----------------------------------------------------------------------------
  529. _SITEMAP = """<?xml version="1.0" encoding="UTF-8"?>
  530. <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  531. %s
  532. </urlset>
  533. """
  534. _SITEMAP_URL = """
  535. <url>
  536. <loc>%s/%s</loc>
  537. <lastmod>%s</lastmod>
  538. <changefreq>%s</changefreq>
  539. <priority>%s</priority>
  540. </url>
  541. """
  542. def hook_preconvert_sitemap():
  543. date = datetime.strftime(datetime.now(), "%Y-%m-%d")
  544. urls = []
  545. for p in pages:
  546. urls.append(_SITEMAP_URL % (BASE_URL, p.url, date, p.get("changefreq", "monthly"), p.get("priority", "0.5")))
  547. fname = os.path.join(options.project, "output", "sitemap.xml")
  548. fp = open(fname, 'w')
  549. fp.write(_SITEMAP % "".join(urls))
  550. fp.close()
  551. # -----------------------------------------------------------------------------
  552. # postconvert hooks
  553. # -----------------------------------------------------------------------------
  554. # -----------------------------------------------------------------------------
  555. # rss feed generation
  556. # -----------------------------------------------------------------------------
  557. _RSS = """<?xml version="1.0" encoding="UTF-8"?>
  558. <?xml-stylesheet href="%s" type="text/xsl"?>
  559. <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  560. <channel>
  561. <title>%s</title>
  562. <link>%s</link>
  563. <atom:link href="%s" rel="self" type="application/rss+xml" />
  564. <description>%s</description>
  565. <language>en-us</language>
  566. <pubDate>%s</pubDate>
  567. <lastBuildDate>%s</lastBuildDate>
  568. <docs>http://blogs.law.harvard.edu/tech/rss</docs>
  569. <generator>Poole</generator>
  570. <ttl>720</ttl>
  571. %s
  572. </channel>
  573. </rss>
  574. """
  575. _RSS_ITEM = """
  576. <item>
  577. <title>%s</title>
  578. <link>%s</link>
  579. <description>%s</description>
  580. <pubDate>%s</pubDate>
  581. <atom:updated>%s</atom:updated>
  582. <guid>%s</guid>
  583. </item>
  584. """
  585. def hook_postconvert_rss():
  586. items = []
  587. # all pages with "date" get put into feed
  588. posts = [p for p in pages if "date" in p]
  589. # sort by update if available, date else
  590. posts.sort(key=lambda p: p.get("update", p.date), reverse=True)
  591. # only put 20 most recent items in feed
  592. posts = posts[:20]
  593. for p in posts:
  594. title = p.title
  595. if "post" in p:
  596. title = p.post
  597. link = "%s/%s" % (BASE_URL, p.url)
  598. desc = p.html.replace("href=\"img", "%s%s%s" % ("href=\"", BASE_URL, "/img"))
  599. desc = desc.replace("src=\"img", "%s%s%s" % ("src=\"", BASE_URL, "/img"))
  600. desc = desc.replace("href=\"/img", "%s%s%s" % ("href=\"", BASE_URL, "/img"))
  601. desc = desc.replace("src=\"/img", "%s%s%s" % ("src=\"", BASE_URL, "/img"))
  602. desc = htmlspecialchars(desc)
  603. date = time.mktime(time.strptime("%s 12" % p.date, "%Y-%m-%d %H"))
  604. date = email.utils.formatdate(date)
  605. update = time.mktime(time.strptime("%s 12" % p.get("update", p.date), "%Y-%m-%d %H"))
  606. update = email.utils.formatdate(update)
  607. items.append(_RSS_ITEM % (title, link, desc, date, update, link))
  608. items = "".join(items)
  609. style = "/css/rss.xsl"
  610. title = "xythobuz.de Blog"
  611. link = "%s" % BASE_URL
  612. feed = "%s/rss.xml" % BASE_URL
  613. desc = htmlspecialchars("xythobuz Electronics & Software Projects")
  614. date = email.utils.formatdate()
  615. rss = _RSS % (style, title, link, feed, desc, date, date, items)
  616. fp = codecs.open(os.path.join(output, "rss.xml"), "w", "utf-8")
  617. fp.write(rss)
  618. fp.close()
  619. # -----------------------------------------------------------------------------
  620. # compatibility redirect for old mobile pages
  621. # -----------------------------------------------------------------------------
  622. _COMPAT_MOB = """ case "%s":
  623. $loc = "%s/%s";
  624. break;
  625. """
  626. _COMPAT_404_MOB = """ default:
  627. $loc = "%s";
  628. break;
  629. """
  630. def hook_postconvert_mobilecompat():
  631. directory = os.path.join(output, "mobile")
  632. if not os.path.exists(directory):
  633. os.makedirs(directory)
  634. fp = codecs.open(os.path.join(directory, "index.php"), "w", "utf-8")
  635. fp.write("<?\n")
  636. fp.write("// Auto generated xyCMS compatibility mobile/index.php\n")
  637. fp.write("$loc = 'https://www.xythobuz.de/index.de.html';\n")
  638. fp.write("if (isset($_GET['p'])) {\n")
  639. fp.write(" if (isset($_GET['lang'])) {\n")
  640. fp.write(" $_GET['p'] .= 'EN';\n")
  641. fp.write(" }\n")
  642. fp.write(" switch($_GET['p']) {\n")
  643. for p in pages:
  644. if p.get("compat", "") != "":
  645. tmp = p["compat"]
  646. if p.get("lang", DEFAULT_LANG) == DEFAULT_LANG:
  647. tmp = tmp + "EN"
  648. fp.write(_COMPAT_MOB % (tmp, "https://www.xythobuz.de", re.sub(".html", ".html", p.url)))
  649. fp.write("\n")
  650. fp.write(_COMPAT_404_MOB % "/404.mob.html")
  651. fp.write(" }\n")
  652. fp.write("}\n")
  653. fp.write("if ($_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.1') {\n")
  654. fp.write(" if (php_sapi_name() == 'cgi') {\n")
  655. fp.write(" header('Status: 301 Moved Permanently');\n")
  656. fp.write(" } else {\n")
  657. fp.write(" header('HTTP/1.1 301 Moved Permanently');\n")
  658. fp.write(" }\n")
  659. fp.write("}\n");
  660. fp.write("header('Location: '.$loc);\n")
  661. fp.write("?>")
  662. fp.close()
  663. # -----------------------------------------------------------------------------
  664. # displaying filesize for download links
  665. # -----------------------------------------------------------------------------
  666. def hook_postconvert_size():
  667. file_ext = '|'.join(['pdf', 'zip', 'rar', 'ods', 'odt', 'odp', 'doc', 'xls', 'ppt', 'docx', 'xlsx', 'pptx', 'exe', 'brd', 'plist'])
  668. def matched_link(matchobj):
  669. try:
  670. path = matchobj.group(1)
  671. if path.startswith("http") or path.startswith("//") or path.startswith("ftp"):
  672. return '<a href=\"%s\">%s</a>' % (matchobj.group(1), matchobj.group(3))
  673. elif path.startswith("/"):
  674. path = path.strip("/")
  675. path = os.path.join("static/", path)
  676. size = os.path.getsize(path)
  677. if size >= (1024 * 1024):
  678. return "<a href=\"%s\">%s</a>&nbsp;(%.1f MiB)" % (matchobj.group(1), matchobj.group(3), size / (1024.0 * 1024.0))
  679. elif size >= 1024:
  680. return "<a href=\"%s\">%s</a>&nbsp;(%d KiB)" % (matchobj.group(1), matchobj.group(3), size // 1024)
  681. else:
  682. return "<a href=\"%s\">%s</a>&nbsp;(%d Byte)" % (matchobj.group(1), matchobj.group(3), size)
  683. except:
  684. print("Unable to estimate file size for %s" % matchobj.group(1))
  685. return '<a href=\"%s\">%s</a>' % (matchobj.group(1), matchobj.group(3))
  686. _re_url = r'<a href=\"([^\"]*?\.(%s))\">(.*?)<\/a>' % file_ext
  687. for p in pages:
  688. p.html = re.sub(_re_url, matched_link, p.html)