interactive preview added
This commit is contained in:
@@ -9,6 +9,7 @@ status-500 = OMG, Server Exploded.
|
|||||||
created-by = Created by [@chmllr](http://twitter.com/chmllr)
|
created-by = Created by [@chmllr](http://twitter.com/chmllr)
|
||||||
|
|
||||||
loading = Loading...
|
loading = Loading...
|
||||||
|
use-password = User password for editing:
|
||||||
preview = Preview
|
preview = Preview
|
||||||
publish = Publish
|
publish = Publish
|
||||||
published = Published
|
published = Published
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
|||||||
|
(function () {
|
||||||
|
var output, Converter;
|
||||||
|
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
|
||||||
|
output = exports;
|
||||||
|
Converter = require("./Markdown.Converter").Converter;
|
||||||
|
} else {
|
||||||
|
output = window.Markdown;
|
||||||
|
Converter = output.Converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.getSanitizingConverter = function () {
|
||||||
|
var converter = new Converter();
|
||||||
|
converter.hooks.chain("postConversion", sanitizeHtml);
|
||||||
|
converter.hooks.chain("postConversion", balanceTags);
|
||||||
|
return converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeHtml(html) {
|
||||||
|
return html.replace(/<[^>]*>?/gi, sanitizeTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (tags that can be opened/closed) | (tags that stand alone)
|
||||||
|
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
|
||||||
|
// <a href="url..." optional title>|</a>
|
||||||
|
var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
|
||||||
|
|
||||||
|
// <img src="url..." optional width optional height optional alt optional title
|
||||||
|
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
|
||||||
|
|
||||||
|
function sanitizeTag(tag) {
|
||||||
|
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
|
||||||
|
return tag;
|
||||||
|
else
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// attempt to balance HTML tags in the html string
|
||||||
|
/// by removing any unmatched opening or closing tags
|
||||||
|
/// IMPORTANT: we *assume* HTML has *already* been
|
||||||
|
/// sanitized and is safe/sane before balancing!
|
||||||
|
///
|
||||||
|
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
|
||||||
|
/// </summary>
|
||||||
|
function balanceTags(html) {
|
||||||
|
|
||||||
|
if (html == "")
|
||||||
|
return "";
|
||||||
|
|
||||||
|
var re = /<\/?\w+[^>]*(\s|$|>)/g;
|
||||||
|
// convert everything to lower case; this makes
|
||||||
|
// our case insensitive comparisons easier
|
||||||
|
var tags = html.toLowerCase().match(re);
|
||||||
|
|
||||||
|
// no HTML tags present? nothing to do; exit now
|
||||||
|
var tagcount = (tags || []).length;
|
||||||
|
if (tagcount == 0)
|
||||||
|
return html;
|
||||||
|
|
||||||
|
var tagname, tag;
|
||||||
|
var ignoredtags = "<p><img><br><li><hr>";
|
||||||
|
var match;
|
||||||
|
var tagpaired = [];
|
||||||
|
var tagremove = [];
|
||||||
|
var needsRemoval = false;
|
||||||
|
|
||||||
|
// loop through matched tags in forward order
|
||||||
|
for (var ctag = 0; ctag < tagcount; ctag++) {
|
||||||
|
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
|
||||||
|
// skip any already paired tags
|
||||||
|
// and skip tags in our ignore list; assume they're self-closed
|
||||||
|
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tag = tags[ctag];
|
||||||
|
match = -1;
|
||||||
|
|
||||||
|
if (!/^<\//.test(tag)) {
|
||||||
|
// this is an opening tag
|
||||||
|
// search forwards (next tags), look for closing tags
|
||||||
|
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
|
||||||
|
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
|
||||||
|
match = ntag;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match == -1)
|
||||||
|
needsRemoval = tagremove[ctag] = true; // mark for removal
|
||||||
|
else
|
||||||
|
tagpaired[match] = true; // mark paired
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsRemoval)
|
||||||
|
return html;
|
||||||
|
|
||||||
|
// delete all orphaned tags from the string
|
||||||
|
|
||||||
|
var ctag = 0;
|
||||||
|
html = html.replace(re, function (match) {
|
||||||
|
var res = tagremove[ctag] ? "" : match;
|
||||||
|
ctag++;
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
})();
|
||||||
+11
-21
@@ -7,13 +7,12 @@
|
|||||||
; frequently used selectors
|
; frequently used selectors
|
||||||
(def $draft ($ :#draft))
|
(def $draft ($ :#draft))
|
||||||
(def $preview ($ :#preview))
|
(def $preview ($ :#preview))
|
||||||
(def $session-key ($ :#session-key))
|
(def $input-elems ($ :#input-elems))
|
||||||
(def $preview-start-line ($ :#preview-start-line))
|
(def $preview-start-line ($ :#preview-start-line))
|
||||||
|
|
||||||
(defn scroll-to
|
; Markdown Converter & Sanitizer instantiation
|
||||||
"scrolls to the given selector"
|
|
||||||
[$id]
|
(def md-converter (Markdown/getSanitizingConverter))
|
||||||
(anim ($ :body) {:scrollTop ((js->clj (.offset $id)) "top")} 500))
|
|
||||||
|
|
||||||
; try to detect iOS
|
; try to detect iOS
|
||||||
(def ios-detected (.match (.-userAgent js/navigator) "(iPad|iPod|iPhone)"))
|
(def ios-detected (.match (.-userAgent js/navigator) "(iPad|iPod|iPhone)"))
|
||||||
@@ -28,26 +27,17 @@
|
|||||||
(.focus $draft))))
|
(.focus $draft))))
|
||||||
|
|
||||||
; show the preview & publish buttons as soon as the user starts typing.
|
; show the preview & publish buttons as soon as the user starts typing.
|
||||||
(.keypress $draft
|
(.keyup $draft
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(css ($ :#buttons) {:display :block})))
|
(do
|
||||||
|
(show $preview-start-line)
|
||||||
; on a preview button click, transform markdown to html, put it
|
(show $input-elems)
|
||||||
; to the preview layer and scroll to it
|
(inner $preview
|
||||||
(.click ($ :#preview-button)
|
(.makeHtml md-converter (val $draft))))))
|
||||||
(fn [e]
|
|
||||||
(xhr [:post "/preview"]
|
|
||||||
{:draft (val $draft)}
|
|
||||||
(fn [json-map]
|
|
||||||
(let [m (js->clj (JSON/parse json-map))]
|
|
||||||
(do
|
|
||||||
(inner $preview (m "preview"))
|
|
||||||
(show $preview-start-line)
|
|
||||||
(scroll-to $preview-start-line)))))))
|
|
||||||
|
|
||||||
; when the publish button is clicked, compute the hash of the entered text and
|
; when the publish button is clicked, compute the hash of the entered text and
|
||||||
; provided session key and assign to the field session-value
|
; provided session key and assign to the field session-value
|
||||||
(.click ($ :#publish-button)
|
(.click ($ :#publish-button)
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(val ($ :#session-value)
|
(val ($ :#session-value)
|
||||||
(lib/hash #(.charCodeAt % 0) (str (val $draft) (val $session-key))))))
|
(lib/hash #(.charCodeAt % 0) (str (val $draft) (val ($ :#session-key)))))))
|
||||||
|
|||||||
@@ -48,6 +48,8 @@
|
|||||||
(html
|
(html
|
||||||
(javascript-tag "var CLOSURE_NO_DEPS = true;")
|
(javascript-tag "var CLOSURE_NO_DEPS = true;")
|
||||||
(include-js "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js")
|
(include-js "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js")
|
||||||
|
(include-js "/js/pagedown/Markdown.Converter.js")
|
||||||
|
(include-js "/js/pagedown/Markdown.Sanitizer.js")
|
||||||
(include-js "/cljs/main.js")))])))
|
(include-js "/cljs/main.js")))])))
|
||||||
|
|
||||||
(defn layout
|
(defn layout
|
||||||
|
|||||||
@@ -26,8 +26,6 @@
|
|||||||
(def central-element
|
(def central-element
|
||||||
(mixin
|
(mixin
|
||||||
:width (px page-width)
|
:width (px page-width)
|
||||||
:margin-top :5em
|
|
||||||
:margin-bottom :10em
|
|
||||||
:margin-left "auto"
|
:margin-left "auto"
|
||||||
:margin-right "auto"))
|
:margin-right "auto"))
|
||||||
|
|
||||||
@@ -130,6 +128,7 @@
|
|||||||
:margin :2em))
|
:margin :2em))
|
||||||
(rule "article"
|
(rule "article"
|
||||||
central-element
|
central-element
|
||||||
|
:margin-top :5em
|
||||||
:font-family text-fonts
|
:font-family text-fonts
|
||||||
:text-align :justify
|
:text-align :justify
|
||||||
:font-size :1.2em
|
:font-size :1.2em
|
||||||
@@ -153,8 +152,7 @@
|
|||||||
:font-family :Courier
|
:font-family :Courier
|
||||||
:font-size :1em
|
:font-size :1em
|
||||||
:border :none
|
:border :none
|
||||||
:height :500px
|
:height :500px)
|
||||||
:margin-bottom :2em)
|
|
||||||
(rule ".hidden"
|
(rule ".hidden"
|
||||||
:display :none)
|
:display :none)
|
||||||
(rule ".button"
|
(rule ".button"
|
||||||
@@ -166,10 +164,13 @@
|
|||||||
:background background)
|
:background background)
|
||||||
(rule ".central-element"
|
(rule ".central-element"
|
||||||
central-element)
|
central-element)
|
||||||
|
(rule "fieldset"
|
||||||
|
:border :none)
|
||||||
(rule "h1"
|
(rule "h1"
|
||||||
:font-size :2em)
|
:font-size :2em)
|
||||||
(rule ".dashed-line"
|
(rule ".dashed-line"
|
||||||
:border-bottom [:1px :dashed foreground-halftone]
|
:border-bottom [:1px :dashed foreground-halftone]
|
||||||
:margin-bottom :5em)
|
:margin-top :3em
|
||||||
|
:margin-bottom :3em)
|
||||||
(rule "h1, h2, h3, h4"
|
(rule "h1, h2, h3, h4"
|
||||||
:font-family header-fonts))))
|
:font-family header-fonts))))
|
||||||
|
|||||||
@@ -88,19 +88,16 @@
|
|||||||
; New Note Page
|
; New Note Page
|
||||||
(defpage "/new" {}
|
(defpage "/new" {}
|
||||||
(layout {:js true} (get-message :new-note)
|
(layout {:js true} (get-message :new-note)
|
||||||
[:div.central-element
|
[:article#preview " "]
|
||||||
|
[:div#preview-start-line.dashed-line.hidden]
|
||||||
|
[:div.central-element {:style "margin-bottom: 3em"}
|
||||||
(form-to [:post "/post-note"]
|
(form-to [:post "/post-note"]
|
||||||
(hidden-field :session-key (create-session))
|
(hidden-field :session-key (create-session))
|
||||||
(hidden-field {:id :session-value} :session-value)
|
(hidden-field {:id :session-value} :session-value)
|
||||||
(text-area {:class :max-width} :draft (get-message :loading))
|
(text-area {:class :max-width} :draft (get-message :loading))
|
||||||
[:div#buttons.hidden
|
[:fieldset#input-elems.hidden
|
||||||
(submit-button {:style "float: left"
|
(submit-button {:class "button ui-border"
|
||||||
:class "button ui-border"
|
:id :publish-button} (get-message :publish))])]))
|
||||||
:id :publish-button} (get-message :publish))
|
|
||||||
[:button#preview-button.button.ui-border {:type :button
|
|
||||||
:style "float: right"} (get-message :preview)]])]
|
|
||||||
[:div#preview-start-line.dashed-line.hidden]
|
|
||||||
[:article#preview]))
|
|
||||||
|
|
||||||
; Displays the note
|
; Displays the note
|
||||||
(defpage "/:year/:month/:day/:title" {:keys [year month day title theme header-font text-font] :as params}
|
(defpage "/:year/:month/:day/:title" {:keys [year month day title theme header-font text-font] :as params}
|
||||||
|
|||||||
Reference in New Issue
Block a user