Page MenuHomeMusing Studio

No OneTemporary

diff --git a/templates.go b/templates.go
index 413f76a..9c2f1d3 100644
--- a/templates.go
+++ b/templates.go
@@ -1,185 +1,189 @@
package writefreely
import (
"fmt"
"github.com/dustin/go-humanize"
"github.com/writeas/web-core/l10n"
"github.com/writeas/web-core/log"
"html/template"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
)
var (
templates = map[string]*template.Template{}
pages = map[string]*template.Template{}
userPages = map[string]*template.Template{}
funcMap = template.FuncMap{
"largeNumFmt": largeNumFmt,
"pluralize": pluralize,
"isRTL": isRTL,
"isLTR": isLTR,
"localstr": localStr,
"localhtml": localHTML,
"tolower": strings.ToLower,
}
)
const (
templatesDir = "templates"
pagesDir = "pages"
)
func showUserPage(w http.ResponseWriter, name string, obj interface{}) {
if obj == nil {
log.Error("showUserPage: data is nil!")
return
}
if err := userPages[filepath.Join("user", name+".tmpl")].ExecuteTemplate(w, name, obj); err != nil {
log.Error("Error parsing %s: %v", name, err)
}
}
func initTemplate(name string) {
if debugging {
log.Info(" %s%s%s.tmpl", templatesDir, string(filepath.Separator), name)
}
if name == "collection" || name == "collection-tags" {
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(
filepath.Join(templatesDir, name+".tmpl"),
filepath.Join(templatesDir, "include", "posts.tmpl"),
filepath.Join(templatesDir, "include", "footer.tmpl"),
+ filepath.Join(templatesDir, "include", "render.tmpl"),
filepath.Join(templatesDir, "base.tmpl"),
))
} else {
templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(
filepath.Join(templatesDir, name+".tmpl"),
filepath.Join(templatesDir, "include", "footer.tmpl"),
+ filepath.Join(templatesDir, "include", "render.tmpl"),
filepath.Join(templatesDir, "base.tmpl"),
))
}
}
func initPage(path, key string) {
if debugging {
log.Info(" %s", key)
}
pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(
path,
filepath.Join(templatesDir, "include", "footer.tmpl"),
+ filepath.Join(templatesDir, "include", "render.tmpl"),
filepath.Join(templatesDir, "base.tmpl"),
))
}
func initUserPage(path, key string) {
if debugging {
log.Info(" %s", key)
}
userPages[key] = template.Must(template.New(key).Funcs(funcMap).ParseFiles(
path,
filepath.Join(templatesDir, "user", "include", "header.tmpl"),
filepath.Join(templatesDir, "user", "include", "footer.tmpl"),
+ filepath.Join(templatesDir, "user", "include", "render.tmpl"),
))
}
func initTemplates() error {
log.Info("Loading templates...")
tmplFiles, err := ioutil.ReadDir(templatesDir)
if err != nil {
return err
}
for _, f := range tmplFiles {
if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
parts := strings.Split(f.Name(), ".")
key := parts[0]
initTemplate(key)
}
}
log.Info("Loading pages...")
// Initialize all static pages that use the base template
filepath.Walk(pagesDir, func(path string, i os.FileInfo, err error) error {
if !i.IsDir() && !strings.HasPrefix(i.Name(), ".") {
parts := strings.Split(path, string(filepath.Separator))
key := i.Name()
if len(parts) > 2 {
key = fmt.Sprintf("%s%s%s", parts[1], string(filepath.Separator), i.Name())
}
initPage(path, key)
}
return nil
})
log.Info("Loading user pages...")
// Initialize all user pages that use base templates
filepath.Walk(filepath.Join(templatesDir, "user"), func(path string, f os.FileInfo, err error) error {
if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
parts := strings.Split(path, string(filepath.Separator))
key := f.Name()
if len(parts) > 2 {
key = filepath.Join(parts[1], f.Name())
}
initUserPage(path, key)
}
return nil
})
return nil
}
// renderPage retrieves the given template and renders it to the given io.Writer.
// If something goes wrong, the error is logged and returned.
func renderPage(w io.Writer, tmpl string, data interface{}) error {
err := pages[tmpl].ExecuteTemplate(w, "base", data)
if err != nil {
log.Error("%v", err)
}
return err
}
func largeNumFmt(n int64) string {
return humanize.Comma(n)
}
func pluralize(singular, plural string, n int64) string {
if n == 1 {
return singular
}
return plural
}
func isRTL(d string) bool {
return d == "rtl"
}
func isLTR(d string) bool {
return d == "ltr" || d == "auto"
}
func localStr(term, lang string) string {
s := l10n.Strings(lang)[term]
if s == "" {
s = l10n.Strings("")[term]
}
return s
}
func localHTML(term, lang string) template.HTML {
s := l10n.Strings(lang)[term]
if s == "" {
s = l10n.Strings("")[term]
}
s = strings.Replace(s, "write.as", "<a href=\"https://writefreely.org\">write freely</a>", 1)
return template.HTML(s)
}
diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl
index b3e04cc..5baea38 100644
--- a/templates/collection-post.tmpl
+++ b/templates/collection-post.tmpl
@@ -1,147 +1,138 @@
{{define "post"}}<!DOCTYPE HTML>
<html {{if .Language.Valid}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">
<head prefix="og: http://ogp.me/ns# article: http://ogp.me/ns/article#">
<meta charset="utf-8">
<title>{{.PlainDisplayTitle}} &mdash; {{.Collection.DisplayTitle}}</title>
<link rel="stylesheet" type="text/css" href="/css/write.css" />
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="canonical" href="{{.CanonicalURL}}" />
<meta name="generator" content="Write Freely">
<meta name="title" content="{{.PlainDisplayTitle}} &mdash; {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
<meta name="description" content="{{.Summary}}">
{{if gt .Views 1}}<meta name="twitter:label1" value="Views">
<meta name="twitter:data1" value="{{largeNumFmt .Views}}">{{end}}
<meta name="author" content="{{.Collection.Title}}" />
<meta itemprop="description" content="{{.Summary}}">
<meta itemprop="datePublished" content="{{.CreatedDate}}" />
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="{{.Summary}}">
<meta name="twitter:title" content="{{.PlainDisplayTitle}} &mdash; {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
{{if gt (len .Images) 0}}<meta name="twitter:image" content="{{index .Images 0}}">{{else}}<meta name="twitter:image" content="{{.Collection.AvatarURL}}">{{end}}
<meta property="og:title" content="{{.PlainDisplayTitle}}" />
<meta property="og:description" content="{{.Summary}}" />
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" />
<meta property="og:type" content="article" />
<meta property="og:url" content="{{.CanonicalURL}}" />
<meta property="og:updated_time" content="{{.Created8601}}" />
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
<meta property="article:published_time" content="{{.Created8601}}">
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
{{if .Collection.RenderMathJax}}
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
extensions: ["tex2jax.js"],
jax: ["input/TeX", "output/HTML-CSS"],
tex2jax: {
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
processEscapes: true
},
"HTML-CSS": { fonts: ["TeX"] }
});
</script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML" async></script>{{end}}
- {{if .Collection.CodeHighlight}}
- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/atom-one-light.min.css" />
- <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js"></script>
- <script>
- addEventListener('load', function () {
- var x = document.querySelectorAll("code[class^='language-']");
- for (i=0; i<x.length; i++) {
- hljs.highlightBlock(x[i]);
- }});
- </script>
- {{end}}
+ <!-- Add highlighting logic -->
+ {{template "highlighting" .}}
</head>
<body id="post">
<div id="overlay"></div>
<header>
<h1 dir="{{.Direction}}" id="blog-title"><a rel="author" href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a></h1>
<nav>
{{if .PinnedPosts}}
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
{{end}}
{{ if .IsOwner }}<span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span>
<a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a>
{{if .IsPinned}}<a class="xtra-feature unpin" href="/{{.Collection.Alias}}/{{.Slug.String}}/unpin" dir="{{.Direction}}" onclick="unpinPost(event, '{{.ID}}')">Unpin</a>{{end}}
{{ end }}
</nav>
</header>
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
{{ if .Collection.ShowFooterBranding }}
<footer dir="ltr"><hr><nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav></footer>
{{ end }}
</body>
{{if .Collection.CanShowScript}}
{{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}}
{{end}}
<script type="text/javascript">
var pinning = false;
function unpinPost(e, postID) {
e.preventDefault();
if (pinning) {
return;
}
pinning = true;
var $header = document.getElementsByTagName('header')[0];
var callback = function() {
// Hide current page
var $pinnedNavLink = $header.getElementsByTagName('nav')[0].querySelector('.pinned.selected');
$pinnedNavLink.style.display = 'none';
};
var $pinBtn = $header.getElementsByClassName('unpin')[0];
$pinBtn.innerHTML = '...';
var http = new XMLHttpRequest();
var url = "/api/collections/{{.Collection.Alias}}/unpin";
var params = [ { "id": postID } ];
http.open("POST", url, true);
http.setRequestHeader("Content-type", "application/json");
http.onreadystatechange = function() {
if (http.readyState == 4) {
pinning = false;
if (http.status == 200) {
callback();
$pinBtn.style.display = 'none';
$pinBtn.innerHTML = 'Pin';
} else if (http.status == 409) {
$pinBtn.innerHTML = 'Unpin';
} else {
$pinBtn.innerHTML = 'Unpin';
alert("Failed to unpin." + (http.status>=500?" Please try again.":""));
}
}
}
http.send(JSON.stringify(params));
};
try { // Fonts
WebFontConfig = {
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans:400,700:latin' ], urls: [ '/css/fonts.css' ] }
};
(function() {
var wf = document.createElement('script');
wf.src = '/js/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
})();
} catch (e) { /* ¯\_(ツ)_/¯ */ }
</script>
</html>{{end}}
diff --git a/templates/collection-tags.tmpl b/templates/collection-tags.tmpl
index 4c2a126..e5c5fdc 100644
--- a/templates/collection-tags.tmpl
+++ b/templates/collection-tags.tmpl
@@ -1,213 +1,203 @@
{{define "collection-tags"}}<!DOCTYPE HTML>
<html>
<head prefix="og: http://ogp.me/ns# article: http://ogp.me/ns/article#">
<meta charset="utf-8">
<title>{{.Tag}} &mdash; {{.Collection.DisplayTitle}}</title>
<link rel="stylesheet" type="text/css" href="{{if .IsCustomDomain}}https://files.writeas.org{{end}}/css/write.css" />
<link rel="shortcut icon" href="{{ if .Collection.ShowFooterBranding }}{{if .IsCustomDomain}}https://files.writeas.org{{end}}/favicon.ico{{ else }}/none.ico{{ end }}" />
{{if not .Collection.IsPrivate}}<link rel="alternate" type="application/rss+xml" title="{{.Tag}} posts on {{.DisplayTitle}}" href="{{.CanonicalURL}}tag:{{.Tag}}/feed/" />{{end}}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="canonical" href="{{.CanonicalURL}}tag:{{.Tag | tolower}}" />
<meta name="generator" content="Write.as">
<meta name="title" content="{{.Tag}} &mdash; {{.Collection.DisplayTitle}}">
<meta name="description" content="{{.Tag}} posts on {{.Collection.DisplayTitle}}">
<meta name="application-name" content="Write.as">
<meta name="application-url" content="https://write.as">
{{if gt .Views 1}}<meta name="twitter:label1" value="Views">
<meta name="twitter:data1" value="{{largeNumFmt .Views}}">{{end}}
<meta itemprop="name" content="{{.Collection.DisplayTitle}}">
<meta itemprop="description" content="{{.Tag}} posts on {{.Collection.DisplayTitle}}">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@writeas__">
<meta name="twitter:description" content="{{.Tag}} posts on {{.Collection.DisplayTitle}}">
<meta name="twitter:title" content="{{.Tag}} &mdash; {{.Collection.DisplayTitle}}">
<meta name="twitter:image" content="{{.Collection.AvatarURL}}">
<meta property="og:title" content="{{.Tag}} &mdash; {{.Collection.DisplayTitle}}" />
<meta property="og:site_name" content="{{.DisplayTitle}}" />
<meta property="og:type" content="article" />
<meta property="og:url" content="{{.CanonicalURL}}tag:{{.Tag}}" />
<meta property="og:image" content="{{.Collection.AvatarURL}}">
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}}
{{if .Collection.RenderMathJax}}
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
extensions: ["tex2jax.js"],
jax: ["input/TeX", "output/HTML-CSS"],
tex2jax: {
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
processEscapes: true
},
"HTML-CSS": { fonts: ["TeX"] }
});
</script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML" async></script>{{end}}
- {{if .CodeHighlight}}
- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/atom-one-light.min.css" />
- <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js"></script>
- <script>
- addEventListener('load', function () {
- var x = document.querySelectorAll("code[class^='language-']");
- for (i=0; i<x.length; i++) {
- hljs.highlightBlock(x[i]);
- }});
- </script>
- {{end}}
-
+ <!-- Add highlighting logic -->
+ {{template "highlighting" . }}
</head>
<body id="subpage">
<div id="overlay"></div>
<header>
<h1 dir="{{.Direction}}" id="blog-title"><a href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a></h1>
<nav>
{{if .PinnedPosts}}
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.DisplayTitle}}</a>{{end}}
{{end}}
</nav>
</header>
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
<h1>{{.Tag}}</h1>
{{template "posts" .}}
{{if .Posts}}</section>{{else}}</div>{{end}}
{{ if .Collection.ShowFooterBranding }}
<footer dir="ltr">
<hr>
<nav>
<p style="font-size: 0.9em"><a class="home pubd" href="/">{{.SiteName}}</a> &middot; powered by <a style="margin-left:0" href="https://writefreely.org">write freely</a></p>
</nav>
</footer>
{{ end }}
</body>
{{if .CanShowScript}}
{{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
{{if .Collection.Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}}
{{end}}
{{if .IsOwner}}
<script src="/js/h.js"></script>
<script src="/js/postactions.js"></script>
{{end}}
<script type="text/javascript">
{{if .IsOwner}}
var deleting = false;
function delPost(e, id, owned) {
e.preventDefault();
if (deleting) {
return;
}
// TODO: UNDO!
if (window.confirm('Are you sure you want to delete this post?')) {
// AJAX
deletePost(id, "", function() {
// Remove post from list
var $postEl = document.getElementById('post-' + id);
$postEl.parentNode.removeChild($postEl);
// TODO: add next post from this collection at the bottom
});
}
}
var deletePost = function(postID, token, callback) {
deleting = true;
var $delBtn = document.getElementById('post-' + postID).getElementsByClassName('delete action')[0];
$delBtn.innerHTML = '...';
var http = new XMLHttpRequest();
var url = "/api/posts/" + postID;
http.open("DELETE", url, true);
http.onreadystatechange = function() {
if (http.readyState == 4) {
deleting = false;
if (http.status == 204) {
callback();
} else if (http.status == 409) {
$delBtn.innerHTML = 'delete';
alert("Post is synced to another account. Delete the post from that account instead.");
// TODO: show "remove" button instead of "delete" now
// Persist that state.
// Have it remove the post locally only.
} else {
$delBtn.innerHTML = 'delete';
alert("Failed to delete." + (http.status>=500?" Please try again.":""));
}
}
}
http.send();
};
var pinning = false;
function pinPost(e, postID, slug, title) {
e.preventDefault();
if (pinning) {
return;
}
pinning = true;
var callback = function() {
// Visibly remove post from collection
var $postEl = document.getElementById('post-' + postID);
$postEl.parentNode.removeChild($postEl);
var $header = document.getElementsByTagName('header')[0];
var $pinnedNavs = $header.getElementsByTagName('nav');
// Add link to nav
var link = '<a class="pinned" href="/{{.Alias}}/'+slug+'">'+title+'</a>';
if ($pinnedNavs.length == 0) {
$header.insertAdjacentHTML("beforeend", '<nav>'+link+'</nav>');
} else {
$pinnedNavs[0].insertAdjacentHTML("beforeend", link);
}
};
var $pinBtn = document.getElementById('post-' + postID).getElementsByClassName('pin action')[0];
$pinBtn.innerHTML = '...';
var http = new XMLHttpRequest();
var url = "/api/collections/{{.Alias}}/pin";
var params = [ { "id": postID } ];
http.open("POST", url, true);
http.setRequestHeader("Content-type", "application/json");
http.onreadystatechange = function() {
if (http.readyState == 4) {
pinning = false;
if (http.status == 200) {
callback();
} else if (http.status == 409) {
$pinBtn.innerHTML = 'pin';
alert("Post is synced to another account. Delete the post from that account instead.");
// TODO: show "remove" button instead of "delete" now
// Persist that state.
// Have it remove the post locally only.
} else {
$pinBtn.innerHTML = 'pin';
alert("Failed to pin." + (http.status>=500?" Please try again.":""));
}
}
}
http.send(JSON.stringify(params));
};
{{end}}
try { // Fonts
WebFontConfig = {
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans:400,700:latin' ], urls: [ '{{if .IsCustomDomain}}https://files.writeas.org{{end}}/css/fonts.css' ] }
};
(function() {
var wf = document.createElement('script');
wf.src = '{{if .IsCustomDomain}}https://files.writeas.org{{end}}/js/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
})();
} catch (e) { /* ¯\_(ツ)_/¯ */ }
</script>
</html>{{end}}
diff --git a/templates/collection.tmpl b/templates/collection.tmpl
index a3fe8f6..54f8b2f 100644
--- a/templates/collection.tmpl
+++ b/templates/collection.tmpl
@@ -1,247 +1,239 @@
{{define "collection"}}<!DOCTYPE HTML>
<html {{if .Language}}lang="{{.Language}}"{{end}} dir="{{.Direction}}">
<head>
<meta charset="utf-8">
<title>{{.DisplayTitle}}{{if not .SingleUser}} &mdash; {{.SiteName}}{{end}}</title>
<link rel="stylesheet" type="text/css" href="/css/write.css" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="canonical" href="{{.CanonicalURL}}">
{{if gt .CurrentPage 1}}<link rel="prev" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}}
{{if lt .CurrentPage .TotalPages}}<link rel="next" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}}
{{if not .IsPrivate}}<link rel="alternate" type="application/rss+xml" title="{{.DisplayTitle}} &raquo; Feed" href="{{.CanonicalURL}}feed/" />{{end}}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content="Write Freely">
<meta name="description" content="{{.Description}}">
<meta itemprop="name" content="{{.DisplayTitle}}">
<meta itemprop="description" content="{{.Description}}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{.DisplayTitle}}">
<meta name="twitter:image" content="{{.AvatarURL}}">
<meta name="twitter:description" content="{{.Description}}">
<meta property="og:title" content="{{.DisplayTitle}}" />
<meta property="og:site_name" content="{{.DisplayTitle}}" />
<meta property="og:type" content="article" />
<meta property="og:url" content="{{.CanonicalURL}}" />
<meta property="og:description" content="{{.Description}}" />
<meta property="og:image" content="{{.AvatarURL}}">
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}}
{{if .RenderMathJax}}
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
extensions: ["tex2jax.js"],
jax: ["input/TeX", "output/HTML-CSS"],
tex2jax: {
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
processEscapes: true
},
"HTML-CSS": { fonts: ["TeX"] }
});
</script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML" async></script>{{end}}
- {{if .CodeHighlight}}
- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/atom-one-light.min.css" />
- <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js"></script>
- <script>
- addEventListener('load', function () {
- var x = document.querySelectorAll("code[class^='language-']");
- for (i=0; i<x.length; i++) {
- hljs.highlightBlock(x[i]);
- }});
- </script>
- {{end}}
+ <!-- Add highlighting logic -->
+ {{template "highlighting" . }}
+
</head>
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
{{if or .IsOwner .SingleUser}}<nav id="manage"><ul>
<li><a onclick="void(0)">&#9776; Menu</a>
<ul>
{{ if .IsOwner }}
{{if .SingleUser}}
<li><a href="/me/new">New Post</a></li>
{{else}}
<li><a href="/#{{.Alias}}" class="write">{{.SiteName}}</a></li>
{{end}}
<li><a href="/me/c/{{.Alias}}">Customize</a></li>
<li><a href="/me/c/{{.Alias}}/stats">Stats</a></li>
<li class="separator"><hr /></li>
{{if not .SingleUser}}<li><a href="/me/c/"><img class="ic-18dp" src="/img/ic_blogs_dark@2x.png" /> View Blogs</a></li>{{end}}
<li><a href="/me/posts/"><img class="ic-18dp" src="/img/ic_list_dark@2x.png" /> View Drafts</a></li>
{{ else }}
<li><a href="/login">Log in</a></li>
{{ end }}
</ul>
</li>
</ul></nav>{{end}}
<header>
<h1 dir="{{.Direction}}" id="blog-title">{{if .Posts}}{{else}}<span class="writeas-prefix"><a href="/">write.as</a></span> {{end}}<a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
{{/*if not .Public/*}}
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p-->
{{/*end*/}}
{{if .PinnedPosts}}<nav>
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
{{end}}
</header>
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
{{if .IsWelcome}}
<div id="welcome">
<h2>Welcome, <strong>{{.Username}}</strong>!</h2>
<p>This is your new blog.</p>
<p><a class="simple-cta" href="/#{{.Alias}}">Start writing</a>, or <a class="simple-cta" href="/me/c/{{.Alias}}">customize</a> your blog.</p>
<p>Check out our <a class="simple-cta" href="https://guides.write.as/writing/?pk_campaign=welcome">writing guide</a> to see what else you can do, and <a class="simple-cta" href="/contact">get in touch</a> anytime with questions or feedback.</p>
</div>
{{end}}
{{template "posts" .}}
{{if gt .TotalPages 1}}<nav id="paging" class="content-container clearfix">
{{if or (and .Format.Ascending (lt .CurrentPage .TotalPages)) (isRTL .Direction)}}
{{if gt .CurrentPage 1}}<a href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">&#8672; {{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Previous{{else}}Newer{{end}}</a>{{end}}
{{if lt .CurrentPage .TotalPages}}<a style="float:right;" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Next{{else}}Older{{end}} &#8674;</a>{{end}}
{{else}}
{{if lt .CurrentPage .TotalPages}}<a href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">&#8672; Older</a>{{end}}
{{if gt .CurrentPage 1}}<a style="float:right;" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">Newer &#8674;</a>{{end}}
{{end}}
</nav>{{end}}
{{if .Posts}}</section>{{else}}</div>{{end}}
{{if .ShowFooterBranding }}
<footer>
<hr />
<nav dir="ltr">
{{if not .SingleUser}}<a class="home pubd" href="/">{{.SiteName}}</a> &middot; {{end}}powered by <a style="margin-left:0" href="https://writefreely.org">write freely</a>
</nav>
</footer>
{{ end }}
</body>
{{if .CanShowScript}}
{{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
{{if .Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}}
{{end}}
<script src="/js/h.js"></script>
<script src="/js/postactions.js"></script>
<script type="text/javascript">
var deleting = false;
function delPost(e, id, owned) {
e.preventDefault();
if (deleting) {
return;
}
// TODO: UNDO!
if (window.confirm('Are you sure you want to delete this post?')) {
// AJAX
deletePost(id, "", function() {
// Remove post from list
var $postEl = document.getElementById('post-' + id);
$postEl.parentNode.removeChild($postEl);
// TODO: add next post from this collection at the bottom
});
}
}
var deletePost = function(postID, token, callback) {
deleting = true;
var $delBtn = document.getElementById('post-' + postID).getElementsByClassName('delete action')[0];
$delBtn.innerHTML = '...';
var http = new XMLHttpRequest();
var url = "/api/posts/" + postID;
http.open("DELETE", url, true);
http.onreadystatechange = function() {
if (http.readyState == 4) {
deleting = false;
if (http.status == 204) {
callback();
} else if (http.status == 409) {
$delBtn.innerHTML = 'delete';
alert("Post is synced to another account. Delete the post from that account instead.");
// TODO: show "remove" button instead of "delete" now
// Persist that state.
// Have it remove the post locally only.
} else {
$delBtn.innerHTML = 'delete';
alert("Failed to delete." + (http.status>=500?" Please try again.":""));
}
}
}
http.send();
};
var pinning = false;
function pinPost(e, postID, slug, title) {
e.preventDefault();
if (pinning) {
return;
}
pinning = true;
var callback = function() {
// Visibly remove post from collection
var $postEl = document.getElementById('post-' + postID);
$postEl.parentNode.removeChild($postEl);
var $header = document.getElementsByTagName('header')[0];
var $pinnedNavs = $header.getElementsByTagName('nav');
// Add link to nav
var link = '<a class="pinned" href="/{{.Alias}}/'+slug+'">'+title+'</a>';
if ($pinnedNavs.length == 0) {
$header.insertAdjacentHTML("beforeend", '<nav>'+link+'</nav>');
} else {
$pinnedNavs[0].insertAdjacentHTML("beforeend", link);
}
};
var $pinBtn = document.getElementById('post-' + postID).getElementsByClassName('pin action')[0];
$pinBtn.innerHTML = '...';
var http = new XMLHttpRequest();
var url = "/api/collections/{{.Alias}}/pin";
var params = [ { "id": postID } ];
http.open("POST", url, true);
http.setRequestHeader("Content-type", "application/json");
http.onreadystatechange = function() {
if (http.readyState == 4) {
pinning = false;
if (http.status == 200) {
callback();
} else if (http.status == 409) {
$pinBtn.innerHTML = 'pin';
alert("Post is synced to another account. Delete the post from that account instead.");
// TODO: show "remove" button instead of "delete" now
// Persist that state.
// Have it remove the post locally only.
} else {
$pinBtn.innerHTML = 'pin';
alert("Failed to pin." + (http.status>=500?" Please try again.":""));
}
}
}
http.send(JSON.stringify(params));
};
try {
WebFontConfig = {
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans:400,700:latin' ], urls: [ '/css/fonts.css' ] }
};
(function() {
var wf = document.createElement('script');
wf.src = '/js/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
})();
} catch (e) {}
</script>
</html>{{end}}
diff --git a/templates/include/render.tmpl b/templates/include/render.tmpl
new file mode 100644
index 0000000..5a0eb8e
--- /dev/null
+++ b/templates/include/render.tmpl
@@ -0,0 +1,14 @@
+<!-- Miscelaneous render related template parts we use multiple times -->
+{{define "highlighting"}}
+ <!-- TODO: make this conditional on presence of code blocks -->
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/atom-one-light.min.css"/>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js" type="text/javascript"></script>
+
+ <script>
+ addEventListener('load', function () {
+ var x = document.querySelectorAll("code[class^='language-']");
+ for (i=0; i<x.length; i++) {
+ hljs.highlightBlock(x[i]);
+ }});
+ </script>
+{{end}}
diff --git a/templates/post.tmpl b/templates/post.tmpl
index 9466ae2..962f795 100644
--- a/templates/post.tmpl
+++ b/templates/post.tmpl
@@ -1,113 +1,100 @@
{{define "post"}}<!DOCTYPE HTML>
<html {{if .Language}}lang="{{.Language}}"{{end}} dir="{{.Direction}}">
<head prefix="og: http://ogp.me/ns#">
<meta charset="utf-8">
<title>{{if .Title}}{{.Title}}{{else}}{{.GenTitle}}{{end}} &mdash; {{.SiteName}}</title>
{{if .IsCode}}
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/mono-blue.min.css">
{{end}}
<link rel="stylesheet" type="text/css" href="/css/write.css" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="canonical" href="{{.Host}}/{{.ID}}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content="{{.SiteName}}">
<meta name="title" content="{{if .Title}}{{.Title}}{{else}}{{.GenTitle}}{{end}}">
<meta name="description" content="{{.Description}}">
<meta itemprop="name" content="{{.SiteName}}">
<meta itemprop="description" content="{{.Description}}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{if .Title}}{{.Title}}{{else}}{{.GenTitle}}{{end}}">
<meta name="twitter:description" content="{{.Description}}">
{{if gt .Views 1}}<meta name="twitter:label1" value="Views">
<meta name="twitter:data1" value="{{largeNumFmt .Views}}">{{end}}
<meta name="twitter:image" content="https://write.as/img/w-sq-light.png">
<meta property="og:title" content="{{if .Title}}{{.Title}}{{else}}{{.GenTitle}}{{end}}" />
<meta property="og:site_name" content="{{.SiteName}}" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://write.as/{{.ID}}" />
<meta property="og:description" content="{{.Description}}" />
<meta property="og:image" content="https://write.as/img/w-sq-light.png">
{{if .Author}}<meta property="article:author" content="https://{{.Author}}" />{{end}}
-
- <!-- Collection not known during single User draft? this test feels wrong -->
- {{if not .SingleUser }}
- {{if .Collection.CodeHighlight}}
- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/atom-one-light.min.css" />
- <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js"></script>
- <script>
- addEventListener('load', function () {
- var x = document.querySelectorAll("code[class^='language-']");
- for (i=0; i<x.length; i++) {
- hljs.highlightBlock(x[i]);
- }});
- </script>
- {{end}}
- {{end}}
+ <!-- Add highlighting logic -->
+ {{template "highlighting" .}}
</head>
<body id="post">
<header>
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1>
<nav>
<span class="views{{if not .IsOwner}} owner-visible{{end}}" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span>
{{if .IsCode}}<a href="/{{.ID}}.txt" rel="noindex" dir="{{.Direction}}">View raw</a>{{end}}
{{ if .Username }}
{{if .IsOwner}}
<a href="/{{if .SingleUser}}d/{{end}}{{.ID}}/edit" dir="{{.Direction}}">Edit</a>
{{end}}
<a class="xtra-feature dash-nav" href="/me/posts/" dir="{{.Direction}}">Drafts</a>
{{ end }}
</nav>
</header>
<article class="{{.Font}} h-entry">{{if .Title}}<h2 id="title" class="p-name">{{.Title}}</h2>{{end}}{{ if .IsPlainText }}<p id="post-body" class="e-content">{{.Content}}</p>{{ else }}<div id="post-body" class="e-content">{{.HTMLContent}}</div>{{ end }}</article>
<footer dir="ltr"><hr><nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language}}</p></nav></footer>
</body>
{{if .IsCode}}
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js"></script>
<script>
hljs.highlightBlock(document.getElementById('post-body'));
</script>
{{else}}
<script src="/js/h.js"></script>
{{if .IsPlainText}}<script src="/js/twitter-text.min.js"></script>{{end}}
{{end}}
<script type="text/javascript">
try {
WebFontConfig = {
custom: { families: [ 'Lora:400,700:latin'{{if eq .Font "sans"}}, 'Open+Sans:400,700:latin'{{end}} ], urls: [ '/css/fonts.css' ] }
};
(function() {
var wf = document.createElement('script');
wf.src = '/js/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
})();
} catch (e) { /* ¯\_(ツ)_/¯ */ }
var posts = localStorage.getItem('posts');
if (posts != null) {
posts = JSON.parse(posts);
var $nav = document.getElementsByTagName('nav')[0];
for (var i=0; i<posts.length; i++) {
if (posts[i].id == "{{.ID}}") {
$nav.innerHTML = $nav.innerHTML + '<a class="xtra-feature" href="/edit/{{.ID}}" dir="{{.Direction}}">Edit</a>';
var $ownerVis = document.querySelectorAll('.owner-visible');
for (var i=0; i<$ownerVis.length; i++) {
$ownerVis[i].classList.remove('owner-visible');
}
break;
}
}
}
</script>
</html>{{end}}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jan 31, 5:54 PM (1 d, 2 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3145331

Event Timeline