# -*- coding: utf-8 -*- import os import re import json import time import datetime import shutil import urllib import requests import argparse from github import Github from xpinyin import Pinyin from feedgen.feed import FeedGenerator from jinja2 import Environment, FileSystemLoader from transliterate import translit ###################################################################################### i18n={"Search":"Search","switchTheme":"switch theme","link":"link","home":"home","comments":"comments","run":"run ","days":" days","Previous":"Previous","Next":"Next"} i18nCN={"Search":"搜索","switchTheme":"切换主题","link":"友情链接","home":"首页","comments":"评论","run":"网站运行","days":"天","Previous":"上一页","Next":"下一页"} i18nRU={"Search":"Поиск","switchTheme": "Сменить тему","link":"ссылка","home":"Главная","comments":"Комментарии","run":"работает ","days":" дней","Previous":"Предыдущая","Next":"Следующая"} IconList={ "post":"M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25Zm1.75-.25a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25ZM3.5 6.25a.75.75 0 0 1 .75-.75h7a.75.75 0 0 1 0 1.5h-7a.75.75 0 0 1-.75-.75Zm.75 2.25h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1 0-1.5Z", "link":"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z", "about":"M10.561 8.073a6.005 6.005 0 0 1 3.432 5.142.75.75 0 1 1-1.498.07 4.5 4.5 0 0 0-8.99 0 .75.75 0 0 1-1.498-.07 6.004 6.004 0 0 1 3.431-5.142 3.999 3.999 0 1 1 5.123 0ZM10.5 5a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z", "sun":"M8 10.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.06zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.06 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z", "moon":"M9.598 1.591a.75.75 0 01.785-.175 7 7 0 11-8.967 8.967.75.75 0 01.961-.96 5.5 5.5 0 007.046-7.046.75.75 0 01.175-.786zm1.616 1.945a7 7 0 01-7.678 7.678 5.5 5.5 0 107.678-7.678z", "search":"M15.7 13.3l-3.81-3.83A5.93 5.93 0 0 0 13 6c0-3.31-2.69-6-6-6S1 2.69 1 6s2.69 6 6 6c1.3 0 2.48-.41 3.47-1.11l3.83 3.81c.19.2.45.3.7.3.25 0 .52-.09.7-.3a.996.996 0 0 0 0-1.41v.01zM7 10.7c-2.59 0-4.7-2.11-4.7-4.7 0-2.59 2.11-4.7 4.7-4.7 2.59 0 4.7 2.11 4.7 4.7 0 2.59-2.11 4.7-4.7 4.7z", "rss":"M2.002 2.725a.75.75 0 0 1 .797-.699C8.79 2.42 13.58 7.21 13.974 13.201a.75.75 0 0 1-1.497.098 10.502 10.502 0 0 0-9.776-9.776.747.747 0 0 1-.7-.798ZM2.84 7.05h-.002a7.002 7.002 0 0 1 6.113 6.111.75.75 0 0 1-1.49.178 5.503 5.503 0 0 0-4.8-4.8.75.75 0 0 1 .179-1.489ZM2 13a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z", "upload":"M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z M11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.811V9.5a.75.75 0 0 1-1.5 0V3.811L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z", "github":"M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z", "home":"M6.906.664a1.749 1.749 0 0 1 2.187 0l5.25 4.2c.415.332.657.835.657 1.367v7.019A1.75 1.75 0 0 1 13.25 15h-3.5a.75.75 0 0 1-.75-.75V9H7v5.25a.75.75 0 0 1-.75.75h-3.5A1.75 1.75 0 0 1 1 13.25V6.23c0-.531.242-1.034.657-1.366l5.25-4.2Zm1.25 1.171a.25.25 0 0 0-.312 0l-5.25 4.2a.25.25 0 0 0-.094.196v7.019c0 .138.112.25.25.25H5.5V8.25a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 .75.75v5.25h2.75a.25.25 0 0 0 .25-.25V6.23a.25.25 0 0 0-.094-.195Z" } ###################################################################################### class GMEEK(): def __init__(self,options): self.options=options self.root_dir='docs/' self.post_folder='post/' self.backup_dir='backup/' self.post_dir=self.root_dir+self.post_folder user = Github(self.options.github_token) self.repo = self.get_repo(user, options.repo_name) self.feed = FeedGenerator() self.oldFeedString='' self.labelColorDict=json.loads('{}') for label in self.repo.get_labels(): self.labelColorDict[label.name]='#'+label.color print(self.labelColorDict) self.defaultConfig() def cleanFile(self): if os.path.exists(self.backup_dir): shutil.rmtree(self.backup_dir) if os.path.exists(self.root_dir): shutil.rmtree(self.root_dir) os.mkdir(self.backup_dir) os.mkdir(self.root_dir) os.mkdir(self.post_dir) def defaultConfig(self): dconfig={"singlePage":[],"startSite":"","filingNum":"","onePageListNum":15,"commentLabelColor":"#006b75","yearColorList":["#bc4c00", "#0969da", "#1f883d", "#A333D0"],"i18n":"CN","dayTheme":"light","nightTheme":"dark","urlMode":"pinyin","script":"","style":"","bottomText":"","showPostSource":1} config=json.loads(open('config.json', 'r', encoding='utf-8').read()) self.blogBase={**dconfig,**config}.copy() self.blogBase["postListJson"]=json.loads('{}') self.blogBase["singeListJson"]=json.loads('{}') if "displayTitle" not in self.blogBase: self.blogBase["displayTitle"]=self.blogBase["title"] if "faviconUrl" not in self.blogBase: self.blogBase["faviconUrl"]=self.blogBase["avatarUrl"] if "homeUrl" not in self.blogBase: if str(self.repo.name) == (str(self.repo.owner.login)+".github.io"): pages_url = f"https://{self.repo.name}" else: pages_url = f"https://{self.repo.owner.login}.github.io/{self.repo.name}" self.blogBase["homeUrl"]=pages_url print("GitHub Pages URL: ", self.blogBase["homeUrl"]) if self.blogBase["i18n"]=="CN": self.i18n=i18nCN elif self.blogBase["i18n"]=="RU": self.i18n=i18nRU else: self.i18n=i18n def get_repo(self,user:Github, repo:str): return user.get_repo(repo) def markdown2html(self, mdstr): payload = {"text": mdstr, "mode": "gfm"} headers = {"Authorization": "token {}".format(self.options.github_token)} try: response = requests.post("https://api.github.com/markdown", json=payload, headers=headers) response.raise_for_status() # Raises an exception if status code is not 200 return response.text except requests.RequestException as e: raise Exception("markdown2html error: {}".format(e)) def renderHtml(self,template,blogBase,postListJson,htmlDir): file_loader = FileSystemLoader('templates') env = Environment(loader=file_loader) template = env.get_template(template) output = template.render(blogBase=blogBase,postListJson=postListJson,i18n=self.i18n,IconList=IconList) f = open(htmlDir, 'w', encoding='UTF-8') f.write(output) f.close() def createPostHtml(self,issue): f = open("backup/"+issue["postTitle"]+".md", 'r', encoding='UTF-8') post_body=self.markdown2html(f.read()) f.close() postBase=self.blogBase.copy() if '','',post_body) post_body=re.sub(r'','',post_body) postBase["script"]=postBase["script"]+'' postBase["postTitle"]=issue["postTitle"] postBase["postBody"]=post_body postBase["commentNum"]=issue["commentNum"] postBase["style"]=issue["style"] postBase["script"]=postBase["script"]+issue["script"] postBase["top"]=issue["top"] postBase["postSourceUrl"]=issue["postSourceUrl"] postBase["repoName"]=options.repo_name if "highlight" in post_body: postBase["highlight"]=1 else: postBase["highlight"]=0 if issue["label"] in self.blogBase["singlePage"]: postBase["bottomText"]='' self.renderHtml('post.html',postBase,{},issue["htmlDir"]) print("create postPage title=%s file=%s " % (issue["postTitle"],issue["htmlDir"])) def createPlistHtml(self): self.blogBase["postListJson"]=dict(sorted(self.blogBase["postListJson"].items(),key=lambda x:(x[1]["top"],x[1]["createdAt"]),reverse=True))#使列表由时间排序 postNum=len(self.blogBase["postListJson"]) pageFlag=0 while True: topNum=pageFlag*self.blogBase["onePageListNum"] print("topNum=%d postNum=%d"%(topNum,postNum)) if postNum<=self.blogBase["onePageListNum"]: if pageFlag==0: onePageList=dict(list(self.blogBase["postListJson"].items())[:postNum]) htmlDir=self.root_dir+"index.html" self.blogBase["prevUrl"]="disabled" self.blogBase["nextUrl"]="disabled" else: onePageList=dict(list(self.blogBase["postListJson"].items())[topNum:topNum+postNum]) htmlDir=self.root_dir+("page%d.html" % (pageFlag+1)) if pageFlag==1: self.blogBase["prevUrl"]="/index.html" else: self.blogBase["prevUrl"]="/page%d.html" % pageFlag self.blogBase["nextUrl"]="disabled" self.renderHtml('plist.html',self.blogBase,onePageList,htmlDir) print("create "+htmlDir) break else: onePageList=dict(list(self.blogBase["postListJson"].items())[topNum:topNum+self.blogBase["onePageListNum"]]) postNum=postNum-self.blogBase["onePageListNum"] if pageFlag==0: htmlDir=self.root_dir+"index.html" self.blogBase["prevUrl"]="disabled" self.blogBase["nextUrl"]="/page2.html" else: htmlDir=self.root_dir+("page%d.html" % (pageFlag+1)) if pageFlag==1: self.blogBase["prevUrl"]="/index.html" else: self.blogBase["prevUrl"]="/page%d.html" % pageFlag self.blogBase["nextUrl"]="/page%d.html" % (pageFlag+2) self.renderHtml('plist.html',self.blogBase,onePageList,htmlDir) print("create "+htmlDir) pageFlag=pageFlag+1 self.renderHtml('tag.html',self.blogBase,onePageList,self.root_dir+"tag.html") print("create tag.html") def createFeedXml(self): self.blogBase["postListJson"]=dict(sorted(self.blogBase["postListJson"].items(),key=lambda x:x[1]["createdAt"],reverse=False))#使列表由时间排序 feed = FeedGenerator() feed.title(self.blogBase["title"]) feed.description(self.blogBase["subTitle"]) feed.link(href=self.blogBase["homeUrl"]) feed.image(url=self.blogBase["avatarUrl"],title="avatar", link=self.blogBase["homeUrl"]) feed.copyright(self.blogBase["title"]) feed.managingEditor(self.blogBase["title"]) feed.webMaster(self.blogBase["title"]) feed.ttl("60") for num in self.blogBase["singeListJson"]: item=feed.add_item() item.guid(self.blogBase["homeUrl"]+"/"+self.blogBase["singeListJson"][num]["label"]+".html",permalink=True) item.title(self.blogBase["singeListJson"][num]["postTitle"]) item.description(self.blogBase["singeListJson"][num]["description"]) item.link(href=self.blogBase["homeUrl"]+"/"+self.blogBase["singeListJson"][num]["label"]+".html") item.pubDate(time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(self.blogBase["singeListJson"][num]["createdAt"]))) for num in self.blogBase["postListJson"]: item=feed.add_item() item.guid(self.blogBase["homeUrl"]+"/"+self.blogBase["postListJson"][num]["postUrl"],permalink=True) item.title(self.blogBase["postListJson"][num]["postTitle"]) item.description(self.blogBase["postListJson"][num]["description"]) item.link(href=self.blogBase["homeUrl"]+"/"+self.blogBase["postListJson"][num]["postUrl"]) item.pubDate(time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(self.blogBase["postListJson"][num]["createdAt"]))) if self.oldFeedString!='': feed.rss_file(self.root_dir+'new.xml') newFeed=open(self.root_dir+'new.xml','r',encoding='utf-8') new=newFeed.read() newFeed.close() new=re.sub(r'.*?','',new) old=re.sub(r'.*?','',self.oldFeedString) os.remove(self.root_dir+'new.xml') if new==old: print("====== rss xml no update ======") feedFile=open(self.root_dir+'rss.xml',"w") feedFile.write(self.oldFeedString) feedFile.close() return print("====== create rss xml ======") feed.rss_file(self.root_dir+'rss.xml') def addOnePostJson(self,issue): if len(issue.labels)==1: if issue.labels[0].name in self.blogBase["singlePage"]: listJsonName='singeListJson' gen_Html = self.root_dir+'{}.html'.format(issue.labels[0].name) else: listJsonName='postListJson' if self.blogBase["urlMode"]=="issue": gen_Html = self.post_dir+'{}.html'.format(str(issue.number)) elif self.blogBase["urlMode"]=="ru_translit": gen_Html = self.post_dir+'{}.html'.format(str(translit(issue.title, language_code='ru', reversed=True)).replace(' ', '-')) else: gen_Html = self.post_dir+'{}.html'.format(Pinyin().get_pinyin(issue.title)) postNum="P"+str(issue.number) self.blogBase[listJsonName][postNum]=json.loads('{}') self.blogBase[listJsonName][postNum]["htmlDir"]=gen_Html self.blogBase[listJsonName][postNum]["label"]=issue.labels[0].name self.blogBase[listJsonName][postNum]["labelColor"]=self.labelColorDict[issue.labels[0].name] self.blogBase[listJsonName][postNum]["postTitle"]=issue.title if self.blogBase["urlMode"]=="issue": self.blogBase[listJsonName][postNum]["postUrl"]=urllib.parse.quote(self.post_folder+'{}.html'.format(str(issue.number))) elif self.blogBase["urlMode"]=="ru_translit": self.blogBase[listJsonName][postNum]["postUrl"]=urllib.parse.quote(self.post_folder+'{}.html'.format(str(translit(issue.title, language_code='ru', reversed=True)).replace(' ', '-'))) else: self.blogBase[listJsonName][postNum]["postUrl"]=urllib.parse.quote(self.post_folder+'{}.html'.format(Pinyin().get_pinyin(issue.title))) self.blogBase[listJsonName][postNum]["postSourceUrl"]="https://github.com/"+options.repo_name+"/issues/"+str(issue.number) self.blogBase[listJsonName][postNum]["commentNum"]=issue.get_comments().totalCount if self.blogBase["i18n"]=="CN": period="。" else: period="." if issue.body==None: self.blogBase[listJsonName][postNum]["description"]='' else: self.blogBase[listJsonName][postNum]["description"]=issue.body.split(period)[0]+period self.blogBase[listJsonName][postNum]["top"]=0 for event in issue.get_events(): if event.event=="pinned": self.blogBase[listJsonName][postNum]["top"]=1 elif event.event=="unpinned": self.blogBase[listJsonName][postNum]["top"]=0 try: postConfig=json.loads(issue.body.split("\r\n")[-1:][0].split("##")[1]) print("Has Custom JSON parameters") print(postConfig) except: postConfig={} if "timestamp" in postConfig: self.blogBase[listJsonName][postNum]["createdAt"]=postConfig["timestamp"] else: self.blogBase[listJsonName][postNum]["createdAt"]=int(time.mktime(issue.created_at.timetuple())) if "style" in postConfig: self.blogBase[listJsonName][postNum]["style"]=self.blogBase["style"]+str(postConfig["style"]) else: self.blogBase[listJsonName][postNum]["style"]=self.blogBase["style"] if "script" in postConfig: self.blogBase[listJsonName][postNum]["script"]=self.blogBase["script"]+str(postConfig["script"]) else: self.blogBase[listJsonName][postNum]["script"]=self.blogBase["script"] thisTime=datetime.datetime.fromtimestamp(self.blogBase[listJsonName][postNum]["createdAt"]) thisYear=thisTime.year self.blogBase[listJsonName][postNum]["createdDate"]=thisTime.strftime("%Y-%m-%d") self.blogBase[listJsonName][postNum]["dateLabelColor"]=self.blogBase["yearColorList"][int(thisYear)%len(self.blogBase["yearColorList"])] f = open("backup/"+issue.title+".md", 'w', encoding='UTF-8') if issue.body==None: f.write('') else: f.write(issue.body) f.close() return listJsonName def runAll(self): print("====== start create static html ======") self.cleanFile() issues=self.repo.get_issues() for issue in issues: self.addOnePostJson(issue) for issue in self.blogBase["postListJson"].values(): self.createPostHtml(issue) for issue in self.blogBase["singeListJson"].values(): self.createPostHtml(issue) self.createPlistHtml() self.createFeedXml() print("====== create static html end ======") def runOne(self,number_str): print("====== start create static html ======") issue=self.repo.get_issue(int(number_str)) listJsonName=self.addOnePostJson(issue) self.createPostHtml(self.blogBase[listJsonName]["P"+number_str]) self.createPlistHtml() self.createFeedXml() print("====== create static html end ======") ###################################################################################### parser = argparse.ArgumentParser() parser.add_argument("github_token", help="github_token") parser.add_argument("repo_name", help="repo_name") parser.add_argument("--issue_number", help="issue_number", default=0, required=False) options = parser.parse_args() blog=GMEEK(options) if not os.path.exists("blogBase.json"): print("blogBase is not exists, runAll") blog.runAll() else: if os.path.exists(blog.root_dir+'rss.xml'): oldFeedFile=open(blog.root_dir+'rss.xml','r',encoding='utf-8') blog.oldFeedString=oldFeedFile.read() oldFeedFile.close() if options.issue_number=="0" or options.issue_number=="": print("issue_number=='0', runAll") blog.runAll() else: f=open("blogBase.json","r") print("blogBase is exists and issue_number!=0, runOne") blog.blogBase=json.loads(f.read()) f.close() blog.runOne(options.issue_number) listFile=open("blogBase.json","w") listFile.write(json.dumps(blog.blogBase)) listFile.close() print("====== create postList.json file ======") blog.blogBase["postListJson"]=dict(sorted(blog.blogBase["postListJson"].items(),key=lambda x:x[1]["createdAt"],reverse=True))#使列表由时间排序 for i in blog.blogBase["postListJson"]: del blog.blogBase["postListJson"][i]["description"] del blog.blogBase["postListJson"][i]["postSourceUrl"] del blog.blogBase["postListJson"][i]["htmlDir"] del blog.blogBase["postListJson"][i]["createdAt"] del blog.blogBase["postListJson"][i]["script"] del blog.blogBase["postListJson"][i]["style"] del blog.blogBase["postListJson"][i]["top"] docListFile=open(blog.root_dir+"postList.json","w") docListFile.write(json.dumps(blog.blogBase["postListJson"])) docListFile.close() print("====== update readme file ======") readme="# "+blog.blogBase["title"]+'\r\n' readme=readme+"### Blog Home :link: "+blog.blogBase["homeUrl"]+'\r\n' readme=readme+"### Powered by :heart: [Gmeek](https://github.com/Meekdai/Gmeek)\r\n" readmeFile=open("README.md","w") readmeFile.write(readme) readmeFile.close() workspace_path = os.environ.get('GITHUB_WORKSPACE') print(f"GitHub Workspace Path: {workspace_path}") ######################################################################################