FEATURE: Support [description|attachment](upload://<short-sha>) in MD. (#7603)

This commit is contained in:
Guo Xiang Tan
2019-05-28 23:18:21 +08:00
committed by Penar Musaraj
parent 42818b810e
commit b1d3c678ca
25 changed files with 574 additions and 282 deletions

View File

@@ -36,7 +36,7 @@ import {
import {
cacheShortUploadUrl,
resolveAllShortUrls
} from "pretty-text/image-short-url";
} from "pretty-text/upload-short-url";
import {
INLINE_ONEBOX_LOADING_CSS_CLASS,

View File

@@ -13,7 +13,7 @@ const CookText = Ember.Component.extend({
// pretty text may only be loaded now
Ember.run.next(() =>
window
.requireModule("pretty-text/image-short-url")
.requireModule("pretty-text/upload-short-url")
.resolveAllShortUrls(ajax)
);
});

View File

@@ -444,15 +444,9 @@ export function getUploadMarkdown(upload) {
) {
return uploadLocation(upload.url);
} else {
return (
'<a class="attachment" href="' +
upload.url +
'">' +
upload.original_filename +
"</a> (" +
I18n.toHumanSize(upload.filesize) +
")\n"
);
return `[${upload.original_filename} (${I18n.toHumanSize(
upload.filesize
)})|attachment](${upload.short_url})`;
}
}

View File

@@ -14,6 +14,6 @@
//= require ./pretty-text/engines/discourse-markdown/newline
//= require ./pretty-text/engines/discourse-markdown/html-img
//= require ./pretty-text/engines/discourse-markdown/text-post-process
//= require ./pretty-text/engines/discourse-markdown/image-protocol
//= require ./pretty-text/engines/discourse-markdown/upload-protocol
//= require ./pretty-text/engines/discourse-markdown/inject-line-number
//= require ./pretty-text/engines/discourse-markdown/d-wrap

View File

@@ -11,4 +11,4 @@
//= require ./pretty-text/sanitizer
//= require ./pretty-text/oneboxer
//= require ./pretty-text/inline-oneboxer
//= require ./pretty-text/image-short-url
//= require ./pretty-text/upload-short-url

View File

@@ -1,6 +1,7 @@
import { default as WhiteLister } from "pretty-text/white-lister";
import { sanitize } from "pretty-text/sanitizer";
import guid from "pretty-text/guid";
import { ATTACHMENT_CSS_CLASS } from "pretty-text/upload-short-url";
function deprecate(feature, name) {
return function() {
@@ -187,6 +188,26 @@ function setupImageDimensions(md) {
md.renderer.rules.image = renderImage;
}
function renderAttachment(tokens, idx, options, env, slf) {
const linkOpenToken = tokens[idx];
const linkTextToken = tokens[idx + 1];
const split = linkTextToken.content.split("|");
const isValid = !linkOpenToken.attrs[
linkOpenToken.attrIndex("data-orig-href")
];
if (isValid && split.length === 2 && split[1] === ATTACHMENT_CSS_CLASS) {
linkOpenToken.attrs.unshift(["class", split[1]]);
linkTextToken.content = split[0];
}
return slf.renderToken(tokens, idx, options);
}
function setupAttachments(md) {
md.renderer.rules.link_open = renderAttachment;
}
let Helpers;
export function setup(opts, siteSettings, state) {
@@ -276,6 +297,7 @@ export function setup(opts, siteSettings, state) {
setupUrlDecoding(opts.engine);
setupHoister(opts.engine);
setupImageDimensions(opts.engine);
setupAttachments(opts.engine);
setupBlockBBCode(opts.engine);
setupInlineBBCode(opts.engine);
setupTextPostProcessRuler(opts.engine);

View File

@@ -1,62 +0,0 @@
// add image to array if src has an upload
function addImage(images, token) {
if (token.attrs) {
for (let i = 0; i < token.attrs.length; i++) {
if (token.attrs[i][1].indexOf("upload://") === 0) {
images.push([token, i]);
break;
}
}
}
}
function rule(state) {
let images = [];
for (let i = 0; i < state.tokens.length; i++) {
let blockToken = state.tokens[i];
if (blockToken.tag === "img") {
addImage(images, blockToken);
}
if (!blockToken.children) {
continue;
}
for (let j = 0; j < blockToken.children.length; j++) {
let token = blockToken.children[j];
if (token.tag === "img") {
addImage(images, token);
}
}
}
if (images.length > 0) {
let srcList = images.map(([token, srcIndex]) => token.attrs[srcIndex][1]);
let lookup = state.md.options.discourse.lookupImageUrls;
let longUrls = (lookup && lookup(srcList)) || {};
images.forEach(([token, srcIndex]) => {
let origSrc = token.attrs[srcIndex][1];
let mapped = longUrls[origSrc];
if (mapped) {
token.attrs[srcIndex][1] = mapped;
} else {
token.attrs[srcIndex][1] = state.md.options.discourse.getURL(
"/images/transparent.png"
);
token.attrs.push(["data-orig-src", origSrc]);
}
});
}
}
export function setup(helper) {
const opts = helper.getOptions();
if (opts.previewing) helper.whiteList(["img.resizable"]);
helper.whiteList(["img[data-orig-src]"]);
helper.registerPlugin(md => {
md.core.ruler.push("image-protocol", rule);
});
}

View File

@@ -0,0 +1,81 @@
// add image to array if src has an upload
function addImage(uploads, token) {
if (token.attrs) {
for (let i = 0; i < token.attrs.length; i++) {
if (token.attrs[i][1].indexOf("upload://") === 0) {
uploads.push([token, i]);
break;
}
}
}
}
function rule(state) {
let uploads = [];
for (let i = 0; i < state.tokens.length; i++) {
let blockToken = state.tokens[i];
if (blockToken.tag === "img" || blockToken.tag === "a") {
addImage(uploads, blockToken);
}
if (!blockToken.children) {
continue;
}
for (let j = 0; j < blockToken.children.length; j++) {
let token = blockToken.children[j];
if (token.tag === "img" || token.tag === "a") {
addImage(uploads, token);
}
}
}
if (uploads.length > 0) {
let srcList = uploads.map(([token, srcIndex]) => token.attrs[srcIndex][1]);
let lookup = state.md.options.discourse.lookupUploadUrls;
let longUrls = (lookup && lookup(srcList)) || {};
uploads.forEach(([token, srcIndex]) => {
let origSrc = token.attrs[srcIndex][1];
let mapped = longUrls[origSrc];
switch (token.tag) {
case "img":
if (mapped) {
token.attrs[srcIndex][1] = mapped.url;
} else {
token.attrs[srcIndex][1] = state.md.options.discourse.getURL(
"/images/transparent.png"
);
token.attrs.push(["data-orig-src", origSrc]);
}
break;
case "a":
if (mapped) {
token.attrs[srcIndex][1] = mapped.short_path;
} else {
token.attrs[srcIndex][1] = state.md.options.discourse.getURL(
"/404"
);
token.attrs.push(["data-orig-href", origSrc]);
}
break;
}
});
}
}
export function setup(helper) {
const opts = helper.getOptions();
if (opts.previewing) helper.whiteList(["img.resizable"]);
helper.whiteList(["img[data-orig-src]", "a[data-orig-href]"]);
helper.registerPlugin(md => {
md.core.ruler.push("upload-protocol", rule);
});
}

View File

@@ -1,68 +0,0 @@
let _cache = {};
export function lookupCachedUploadUrl(shortUrl) {
return _cache[shortUrl];
}
export function lookupUncachedUploadUrls(urls, ajax) {
return ajax("/uploads/lookup-urls", {
method: "POST",
data: { short_urls: urls }
}).then(uploads => {
uploads.forEach(upload =>
cacheShortUploadUrl(upload.short_url, upload.url)
);
urls.forEach(url =>
cacheShortUploadUrl(url, lookupCachedUploadUrl(url) || "missing")
);
return uploads;
});
}
export function cacheShortUploadUrl(shortUrl, url) {
_cache[shortUrl] = url;
}
export function resetCache() {
_cache = {};
}
function _loadCachedShortUrls($images) {
$images.each((idx, image) => {
const $image = $(image);
const url = lookupCachedUploadUrl($image.data("orig-src"));
if (url) {
$image.removeAttr("data-orig-src");
if (url !== "missing") {
$image.attr("src", url);
}
}
});
}
function _loadShortUrls($images, ajax) {
const urls = $images.toArray().map(img => $(img).data("orig-src"));
return lookupUncachedUploadUrls(urls, ajax).then(() =>
_loadCachedShortUrls($images)
);
}
export function resolveAllShortUrls(ajax) {
let $shortUploadUrls = $("img[data-orig-src]");
if ($shortUploadUrls.length > 0) {
_loadCachedShortUrls($shortUploadUrls);
$shortUploadUrls = $("img[data-orig-src]");
if ($shortUploadUrls.length > 0) {
// this is carefully batched so we can do a leading debounce (trigger right away)
return Ember.run.debounce(
null,
() => _loadShortUrls($shortUploadUrls, ajax),
450,
true
);
}
}
}

View File

@@ -26,7 +26,7 @@ export function buildOptions(state) {
lookupPrimaryUserGroupByPostNumber,
formatUsername,
emojiUnicodeReplacer,
lookupImageUrls,
lookupUploadUrls,
previewing,
linkify,
censoredWords
@@ -65,7 +65,7 @@ export function buildOptions(state) {
lookupPrimaryUserGroupByPostNumber,
formatUsername,
emojiUnicodeReplacer,
lookupImageUrls,
lookupUploadUrls,
censoredWords,
allowedHrefSchemes: siteSettings.allowed_href_schemes
? siteSettings.allowed_href_schemes.split("|")

View File

@@ -0,0 +1,111 @@
let _cache = {};
export function lookupCachedUploadUrl(shortUrl) {
return _cache[shortUrl] || {};
}
const MISSING = "missing";
export function lookupUncachedUploadUrls(urls, ajax) {
return ajax("/uploads/lookup-urls", {
method: "POST",
data: { short_urls: urls }
}).then(uploads => {
uploads.forEach(upload => {
cacheShortUploadUrl(upload.short_url, {
url: upload.url,
short_path: upload.short_path
});
});
urls.forEach(url =>
cacheShortUploadUrl(url, {
url: lookupCachedUploadUrl(url).url || MISSING,
short_path: lookupCachedUploadUrl(url).short_path || MISSING
})
);
return uploads;
});
}
export function cacheShortUploadUrl(shortUrl, value) {
_cache[shortUrl] = value;
}
export function resetCache() {
_cache = {};
}
export const ATTACHMENT_CSS_CLASS = "attachment";
function _loadCachedShortUrls($uploads) {
$uploads.each((idx, upload) => {
const $upload = $(upload);
let url;
switch (upload.tagName) {
case "A":
url = lookupCachedUploadUrl($upload.data("orig-href")).short_path;
if (url) {
$upload.removeAttr("data-orig-href");
if (url !== MISSING) {
$upload.attr("href", url);
const content = $upload.text().split("|");
if (content[1] === ATTACHMENT_CSS_CLASS) {
$upload.addClass(ATTACHMENT_CSS_CLASS);
$upload.text(content[0]);
}
}
}
break;
case "IMG":
url = lookupCachedUploadUrl($upload.data("orig-src")).url;
if (url) {
$upload.removeAttr("data-orig-src");
if (url !== MISSING) {
$upload.attr("src", url);
}
}
break;
}
});
}
function _loadShortUrls($uploads, ajax) {
const urls = $uploads.toArray().map(upload => {
const $upload = $(upload);
return $upload.data("orig-src") || $upload.data("orig-href");
});
return lookupUncachedUploadUrls(urls, ajax).then(() =>
_loadCachedShortUrls($uploads)
);
}
export function resolveAllShortUrls(ajax) {
const attributes = "img[data-orig-src], a[data-orig-href]";
let $shortUploadUrls = $(attributes);
if ($shortUploadUrls.length > 0) {
_loadCachedShortUrls($shortUploadUrls);
$shortUploadUrls = $(attributes);
if ($shortUploadUrls.length > 0) {
// this is carefully batched so we can do a leading debounce (trigger right away)
return Ember.run.debounce(
null,
() => _loadShortUrls($shortUploadUrls, ajax),
450,
true
);
}
}
}