- relative urls! - got rid of bleach and reacji detection, nobody is using it - removed google vision and google text classification - 410 for ^/tag and ^/comment - 80x15 SVG bottom banners - better code syntax hightlight CSS
jump to
@@ -1,22 +1,19 @@
[[source]] -name = "pypi" -url = "https://pypi.org/simple" +url = "https://pypi.python.org/simple" verify_ssl = true - -[dev-packages] +name = "pypi" [packages] +wand = "*" arrow = "*" -bleach = "*" -emoji = "*" -feedgen = "*" -langdetect = "*" +unicode-slugify = "*" requests = "*" -unicode-slugify = "*" -Jinja2 = "*" -Wand = "*" -pyyaml = "*" python-frontmatter = "*" +langdetect = "*" +jinja2 = "*" +feedgen = "*" + +[dev-packages] [requires] python_version = "3.7"
@@ -1,7 +1,7 @@
{ "_meta": { "hash": { - "sha256": "61a7889c295e0054b0526ddb11e48b5f8297c57bc7ce3196e9d49c602146e208" + "sha256": "da45b393e04bad2e1be92dde0f79ba517e3cb90e0db5f76afdc3754f638b38bf" }, "pipfile-spec": 6, "requires": {@@ -10,7 +10,7 @@ },
"sources": [ { "name": "pypi", - "url": "https://pypi.org/simple", + "url": "https://pypi.python.org/simple", "verify_ssl": true } ]@@ -18,18 +18,11 @@ },
"default": { "arrow": { "hashes": [ - "sha256:9cb4a910256ed536751cd5728673bfb53e6f0026e240466f90c2a92c0b79c895" + "sha256:3397e5448952e18e1295bf047014659effa5ae8da6a5371d37ff0ddc46fa6872", + "sha256:6f54d9f016c0b7811fac9fb8c2c7fa7421d80c54dbdd75ffb12913c55db60b8a" ], "index": "pypi", - "version": "==0.13.0" - }, - "bleach": { - "hashes": [ - "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", - "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa" - ], - "index": "pypi", - "version": "==3.1.0" + "version": "==0.13.1" }, "certifi": { "hashes": [@@ -45,14 +38,6 @@ "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
], "version": "==3.0.4" }, - "emoji": { - "hashes": [ - "sha256:1e959336dafc7a5ed2c0256ee587bbd38a7187d772141f0b5ba42de9e08599a8", - "sha256:a9e9c08be9907c0042212c86dfbea0f61f78e9897d4df41a1d6307017763ad3e" - ], - "index": "pypi", - "version": "==0.5.1" - }, "feedgen": { "hashes": [ "sha256:82c9e29884e137c3e3e7959a02f142d1f7a46cd387d572e9e40150112a27604f"@@ -84,34 +69,34 @@ "version": "==1.0.7"
}, "lxml": { "hashes": [ - "sha256:0dd6589fa75d369ba06d2b5f38dae107f76ea127f212f6a7bee134f6df2d1d21", - "sha256:1afbac344aa68c29e81ab56c1a9411c3663157b5aee5065b7fa030b398d4f7e0", - "sha256:1baad9d073692421ad5dbbd81430aba6c7f5fdc347f03537ae046ddf2c9b2297", - "sha256:1d8736421a2358becd3edf20260e41a06a0bf08a560480d3a5734a6bcbacf591", - "sha256:1e1d9bddc5afaddf0de76246d3f2152f961697ad7439c559f179002682c45801", - "sha256:1f179dc8b2643715f020f4d119d5529b02cd794c1c8f305868b73b8674d2a03f", - "sha256:241fb7bdf97cb1df1edfa8f0bcdfd80525d4023dac4523a241907c8b2f44e541", - "sha256:2f9765ee5acd3dbdcdc0d0c79309e01f7c16bc8d39b49250bf88de7b46daaf58", - "sha256:312e1e1b1c3ce0c67e0b8105317323e12807955e8186872affb667dbd67971f6", - "sha256:3273db1a8055ca70257fd3691c6d2c216544e1a70b673543e15cc077d8e9c730", - "sha256:34dfaa8c02891f9a246b17a732ca3e99c5e42802416628e740a5d1cb2f50ff49", - "sha256:3aa3f5288af349a0f3a96448ebf2e57e17332d99f4f30b02093b7948bd9f94cc", - "sha256:51102e160b9d83c1cc435162d90b8e3c8c93b28d18d87b60c56522d332d26879", - "sha256:56115fc2e2a4140e8994eb9585119a1ae9223b506826089a3ba753a62bd194a6", - "sha256:69d83de14dbe8fe51dccfd36f88bf0b40f5debeac763edf9f8325180190eba6e", - "sha256:99fdce94aeaa3ccbdfcb1e23b34273605c5853aa92ec23d84c84765178662c6c", - "sha256:a7c0cd5b8a20f3093ee4a67374ccb3b8a126743b15a4d759e2a1bf098faac2b2", - "sha256:abe12886554634ed95416a46701a917784cb2b4c77bfacac6916681d49bbf83d", - "sha256:b4f67b5183bd5f9bafaeb76ad119e977ba570d2b0e61202f534ac9b5c33b4485", - "sha256:bdd7c1658475cc1b867b36d5c4ed4bc316be8d3368abe03d348ba906a1f83b0e", - "sha256:c6f24149a19f611a415a51b9bc5f17b6c2f698e0d6b41ffb3fa9f24d35d05d73", - "sha256:d1e111b3ab98613115a208c1017f266478b0ab224a67bc8eac670fa0bad7d488", - "sha256:d6520aa965773bbab6cb7a791d5895b00d02cf9adc93ac2bf4edb9ac1a6addc5", - "sha256:dd185cde2ccad7b649593b0cda72021bc8a91667417001dbaf24cd746ecb7c11", - "sha256:de2e5b0828a9d285f909b5d2e9d43f1cf6cf21fe65bc7660bdaa1780c7b58298", - "sha256:f726444b8e909c4f41b4fde416e1071cf28fa84634bfb4befdf400933b6463af" + "sha256:0537eee4902e8bf4f41bfee8133f7edf96533dd175930a12086d6a40d62376b2", + "sha256:0562ec748abd230ab87d73384e08fa784f9b9cee89e28696087d2d22c052cc27", + "sha256:09e91831e749fbf0f24608694e4573be0ef51430229450c39c83176cc2e2d353", + "sha256:1ae4c0722fc70c0d4fba43ae33c2885f705e96dce1db41f75ae14a2d2749b428", + "sha256:1c630c083d782cbaf1f7f37f6cac87bda9cff643cf2803a5f180f30d97955cef", + "sha256:2fe74e3836bd8c0fa7467ffae05545233c7f37de1eb765cacfda15ad20c6574a", + "sha256:37af783c2667ead34a811037bda56a0b142ac8438f7ed29ae93f82ddb812fbd6", + "sha256:3f2d9eafbb0b24a33f56acd16f39fc935756524dcb3172892721c54713964c70", + "sha256:47d8365a8ef14097aa4c65730689be51851b4ade677285a3b2daa03b37893e26", + "sha256:510e904079bc56ea784677348e151e1156040dbfb736f1d8ea4b9e6d0ab2d9f4", + "sha256:58d0851da422bba31c7f652a7e9335313cf94a641aa6d73b8f3c67602f75b593", + "sha256:7940d5c2185ffb989203dacbb28e6ae88b4f1bb25d04e17f94b0edd82232bcbd", + "sha256:7cf39bb3a905579836f7a8f3a45320d9eb22f16ab0c1e112efb940ced4d057a5", + "sha256:9563a23c1456c0ab550c087833bc13fcc61013a66c6420921d5b70550ea312bf", + "sha256:95b392952935947e0786a90b75cc33388549dcb19af716b525dae65b186138fc", + "sha256:983129f3fd3cef5c3cf067adcca56e30a169656c00fcc6c648629dbb850b27fa", + "sha256:a0b75b1f1854771844c647c464533def3e0a899dd094a85d1d4ed72ecaaee93d", + "sha256:b5db89cc0ef624f3a81214b7961a99f443b8c91e88188376b6b322fd10d5b118", + "sha256:c0a7751ba1a4bfbe7831920d98cee3ce748007eab8dfda74593d44079568219a", + "sha256:c0c5a7d4aafcc30c9b6d8613a362567e32e5f5b708dc41bc3a81dac56f8af8bb", + "sha256:d4d63d85eacc6cb37b459b16061e1f100d154bee89dc8d8f9a6128a5a538e92e", + "sha256:da5e7e941d6e71c9c9a717c93725cda0708c2474f532e3680ac5e39ec57d224d", + "sha256:dccad2b3c583f036f43f80ac99ee212c2fa9a45151358d55f13004d095e683b2", + "sha256:df46307d39f2aeaafa1d25309b8a8d11738b73e9861f72d4d0a092528f498baa", + "sha256:e70b5e1cb48828ddd2818f99b1662cb9226dc6f57d07fc75485405c77da17436", + "sha256:ea825562b8cd057cbc9810d496b8b5dec37a1e2fc7b27bc7c1e72ce94462a09a" ], - "version": "==4.3.0" + "version": "==4.3.1" }, "markupsafe": { "hashes": [@@ -175,7 +160,6 @@ "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" ], - "index": "pypi", "version": "==3.13" }, "requests": {@@ -216,17 +200,10 @@ "version": "==1.24.1"
}, "wand": { "hashes": [ - "sha256:3e59e4bda9ef9d643d90e881cc950c8eee1508ec2cde1c150a1cbd5a12c1c007", - "sha256:52763dbf65d00cf98d7bc910b49329eea15896249c5555d47e169f2b6efbe166" + "sha256:7d6b8dc9d4eaccc430b9c86e6b749013220c994970a3f39e902b397e2fa732c3", + "sha256:cc0b5c9cd50fecd10dc8888b739dd5984c6f8085d2954f34903b83ca39a91236" ], "index": "pypi", - "version": "==0.5.0" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], "version": "==0.5.1" } },
@@ -13,8 +13,6 @@ import requests
import keys import settings -from pprint import pprint - EXIFDATE = re.compile( r'^(?P<year>[0-9]{4}):(?P<month>[0-9]{2}):(?P<day>[0-9]{2})\s+' r'(?P<time>[0-9]{2}:[0-9]{2}:[0-9]{2})$'@@ -69,130 +67,6 @@ data = json.loads(f.read())
for k, v in data.items(): self[k] = v -class GoogleClassifyText(CachedMeta): - def __init__(self, fpath, txt, lang='en'): - self.fpath = fpath - self.txt = txt - self.lang = lang - self._read() - - def _call_tool(self): - params = { - "document": { - "type": "PLAIN_TEXT", - "content": self.txt, - "language": self.lang, - } - } - - url = "https://language.googleapis.com/v1beta2/documents:classifyText?key=%s" % ( - keys.gcloud.get('key') - ) - logging.info( - "calling Google classifyText for %s", - self.fpath - ) - try: - r = requests.post(url, json=params) - resp = r.json() - for cat in resp.get('categories', []): - self[cat.get('name')] = cat.get('confidence') - except Exception as e: - logging.error( - 'failed to call Google Vision API on: %s, reason: %s', - self.fpath, - e - ) - -class GoogleVision(CachedMeta): - def __init__(self, fpath, imgurl): - self.fpath = fpath - self.imgurl = imgurl - self._read() - - @property - def response(self): - if 'responses' not in self: - return {} - if not len(self['responses']): - return {} - if 'labelAnnotations' not in self['responses'][0]: - return {} - return self['responses'][0] - - @property - def tags(self): - tags = [] - - if 'labelAnnotations' in self.response: - for label in self.response['labelAnnotations']: - tags.append(label['description']) - - if 'webDetection' in self.response: - if 'webEntities' in self.response['webDetection']: - for label in self.response['webDetection']['webEntities']: - tags.append(label['description']) - return tags - - @property - def landmark(self): - landmark = None - if 'landmarkAnnotations' in self.response: - if len(self.response['landmarkAnnotations']): - match = self.response['landmarkAnnotations'].pop() - landmark = { - 'name': match['description'], - 'latitude': match['locations'][0]['latLng']['latitude'], - 'longitude': match['locations'][0]['latLng']['longitude'] - } - return landmark - - @property - def onlinecopies(self): - copies = [] - if 'webDetection' in self.response: - if 'pagesWithMatchingImages' in self.response['webDetection']: - for match in self.response['webDetection']['pagesWithMatchingImages']: - copies.append(match['url']) - return copies - - def _call_tool(self): - params = { - "requests": [{ - "image": {"source": {"imageUri": self.imgurl}}, - "features": [ - { - "type": "LANDMARK_DETECTION", - }, - { - "type": "WEB_DETECTION", - }, - { - "type": "LABEL_DETECTION", - } - ] - }] - } - - url = "https://vision.googleapis.com/v1/images:annotate?key=%s" % ( - keys.gcloud.get('key') - ) - logging.info( - "calling Google Vision for %s", - self.fpath - ) - - try: - r = requests.post(url, json=params) - resp = r.json() - for k, v in resp.items(): - self[k] = v - except Exception as e: - logging.error( - 'failed to call Google Vision API on: %s, reason: %s', - self.fpath, - e - ) class Exif(CachedMeta): def __init__(self, fpath):
@@ -15,11 +15,13 @@ import asyncio
import sqlite3 import json import queue +import base64 from shutil import copy2 as cp from math import ceil from urllib.parse import urlparse from collections import OrderedDict, namedtuple import logging + import arrow import langdetect import wand.image@@ -27,16 +29,14 @@ import jinja2
import yaml import frontmatter from feedgen.feed import FeedGenerator -from bleach import clean -from emoji import UNICODE_EMOJI from slugify import slugify import requests + from pandoc import PandocMarkdown from meta import Exif import settings +from settings import struct import keys - -from pprint import pprint logger = logging.getLogger('NASG')@@ -45,8 +45,6 @@ 'MarkdownImage',
['match', 'alt', 'fname', 'title', 'css'] ) -REPLY_TYPES = ['webmention', 'in-reply-to', 'reply'] - J2 = jinja2.Environment( loader=jinja2.FileSystemLoader(searchpath=settings.paths.get('tmpl')), lstrip_blocks=True,@@ -60,7 +58,7 @@ re.IGNORECASE
) RE_CODE = re.compile( - r'^(?:[~`]{3}).+$', + r'^(?:[~`]{3,4}).+$', re.MULTILINE )@@ -68,6 +66,11 @@ RE_PRECODE = re.compile(
r'<pre class="([^"]+)"><code>' ) +def mtime(path): + if os.path.exists(path): + return int(os.path.getmtime(path)) + return 0 + def utfyamldump(data): return yaml.dump( data,@@ -83,12 +86,43 @@ only_ascii=True,
lower=True )[:limit] +J2.filters['url2slug'] = url2slug + def rfc3339todt(rfc3339): t = arrow.get(rfc3339).format('YYYY-MM-DD HH:mm ZZZ') return "%s" % (t) J2.filters['printdate'] = rfc3339todt -J2.filters['url2slug'] = url2slug + + +RE_MYURL = re.compile( + r'(^(%s[^"]+)$|"(%s[^"]+)")' % ( + settings.site.url, + settings.site.url + ) +) + +def relurl(text, baseurl=None): + if not baseurl: + baseurl = settings.site.url + for match, standalone, href in RE_MYURL.findall(text): + needsquotes = False + if len(href): + needsquotes = True + url = href + else: + url = standalone + + r = os.path.relpath(url, baseurl) + if url.endswith('/') and not r.endswith('/'): + r = "%s/index.html" % r + if needsquotes: + r = '"%s"' % r + logger.debug("RELURL: %s => %s (base: %s)", match, r, baseurl) + text = text.replace(match, r) + return text + +J2.filters['relurl'] = relurl def writepath(fpath, content, mtime=0): d = os.path.dirname(fpath)@@ -126,6 +160,7 @@ return result
class AQ: + """ Async queue which starts execution right on population """ def __init__(self): self.loop = asyncio.get_event_loop() self.queue = asyncio.Queue(loop=self.loop)@@ -145,6 +180,7 @@ self.loop.run_until_complete(consumer)
class Webmention(object): + """ outgoing webmention class """ def __init__(self, source, target, dpath, mtime=0): self.source = source self.target = target@@ -166,7 +202,7 @@ @property
def exists(self): if not os.path.isfile(self.fpath): return False - elif os.path.getmtime(self.fpath) > self.mtime: + elif mtime(self.fpath) > self.mtime: return True else: return False@@ -196,9 +232,10 @@ self.save(r.text)
class MarkdownDoc(object): + """ Base class for anything that is stored as .md """ @property def mtime(self): - return os.path.getmtime(self.fpath) + return mtime(self.fpath) @property def dt(self):@@ -234,7 +271,7 @@ @property
def content(self): return self._parsed[1] - def __pandoc(self, c): + def pandoc(self, c): if c and len(c): c = PandocMarkdown(c) c = RE_PRECODE.sub(@@ -247,7 +284,7 @@ c = "%s" % (self.content)
if hasattr(self, 'images') and len(self.images): for match, img in self.images.items(): c = c.replace(match, str(img)) - return self.__pandoc(c) + return self.pandoc(c) class Comment(MarkdownDoc):@@ -260,7 +297,7 @@ maybe = self.meta.get('date')
if maybe and 'null' != maybe: dt = arrow.get(maybe) else: - dt = arrow.get(os.path.getmtime(self.fpath)) + dt = arrow.get(mtime(self.fpath)) return dt @property@@ -295,11 +332,12 @@ return r
@property def type(self): - if len(self.content): - maybe = clean(self.content, strip=True) - if maybe in UNICODE_EMOJI: - return maybe return self.meta.get('type', 'webmention') + #if len(self.content): + #maybe = clean(self.content, strip=True) + #if maybe in UNICODE_EMOJI: + #return maybe + @cached_property def jsonld(self):@@ -322,7 +360,7 @@ """
def __init__(self, fpath): self.fpath = fpath - self.mtime = os.path.getmtime(fpath) + self.mtime = mtime(fpath) @property def source(self):@@ -472,7 +510,7 @@ @cached_property
def html_summary(self): c = self.summary if c and len(c): - c = PandocMarkdown(self.summary) + c = self.pandoc(self.summary) return c @property@@ -531,13 +569,14 @@
@property def to_ping(self): urls = [] - w = Webmention( - self.url, - 'https://fed.brid.gy/', - os.path.dirname(self.fpath), - self.dt - ) - urls.append(w) + if not self.is_page and self.is_front: + w = Webmention( + self.url, + 'https://fed.brid.gy/', + os.path.dirname(self.fpath), + self.dt + ) + urls.append(w) if self.is_reply: w = Webmention( self.url,@@ -589,6 +628,47 @@ return True
else: return False + #@cached_property + #def oembed_xml(self): + #oembed = etree.Element("oembed", version="1.0") + #xmldoc = etree.ElementTree(oembed) + #for k, v in self.oembed_json.items(): + #x = etree.SubElement(oembed, k).text = "%s" % (v) + #s = etree.tostring( + #xmldoc, + #encoding='utf-8', + #xml_declaration=True, + #pretty_print=True + #) + #return s + + #@cached_property + #def oembed_json(self): + #r = { + #"version": "1.0", + #"provider_name": settings.site.name, + #"provider_url": settings.site.url, + #"author_name": settings.author.name, + #"author_url": settings.author.url, + #"title": self.title, + #"type": "link", + #"html": self.html_content, + #} + + #img = None + #if self.is_photo: + #img = self.photo + #elif not self.is_photo and len(self.images): + #img = list(self.images.values())[0] + #if img: + #r.update({ + #"type": "rich", + #"thumbnail_url": img.jsonld.thumbnail.url, + #"thumbnail_width": img.jsonld.thumbnail.width, + #"thumbnail_height": img.jsonld.thumbnail.height + #}) + #return r + @cached_property def review(self): if 'review' not in self.meta:@@ -632,7 +712,6 @@ "name": self.title
} return r - @cached_property def jsonld(self): r = {@@ -665,11 +744,6 @@ r.update({
"@type": "Photograph", "image": self.photo.jsonld, }) - elif not self.is_photo and len(self.images): - img = list(self.images.values())[0] - r.update({ - "image": img.jsonld, - }) elif self.has_code: r.update({ "@type": "TechArticle",@@ -678,7 +752,11 @@ elif self.is_page:
r.update({ "@type": "WebPage", }) - + if not self.is_photo and len(self.images): + img = list(self.images.values())[0] + r.update({ + "image": img.jsonld, + }) if self.is_reply: r.update({@@ -708,7 +786,7 @@
for mtime in sorted(self.comments.keys()): r["comment"].append(self.comments[mtime].jsonld) - return r + return struct(r) @property def template(self):@@ -734,7 +812,7 @@ return False
elif not os.path.exists(self.renderfile): logger.debug('rendering required: no html yet') return False - elif self.dt > os.path.getmtime(self.renderfile): + elif self.dt > mtime(self.renderfile): logger.debug('rendering required: self.dt > html mtime') return False else:@@ -768,31 +846,40 @@ settings.paths.get('build'),
self.name, os.path.basename(f) ) - if os.path.exists(t) and os.path.getmtime( - f) <= os.path.getmtime(t): + if os.path.exists(t) and mtime( + f) <= mtime(t): continue logger.info("copying '%s' to '%s'", f, t) cp(f, t) - async def render(self): - if self.exists: - return - logger.info("rendering %s", self.name) - r = J2.get_template(self.template).render({ + @cached_property + def html(self): + r = J2.get_template(self.template).render({ 'baseurl': self.url, 'post': self.jsonld, 'site': settings.site, 'menu': settings.menu, 'meta': settings.meta, }) + return r + + async def render(self): + if self.exists: + return + logger.info("rendering %s", self.name) writepath( self.renderfile, - r + self.html ) + j = settings.site.copy() + j.update({ + "mainEntity": self.jsonld + }) writepath( - self.renderfile.replace('.html', '.json'), - json.dumps(self.jsonld, indent=4, ensure_ascii=False) + os.path.join(self.renderdir,'index.json'), + json.dumps(j, indent=4, ensure_ascii=False) ) + del(j) class Home(Singular):@@ -844,7 +931,7 @@ logger.debug("loading image: %s", fpath)
self.mdimg = mdimg self.fpath = fpath self.parent = parent - self.mtime = os.path.getmtime(self.fpath) + self.mtime = mtime(self.fpath) self.fname, self.fext = os.path.splitext(os.path.basename(fpath)) self.resized_images = [ (k, self.Resized(self, k))@@ -863,20 +950,20 @@ if self.fname == self.parent.name:
return True return False - @cached_property + @property def jsonld(self): r = { "@context": "http://schema.org", "@type": "ImageObject", "url": self.href, "image": self.href, - "thumbnail": { + "thumbnail": struct({ "@context": "http://schema.org", "@type": "ImageObject", "url": self.src, "width": self.displayed.width, "height": self.displayed.height, - }, + }), "name": os.path.basename(self.fpath), "encodingFormat": self.mime_type, "contentSize": self.mime_size,@@ -902,21 +989,7 @@ "license": settings.licence['_default']
}) if self.is_mainimg: r.update({"representativeOfPage": True}) - return r - - #@cached_property - #def tmplvars(self): - #return { - #'src': self.src, - #'href': self.href, - #'width': self.displayed.width, - #'height': self.displayed.height, - #'title': self.title, - #'caption': self.caption, - #'exif': self.exif, - #'is_photo': self.is_photo, - #'is_mainimg': self.is_mainimg - #} + return struct(r) def __str__(self): if len(self.mdimg.css):@@ -966,7 +1039,12 @@ return str(self.meta.get('MIMEType', 'image/jpeg'))
@property def mime_size(self): - return os.path.getsize(self.linked.fpath) + try: + size = os.path.getsize(self.linked.fpath) + except Exception as e: + logger.error('Failed to get mime size of %s', self.linked.fpath) + size = self.meta.get('FileSize', 0) + return size @property def displayed(self):@@ -1102,6 +1180,12 @@ self.size = size
self.crop = crop @property + def data(self): + with open(self.fpath, 'rb') as f: + encoded = base64.b64encode(f.read()) + return "data:%s;base64,%s" % (self.parent.mime_type, encoded.decode('utf-8')) + + @property def suffix(self): return settings.photo.get('sizes').get(self.size, '')@@ -1144,7 +1228,7 @@
@property def exists(self): if os.path.isfile(self.fpath): - if os.path.getmtime(self.fpath) >= self.parent.mtime: + if mtime(self.fpath) >= self.parent.mtime: return True return False@@ -1216,13 +1300,13 @@ if settings.args.get('force'):
return False if not os.path.exists(self.renderfile): return False - if self.mtime > os.path.getmtime(self.renderfile): + if self.mtime > mtime(self.renderfile): return False return True @property def mtime(self): - return os.path.getmtime( + return mtime( os.path.join( settings.paths.get('tmpl'), self.templatefile@@ -1238,8 +1322,8 @@ def templatefile(self):
raise ValueError('Not implemented') async def render(self): - if self.exists: - return + #if self.exists: + #return await self._render()@@ -1532,7 +1616,7 @@ if settings.args.get('force'):
return False if not os.path.exists(fpath): return False - if os.path.getmtime(fpath) >= ts: + if mtime(fpath) >= ts: return True return False@@ -1554,10 +1638,10 @@ 'feed': self.feedurl,
'title': self.title, } - def tmplvars(self, posts=[], year=False): + def tmplvars(self, posts=[], year=None): baseurl = self.url if year: - baseurl = '%s/%s/' % (baseurl, year) + baseurl = '%s%s/' % (baseurl, year) return { 'baseurl': baseurl, 'site': settings.site,@@ -1672,8 +1756,10 @@ async def render_archives(self):
for year in self.years.keys(): if year == self.newest_year: fpath = self.indexfpath() + tyear = None else: fpath = self.indexfpath("%d" % (year)) + tyear = year y = arrow.get("%d" % year, self.trange).to('utc') tsmin = y.floor('year').timestamp tsmax = y.ceil('year').timestamp@@ -1697,7 +1783,7 @@ # I don't know why end needs the +1, but without that
# some posts disappear # TODO figure this out... self.get_posts(start, end+1), - year + tyear ) ) writepath(fpath, r)@@ -1752,7 +1838,7 @@ @property
def mtime(self): r = 0 if os.path.exists(self.renderfile): - r = os.path.getmtime(self.renderfile) + r = mtime(self.renderfile) return r def append(self, post):@@ -1969,7 +2055,7 @@ for e in glob.glob(os.path.join(content, '*.*')):
if e.endswith('.md'): continue t = os.path.join(settings.paths.get('build'),os.path.basename(e)) - if os.path.exists(t) and os.path.getmtime(e) <= os.path.getmtime(t): + if os.path.exists(t) and mtime(e) <= mtime(t): continue cp(e, t)
@@ -9,6 +9,13 @@ import re
import argparse import logging + +class struct(dict): + __getattr__ = dict.get + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + base = os.path.abspath(os.path.expanduser('~/Projects/petermolnar.net')) syncserver = 'liveserver:/web/petermolnar.net'@@ -17,12 +24,12 @@ notinfeed = ['note']
flat = ['article', 'journal'] displaydate = 'YYYY-MM-DD HH:mm' -class struct(dict): - __getattr__ = dict.get - __setattr__ = dict.__setitem__ - __delattr__ = dict.__delitem__ +licence = struct({ + 'article': 'CC-BY-4.0', + 'journal': 'CC-BY-NC-4.0', + '_default': 'CC-BY-NC-ND-4.0' +}) -# full author, only for h-card generation author = struct({ "@context": "http://schema.org", "@type": "Person",@@ -39,6 +46,7 @@ "headline": "Peter Molnar",
"url": "https://petermolnar.net", "name": "petermolnar.net", "image": "https://petermolnar.net/favicon.ico", + "license": "https://spdx.org/licenses/%s.html" % (licence['_default']), "author": { "@context": "http://schema.org", "@type": "Person",@@ -127,12 +135,6 @@ 'url': '%s/category/note/' % site['url'],
'text': 'notes' } } - -licence = struct({ - 'article': 'CC-BY-4.0', - 'journal': 'CC-BY-NC-4.0', - '_default': 'CC-BY-NC-ND-4.0' -}) meta = struct({ 'webmention': 'https://webmention.io/petermolnar.net/webmention',
@@ -23,6 +23,8 @@ );
$gone_re = array( '^cache\/.*$', + '^tag\/.*$', + '^comment\/.*$', '^files\/.*$', '^wp-content\/.*$', '^broadcast\/wp-ffpc\.message$',@@ -50,12 +52,12 @@ </head>
<body> <h1><center>This content was deleted.</center></h1> <hr> -<p><center>{{ site.domain }}</center></p> +<p><center>'.$uri.'</center></p> </body> </html>'); } -function notfound() { +function notfound($uri) { header('HTTP/1.0 404 Not Found'); die('<!DOCTYPE html> <html lang="en">@@ -70,13 +72,15 @@ <h1><center>This was not found.</center></h1>
<h2><center>Please search for it instead.</center></h2> <p> <center> -<form action="/search.php" class="search-form" method="get" role="search"> - <label for="search">Search</label> - <input id="q" name="q" placeholder="search..." title="Search for:" type="search" value=""/> - <input type="submit" value="OK"/> -</form> + <form action="/search.php" class="search-form" method="get" role="search"> + <label for="search">Search</label> + <input id="q" name="q" placeholder="search..." title="Search for:" type="search" value=""/> + <input type="submit" value="OK"/> + </form> </center> </p> +<hr> +<p><center>'.$uri.'</center></p> </body> </html>'); }@@ -120,5 +124,5 @@ elseif (strstr($uri, '_')) {
maybe_redirect(str_replace('_', '-', $uri)); } else { - notfound(); + notfound($uri); }
@@ -13,7 +13,7 @@ {% if category.paginated %}
<nav> <ul> {% for y, url in category.years.items() %} - {% if y == category.year %} + {% if (y == category.year) or (not category.year and loop.first) %} <li> <span> {{ y }}@@ -21,7 +21,7 @@ </span>
</li> {% else %} <li> - <a href="{{ url }}"> + <a href="{{ url|relurl(baseurl) }}"> <strong>{{ y }}</strong> </a> </li>@@ -53,7 +53,7 @@ <h2>{{ post.copyrightYear }}</h2>
{% endif %} {% set _ = year.append(post.copyrightYear)%} {% include 'meta-article.j2.html' %} - {% if loop.last %} + {% if not category.paginated and loop.last %} </section> {% endif %} {% endfor %}
@@ -24,7 +24,7 @@
{% for category, post in posts %} <section> <h2>in: - <a href="{{ category.url }}/"> + <a href="{{ category.url|relurl(baseurl) }}/"> <svg width="16" height="16"><use xlink:href="#icon-{{ category.name }}" /></svg> {{ category.name }} </a>
@@ -7,6 +7,9 @@
{% block meta %} <meta name="description" content="{{ post.description|striptags|e }}" /> <link rel="canonical" href="{{ post.url }}" /> + <link rel="alternate" type="application/json" href="{{ post.url }}index.json" /> + <link rel="alternate" type="application/ld+json" href="{{ post.url }}index.json" /> + <link rel="alternate" type="application/mf2+json" href="https://pin13.net/mf2/?url={{ post.url|urlencode }}" /> <meta property="og:title" content="{{ post.headline }}" /> <meta property="og:type" content="article" /> <meta property="og:url" content="{{ post.url }}" />@@ -38,6 +41,16 @@ </script>
{% endif %} {% endblock %} +{% block cc %} + <li> + <a href="{{ post.license }}"> + <svg width="80" height="15"> + <use xlink:href="#button-cc"/> + </svg> + </a> + </li> +{% endblock %} + {% block content %} <main> <article class="h-entry hentry" lang="{{ post.inLanguage }}" id="article">@@ -48,7 +61,7 @@ <span>
<svg width="16" height="16"> <use xlink:href="#icon-reply" /> </svg> - <a href="{{ post.url }}"> + <a href="{{ post.url|relurl(baseurl) }}"> RE: </a> <a href="{{ post.mentions.url }}" class="u-in-reply-to">@@ -56,7 +69,7 @@ {{ post.mentions.url }}
</a> </span> {% else %} - <a href="{{ post.url }}"> + <a href="{{ post.url|relurl(baseurl) }}"> {{ post.headline }} </a> {% endif %}@@ -65,7 +78,7 @@ </header>
{% if post.review %} <div class="h-review hreview"> - <strong>Review summary of: <a href="{{ post.review.url }}" class="p-name u-url p-item h-product item fn">{{ post.review.name }}</a></strong> + <strong>Review summary of: <a href="{{ post.review.url }}" class="p-name u-url p-item h-product item fn url">{{ post.review.name }}</a></strong> <p> By <span class="p-author h-card reviewer vcard">@@ -86,12 +99,12 @@ {% endif %}
{% if post.description|length %} <div class="e-summary entry-summary"> - {{ post.description }} + {{ post.description|relurl(baseurl) }} </div> {% endif %} <div class="e-content entry-content"> - {{ post.text }} + {{ post.text|relurl(baseurl) }} </div> <footer>@@ -164,7 +177,7 @@ </dd>
<dt>Author</dt> <dd class="p-author h-card vcard"> - <img class="u-photo photo" src="{{ post.author.image }}" alt="" /> + <img class="u-photo photo" src="{{ post.author.image|relurl(baseurl) }}" alt="" /> <a class="p-name u-url fn url" href="{{ post.author.url }}">{{ post.author.name }}</a> <a class="u-email email" href="mailto:{{ post.author.email }}">{{ post.author.email }}</a> </dd>@@ -227,13 +240,24 @@ <h2><a id="comments"></a>Responses</h2>
<ol> {% for comment in post.comment %} <li class="h-entry p-comment hentry"> - <a href="{{ comment.url }}"> - {{ comment.disambiguatingDescription }} - </a> + <i> + {% if 'like-of' == comment.disambiguatingDescription %} + {% set icon = 'star' %} + {% elif 'bookmark-of' == comment.disambiguatingDescription %} + {% set icon = 'bookmark' %} + {% elif 'reply' == comment.disambiguatingDescription %} + {% set icon = 'reply' %} + {% else %} + {% set icon = 'link' %} + {% endif %} + <svg width="16" height="16"> + <use xlink:href="#icon-{{ icon }}"></use> + </svg> + </i> from <span class="p-author h-card vcard"> {% if comment.author.url %} - <a class="u-url p-name fn" href="{{ comment.author.url }}"> + <a class="u-url p-name fn url org" href="{{ comment.author.url }}"> {{ comment.author.name }} </a> {% else %}@@ -247,9 +271,6 @@ <time class="dt-published published" datetime="{{ comment.datePublished }}">
{{ comment.datePublished|printdate }} </time> <br /> - <svg width="16" height="16"> - <use xlink:href="#icon-link"></use> - </svg> <a class="u-url" href="{{ comment.url }}"> {{ comment.url }} </a>
@@ -1,11 +1,15 @@
<!DOCTYPE html> <html{% block lang %}{% endblock %} prefix="og: http://ogp.me/ns# article: http://ogp.me/ns/article#"> <head> + <!--[if lt IE 9]> + <script src="{{ site.url}}/html5shiv-printshiv.js"></script> + <![endif]--> <title>{% block title %}{% endblock %}</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1" /> <meta name="author" content="{{ site.author.name }} ({{ site.author.email }})" /> <link rel="icon" href="{{ site.image }}" /> + <!-- <base href="{{ baseurl }}" /> --> {% for key, value in meta.items() %} <link rel="{{ key }}" href="{{ value }}" /> {% endfor %}@@ -35,7 +39,7 @@ <nav>
<ul> {% for key, data in menu.items() %} <li> - <a title="{{ data.text }}" href="{{ data.url }}" {{ activemenu(key) }} > + <a title="{{ data.text }}" href="{{ data.url|relurl(baseurl) }}" {{ activemenu(key) }} > <svg width="16" height="16"> <use xlink:href="#icon-{{ key }}" /> </svg>@@ -62,7 +66,7 @@ </span>
</form> {% for action in site.potentialAction %} {% if 'SearchAction' == action['@type'] %} - <form role="search" method="get" action="{{ action.url }}"> + <form role="search" method="get" action="{{ action.url|relurl(baseurl) }}"> <label for="qsub"> <input type="submit" value="search" id="qsub" name="qsub" /> <svg width="16" height="16">@@ -88,7 +92,7 @@ <section>
<p> <a href="https://creativecommons.org/">CC</a>, 1999-2019, - <img class="u-photo photo" src="{{ site.author.image }}" alt="Photo of {{ site.author.name }}" /> + <img class="u-photo photo" src="{{ site.author.image|relurl(baseurl) }}" alt="Photo of {{ site.author.name }}" /> <a class="p-name u-url fn url" href="{{ site.author.url }}" rel="me"> {{ site.author.name }}</a> <a class="u-email email" rel="me" href="mailto:{{ site.author.email }}"> <svg width="16" height="16">@@ -136,6 +140,42 @@ <nav>
<a href="https://xn--sr8hvo.ws/🇻🇮📢/previous">←</a> Member of <a href="https://xn--sr8hvo.ws">IndieWeb Webring</a> <a href="https://xn--sr8hvo.ws/🇻🇮📢/next">→</a> + </nav> + </section> + <section> + <nav> + <ul> + <li> + <a href="https://indieweb.org/"> + <svg width="80" height="15"> + <use xlink:href="#button-indieweb"/> + </svg> + </a> + </li> + <li> + <a href="http://microformats.org/"> + <svg width="80" height="15"> + <use xlink:href="#button-microformats"/> + </svg> + </a> + </li> + <li> + <a href="https://www.w3.org/TR/webmention/"> + <svg width="80" height="15"> + <use xlink:href="#button-webmention"/> + </svg> + </a> + </li> +{% block cc %} + <li> + <a href="{{ site.license }}"> + <svg width="80" height="15"> + <use xlink:href="#button-cc"/> + </svg> + </a> + </li> +{% endblock %} + </ul> </nav> </section> </footer>
@@ -6,7 +6,7 @@ <span>
<svg width="16" height="16"> <use xlink:href="#icon-reply" /> </svg> - <a href="{{ post.url }}"> + <a href="{{ post.url|relurl(baseurl) }}"> RE: </a> <a href="{{ post.mentions.url }}" class="u-in-reply-to">@@ -14,7 +14,7 @@ {{ post.mentions.url }}
</a> </span> {% else %} - <a href="{{ post.url }}"> + <a href="{{ post.url|relurl(baseurl) }}"> {{ post.headline }} </a> {% endif %}@@ -25,16 +25,16 @@ </header>
{% if post.description|length %} <div class="e-summary entry-summary"> - {{ post.description }} + {{ post.description|relurl(baseurl) }} <span class="more"> - <a href="{{ post.url }}"> + <a href="{{ post.url|relurl(baseurl) }}"> {% if post.inLanguage == 'hu' %}Tovább »{% else %}Continue »{% endif %} </a> </span> </div> {% else %} <div class="e-content entry-content"> - {{ post.text }} + {{ post.text|relurl(baseurl) }} </div> {% endif %}@@ -44,8 +44,8 @@ <time datetime="{{ post.datePublished }}" class="dt-published published">{{ post.datePublished|printdate }}</time>
<time datetime="{{ post.dateModified }}" class="dt-updated updated"></time> by <span class="p-author h-card vcard"> - <img class="u-photo photo" src="{{ post.author.image }}" alt="" /> - <a class="p-name u-url fn url" href="{{ post.author.url }}">{{ post.author.name }}</a> + <img class="u-photo photo" src="{{ post.author.image|relurl(baseurl) }}" alt="" /> + <a class="p-name u-url fn url org" href="{{ post.author.url }}">{{ post.author.name }}</a> <a class="u-email email" href="mailto:{{ post.author.email }}">{{ post.author.email }}</a> </span> </footer>
@@ -1,4 +1,3 @@
- .token.comment, .token.prolog, .token.doctype,@@ -20,7 +19,7 @@ .token.number,
.token.constant, .token.symbol, .token.deleted { - color: #905; + color: #f05; } .token.selector,@@ -29,7 +28,7 @@ .token.string,
.token.char, .token.builtin, .token.inserted { - color: #690; + color: #7a0; } .token.operator,@@ -43,7 +42,7 @@
.token.atrule, .token.attr-value, .token.keyword { - color: #07a; + color: #09c; } .token.function,@@ -67,4 +66,4 @@ }
.token.entity { cursor: help; -} +}
@@ -1,5 +1,3 @@
- - * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box;@@ -358,4 +356,4 @@ bottom: 0;
right: 0; width: 10em; height: auto; -} +}
@@ -71,4 +71,121 @@ </symbol>
<symbol id="icon-resume" viewBox="0 0 16 16"> <path d="M13.5 0h-12c-0.825 0-1.5 0.675-1.5 1.5v13c0 0.825 0.675 1.5 1.5 1.5h12c0.825 0 1.5-0.675 1.5-1.5v-13c0-0.825-0.675-1.5-1.5-1.5zM13 14h-11v-12h11v12zM4 9h7v1h-7zM4 11h7v1h-7zM5 4.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5zM7.5 6h-2c-0.825 0-1.5 0.45-1.5 1v1h5v-1c0-0.55-0.675-1-1.5-1z"></path> </symbol> + <symbol id="icon-bookmark" viewBox="0 0 16 16"> + <path d="M4 2v14l5-5 5 5v-14zM12 0h-10v14l1-1v-12h9z"></path> + </symbol> + <symbol id="icon-star" viewBox="0 0 16 16"> + <path d="M16 6.204l-5.528-0.803-2.472-5.009-2.472 5.009-5.528 0.803 4 3.899-0.944 5.505 4.944-2.599 4.944 2.599-0.944-5.505 4-3.899z"></path> + </symbol> + <symbol id="icon-reply" viewBox="0 0 16 16"> + <path d="M7 12.119v3.881l-6-6 6-6v3.966c6.98 0.164 6.681-4.747 4.904-7.966 4.386 4.741 3.455 12.337-4.904 12.119z"></path> + </symbol> + <symbol id="button-indieweb" viewBox="0 0 80 15"> + <rect width="80" height="15" fill="#666"/> + <rect x="1" y="1" width="78" height="13" fill="#fff"/> + <path d="m3 4v2h7v-2h-7zm0 3v4h7v-4h-7z" fill="#fc0d1b"/> + <path d="m11 4v2h1v-2h-1zm1 2v3h1v-3h-1zm1 0h1v3h1v2h2v-2h1v-3h1v-2h-6v2zm1 3h-1v2h1v-2z" fill="#fc5d20"/> + <polygon points="21 4 25 4 25 5 26 5 26 7 22 7 22 8 26 8 26 10 25 10 25 11 21 11 21 10 20 10 20 8 19 8 19 7 20 7 20 5 21 5" fill="#fdb02a"/> + <rect x="29" y="2" width="49" height="1" fill="#fda829"/> + <rect x="29" y="3" width="49" height="1" fill="#fd9c27"/> + <rect x="29" y="4" width="49" height="1" fill="#fd9025"/> + <rect x="29" y="5" width="49" height="1" fill="#fd8124"/> + <rect x="29" y="6" width="49" height="1" fill="#fd7222"/> + <rect x="29" y="7" width="49" height="1" fill="#fd6420"/> + <rect x="29" y="8" width="49" height="1" fill="#fc561f"/> + <rect x="29" y="9" width="49" height="1" fill="#fc481e"/> + <rect x="29" y="10" width="49" height="1" fill="#fc371d"/> + <rect x="29" y="11" width="49" height="1" fill="#fc291c"/> + <rect x="29" y="12" width="49" height="1" fill="#fc1c1c"/> + <path d="m34 5h1v5h-1z" fill="#fff"/> + <path d="m37 5h1v1h1v1h1v1h1v-3h1v5h-1v-1h-1v-1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m44 5v5h3v-1h-2v-3h2v-1h-3zm3 1v3h1v-3h-1z" fill="#fff"/> + <path d="m50 5h1v5h-1z" fill="#fff"/> + <path d="m53 5h3v1h-2v1h2v1h-2v1h2v1h-3z" fill="#fff"/> + <path d="m58 5h1v4h1v-3h1v3h1v-4h1v4h-1v1h-1v-1h-1v1h-1v-1h-1z" fill="#fff"/> + <path d="m65 5h3v1h-2v1h2v1h-2v1h2v1h-3z" fill="#fff"/> + <path d="m70 5v5h3v-1h-2v-1h2v-1h-2v-1h2v-1h-3zm3 1v1h1v-1h-1zm0 2v1h1v-1h-1z" fill="#fff"/> + </symbol> + <symbol id="button-webmention" viewBox="0 0 80 15"> + <rect width="80" height="15" fill="#666"/> + <rect x="1" y="1" width="78" height="13" fill="#fff" style="paint-order:markers fill stroke"/> + <path d="m13 1v1h-1v1h-1v1h1 1v1h-1v2h-1v2h-1v-3h-1v-2h-1v2h-1v3h-1v-2h-1v-3h-1v-2h-1-1v2h1v3h1v3h1v3h1 1v-3h1v-2h1v2h1v3h1 1v-3h1v-3h1v-3h1 1v-1h-1v-1h-1v-1h-1z" fill="#610371"/> + <rect x="19" y="2" width="59" height="11" fill="#850e9a"/> + <path d="m33 5v5h3v-1h-2v-1h2v-1h-2v-1h2v-1h-3zm3 1v1h1v-1h-1zm0 2v1h1v-1h-1z" fill="#fff"/> + <path d="m28 5h3v1h-2v1h2v1h-2v1h2v1h-3z" fill="#fff"/> + <path d="m21 5h1v4h1v-3h1v3h1v-4h1v4h-1v1h-1v-1h-1v1h-1v-1h-1z" fill="#fff"/> + <path d="m39 5h1v1h1v1h1v-1h1v-1h1v5h-1v-3h-1v1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m51 5h1v1h1v1h1v1h1v-3h1v5h-1v-1h-1v-1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m58 5h3v1h-1v4h-1v-4h-1" fill="#fff"/> + <path d="m63 5h1v5h-1z" fill="#fff"/> + <path d="m67 5v1h2v-1zm2 1v3h1v-3zm0 3h-2v1h2zm-2 0v-3h-1v3z" fill="#fff"/> + <path d="m71 5h1v1h1v1h1v1h1v-3h1v5h-1v-1h-1v-1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m46 5h3v1h-2v1h2v1h-2v1h2v1h-3z" fill="#fff"/> + </symbol> + <symbol id="button-microformats" viewBox="0 0 80 15"> + <rect width="80" height="15" fill="#666"/> + <rect x="1" y="1" width="78" height="13" fill="#fff"/> + <rect x="18" y="2" width="2" height="11" fill="#5d8f17"/> + <rect x="20" y="2" width="2" height="11" fill="#609218"/> + <rect x="22" y="2" width="2" height="11" fill="#639519"/> + <rect x="24" y="2" width="2" height="11" fill="#66981a"/> + <rect x="26" y="2" width="2" height="11" fill="#699b1b"/> + <rect x="28" y="2" width="2" height="11" fill="#6c9e1b"/> + <rect x="30" y="2" width="2" height="11" fill="#6fa11c"/> + <rect x="32" y="2" width="2" height="11" fill="#72a51d"/> + <rect x="34" y="2" width="2" height="11" fill="#76a81e"/> + <rect x="36" y="2" width="2" height="11" fill="#78aa1f"/> + <rect x="38" y="2" width="2" height="11" fill="#7cae20"/> + <rect x="40" y="2" width="2" height="11" fill="#7eb120"/> + <rect x="42" y="2" width="2" height="11" fill="#82b421"/> + <rect x="44" y="2" width="2" height="11" fill="#85b722"/> + <rect x="46" y="2" width="2" height="11" fill="#88ba23"/> + <rect x="48" y="2" width="2" height="11" fill="#8bbd24"/> + <rect x="50" y="2" width="2" height="11" fill="#8ec125"/> + <rect x="52" y="2" width="2" height="11" fill="#91c325"/> + <rect x="54" y="2" width="2" height="11" fill="#94c626"/> + <rect x="56" y="2" width="2" height="11" fill="#97ca27"/> + <rect x="58" y="2" width="2" height="11" fill="#9acd28"/> + <rect x="60" y="2" width="2" height="11" fill="#9dd029"/> + <rect x="62" y="2" width="2" height="11" fill="#a0d32a"/> + <rect x="64" y="2" width="2" height="11" fill="#a4d62b"/> + <rect x="66" y="2" width="2" height="11" fill="#a6d92b"/> + <rect x="68" y="2" width="2" height="11" fill="#a8db2c"/> + <rect x="70" y="2" width="2" height="11" fill="#acdf2d"/> + <rect x="72" y="2" width="2" height="11" fill="#b0e32e"/> + <rect x="74" y="2" width="2" height="11" fill="#b3e62f"/> + <rect x="76" y="2" width="2" height="11" fill="#b6e92f"/> + <polygon points="4 4 6 4 6 9 7 9 7 10 12 10 12 12 11 12 11 13 4 13 4 12 3 12 3 5 4 5" fill="#5c8d17"/> + <polygon points="7 3 9 3 9 6 10 6 10 7 13 7 13 9 8 9 8 8 7 8" fill="#8dc024"/> + <polygon points="10 2 13 2 13 3 14 3 14 6 11 6 11 5 10 5" fill="#a5d82b"/> + <path d="m20 5h1v1h1v1h1v-1h1v-1h1v5h-1v-3h-1v1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m26 5h1v5h-1z" fill="#fff"/> + <path d="m29 5h2v1h1v1h-1v-1h-2v3h2v-1h1v1h-1v1h-2v-1h-1v-3h1z" fill="#fff"/> + <path d="m33 5v5h1v-2h1v1h1v-2h-2v-1h2v-1h-3zm3 1v1h1v-1h-1zm0 3v1h1v-1h-1z" fill="#fff"/> + <path d="m39 5v1h2v-1h-2zm2 1v1 1 1h1v-1-1-1h-1zm0 3h-2v1h2v-1zm-2 0v-3h-1v3h1z" fill="#fff"/> + <path d="m43 5h3v1h-2v1h2v1h-2v2h-1z" fill="#fff"/> + <path d="m48 5v1h2v-1h-2zm2 1v1 1 1h1v-1-1-1h-1zm0 3h-2v1h2v-1zm-2 0v-3h-1v3h1z" fill="#fff"/> + <path d="m52 5v5h1v-2h1v1h1v-2h-2v-1h2v-1h-3zm3 1v1h1v-1h-1zm0 3v1h1v-1h-1z" fill="#fff"/> + <path d="m57 5h1v1h1v1h1v-1h1v-1h1v5h-1v-3h-1v1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m64 5v1h2v-1h-2zm2 1v1h-2v-1h-1v4h1v-2h2v2h1v-4h-1z" fill="#fff"/> + <path d="m68 5h3v1h-1v4h-1v-4h-1" fill="#fff"/> + <path d="m73 5h3v1h-3v1h2v1h1v1h-1v1h-3v-1h3v-1h-2v-1h-1v-1h1" fill="#fff"/> + </symbol> + <symbol id="button-cc" viewBox="0 0 80 15"> + <rect y="0" width="80" height="15" fill="#000"/> + <rect x="1" y="1" width="78" height="13" fill="#fff"/> + <rect x="2" y="2" width="76" height="11" fill="#000"/> + <path d="m42 5h1v1h1v1h1v-1h1v-1h1v5h-1v-3h-1v1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m31 5h2v1h1v1h-1v-1h-2v3h2v-1h1v1h-1v1h-2v-1h-1v-3h1z" fill="#fff"/> + <path d="m37 5v1h2v-1zm2 1v3h1v-3zm0 3h-2v1h2zm-2 0v-3h-1v3z" fill="#fff"/> + <path d="m57 5v1h2v-1zm2 1v3h1v-3zm0 3h-2v1h2zm-2 0v-3h-1v3z" fill="#fff"/> + <path d="m49 5h1v1h1v1h1v-1h1v-1h1v5h-1v-3h-1v1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m70 5h3v1h-3v1h2v1h1v1h-1v1h-3v-1h3v-1h-2v-1h-1v-1h1" fill="#fff"/> + <path d="m62 5h1v1h1v1h1v1h1v-3h1v5h-1v-1h-1v-1h-1v-1h-1v3h-1z" fill="#fff"/> + <path d="m2 2v11h19.891a8.5136 8.5 0 0 0 2.1088-5.5898 8.5136 8.5 0 0 0-1.9563-5.4102h-13.119z" fill="#aaa"/> + <path d="m6.0195 2a8.5 8.5 0 0 0-2.0195 5.5 8.5 8.5 0 0 0 2.0312 5.5h12.949a8.5 8.5 0 0 0 2.0195-5.5 8.5 8.5 0 0 0-2.0312-5.5z" fill="#000"/> + <circle cx="12.5" cy="7.5" r="6.5" fill="#fff"/> + <path d="m9 5h2v1h1v1h-1v-1h-2v3h2v-1h1v1h-1v1h-2v-1h-1v-3h1z" fill="#000"/> + <path d="m14 5h2v1h1v1h-1v-1h-2v3h2v-1h1v1h-1v1h-2v-1h-1v-3h1z" fill="#000"/> + </symbol> </svg>