mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 08:43:25 +08:00
DEV: Extend plugin API for uploads (#8440)
* DEV: Add API to alter uploads Markdown * DEV: Extract data attributes from image / download Markdown For example '[test|attachment|hello=world]' will generate an 'a' element with a data attribute: 'data-hello=world'. This commit also makes MarkdownIt to transform '|attachment' into 'class="attachment"'. This transformation used to be a part of the process which resolves short URLs (i.e. upload://). * DEV: Export imageNameFromFileName
This commit is contained in:
parent
ebe6fa95be
commit
aa24be1a9a
|
@ -64,6 +64,11 @@ export function addComposerUploadHandler(extensions, method) {
|
|||
});
|
||||
}
|
||||
|
||||
const uploadMarkdownResolvers = [];
|
||||
export function addComposerUploadMarkdownResolver(resolver) {
|
||||
uploadMarkdownResolvers.push(resolver);
|
||||
}
|
||||
|
||||
export default Component.extend({
|
||||
classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"],
|
||||
|
||||
|
@ -745,7 +750,11 @@ export default Component.extend({
|
|||
let upload = data.result;
|
||||
this._setUploadPlaceholderDone(data);
|
||||
if (!this._xhr || !this._xhr._userCancelled) {
|
||||
const markdown = getUploadMarkdown(upload);
|
||||
const markdown = uploadMarkdownResolvers.reduce(
|
||||
(md, resolver) => resolver(upload) || md,
|
||||
getUploadMarkdown(upload)
|
||||
);
|
||||
|
||||
cacheShortUploadUrl(upload.short_url, upload.url);
|
||||
this.appEvents.trigger(
|
||||
"composer:replace-text",
|
||||
|
|
|
@ -40,7 +40,10 @@ import { registerCustomAvatarHelper } from "discourse/helpers/user-avatar";
|
|||
import { disableNameSuppression } from "discourse/widgets/poster-name";
|
||||
import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic";
|
||||
import Sharing from "discourse/lib/sharing";
|
||||
import { addComposerUploadHandler } from "discourse/components/composer-editor";
|
||||
import {
|
||||
addComposerUploadHandler,
|
||||
addComposerUploadMarkdownResolver
|
||||
} from "discourse/components/composer-editor";
|
||||
import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
|
||||
import { queryRegistry } from "discourse/widgets/widget";
|
||||
import Composer from "discourse/models/composer";
|
||||
|
@ -867,6 +870,19 @@ class PluginApi {
|
|||
addComposerUploadHandler(extensions, method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a function to generate Markdown after a file has been uploaded.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* api.addComposerUploadMarkdownResolver(upload => {
|
||||
* return `_uploaded ${upload.original_filename}_`;
|
||||
* })
|
||||
*/
|
||||
addComposerUploadMarkdownResolver(resolver) {
|
||||
addComposerUploadMarkdownResolver(resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a "beforeSave" function on the composer. This allows you to
|
||||
* implement custom logic that will happen before the user makes a post.
|
||||
|
|
|
@ -124,64 +124,73 @@ function setupHoister(md) {
|
|||
md.renderer.rules.html_raw = renderHoisted;
|
||||
}
|
||||
|
||||
export function extractDataAttribute(str) {
|
||||
let sep = str.indexOf("=");
|
||||
if (sep === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = `data-${str.substr(0, sep)}`.toLowerCase();
|
||||
if (!/^[A-Za-z]+[\w\-\:\.]*$/.test(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = str.substr(sep + 1);
|
||||
return [key, value];
|
||||
}
|
||||
|
||||
const IMG_SIZE_REGEX = /^([1-9]+[0-9]*)x([1-9]+[0-9]*)(\s*,\s*(x?)([1-9][0-9]{0,2}?)([%x]?))?$/;
|
||||
function renderImage(tokens, idx, options, env, slf) {
|
||||
var token = tokens[idx];
|
||||
const token = tokens[idx];
|
||||
const alt = slf.renderInlineAsText(token.children, options, env);
|
||||
|
||||
let alt = slf.renderInlineAsText(token.children, options, env);
|
||||
const split = alt.split("|");
|
||||
const altSplit = [];
|
||||
|
||||
let split = alt.split("|");
|
||||
if (split.length > 1) {
|
||||
let match;
|
||||
let info = split.splice(split.length - 1)[0];
|
||||
for (let i = 0, match, data; i < split.length; ++i) {
|
||||
if ((match = split[i].match(IMG_SIZE_REGEX)) && match[1] && match[2]) {
|
||||
let width = match[1];
|
||||
let height = match[2];
|
||||
|
||||
if ((match = info.match(IMG_SIZE_REGEX))) {
|
||||
if (match[1] && match[2]) {
|
||||
alt = split.join("|");
|
||||
|
||||
let width = match[1];
|
||||
let height = match[2];
|
||||
|
||||
// calculate using percentage
|
||||
if (match[5] && match[6] && match[6] === "%") {
|
||||
let percent = parseFloat(match[5]) / 100.0;
|
||||
width = parseInt(width * percent, 10);
|
||||
height = parseInt(height * percent, 10);
|
||||
}
|
||||
|
||||
// calculate using only given width
|
||||
if (match[5] && match[6] && match[6] === "x") {
|
||||
let wr = parseFloat(match[5]) / width;
|
||||
width = parseInt(match[5], 10);
|
||||
height = parseInt(height * wr, 10);
|
||||
}
|
||||
|
||||
// calculate using only given height
|
||||
if (match[5] && match[4] && match[4] === "x" && !match[6]) {
|
||||
let hr = parseFloat(match[5]) / height;
|
||||
height = parseInt(match[5], 10);
|
||||
width = parseInt(width * hr, 10);
|
||||
}
|
||||
|
||||
if (token.attrIndex("width") === -1) {
|
||||
token.attrs.push(["width", width]);
|
||||
}
|
||||
|
||||
if (token.attrIndex("height") === -1) {
|
||||
token.attrs.push(["height", height]);
|
||||
}
|
||||
|
||||
if (
|
||||
options.discourse.previewing &&
|
||||
match[6] !== "x" &&
|
||||
match[4] !== "x"
|
||||
)
|
||||
token.attrs.push(["class", "resizable"]);
|
||||
// calculate using percentage
|
||||
if (match[5] && match[6] && match[6] === "%") {
|
||||
let percent = parseFloat(match[5]) / 100.0;
|
||||
width = parseInt(width * percent, 10);
|
||||
height = parseInt(height * percent, 10);
|
||||
}
|
||||
|
||||
// calculate using only given width
|
||||
if (match[5] && match[6] && match[6] === "x") {
|
||||
let wr = parseFloat(match[5]) / width;
|
||||
width = parseInt(match[5], 10);
|
||||
height = parseInt(height * wr, 10);
|
||||
}
|
||||
|
||||
// calculate using only given height
|
||||
if (match[5] && match[4] && match[4] === "x" && !match[6]) {
|
||||
let hr = parseFloat(match[5]) / height;
|
||||
height = parseInt(match[5], 10);
|
||||
width = parseInt(width * hr, 10);
|
||||
}
|
||||
|
||||
if (token.attrIndex("width") === -1) {
|
||||
token.attrs.push(["width", width]);
|
||||
}
|
||||
|
||||
if (token.attrIndex("height") === -1) {
|
||||
token.attrs.push(["height", height]);
|
||||
}
|
||||
|
||||
if (options.discourse.previewing && match[6] !== "x" && match[4] !== "x")
|
||||
token.attrs.push(["class", "resizable"]);
|
||||
} else if ((data = extractDataAttribute(split[i]))) {
|
||||
token.attrs.push(data);
|
||||
} else {
|
||||
altSplit.push(split[i]);
|
||||
}
|
||||
}
|
||||
|
||||
token.attrs[token.attrIndex("alt")][1] = alt;
|
||||
token.attrs[token.attrIndex("alt")][1] = altSplit.join("|");
|
||||
return slf.renderToken(tokens, idx, options);
|
||||
}
|
||||
|
||||
|
@ -190,16 +199,24 @@ function setupImageDimensions(md) {
|
|||
}
|
||||
|
||||
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")
|
||||
];
|
||||
const linkToken = tokens[idx];
|
||||
const textToken = tokens[idx + 1];
|
||||
|
||||
if (isValid && split.length === 2 && split[1] === ATTACHMENT_CSS_CLASS) {
|
||||
linkOpenToken.attrs.unshift(["class", split[1]]);
|
||||
linkTextToken.content = split[0];
|
||||
const split = textToken.content.split("|");
|
||||
const contentSplit = [];
|
||||
|
||||
for (let i = 0, data; i < split.length; ++i) {
|
||||
if (split[i] === ATTACHMENT_CSS_CLASS) {
|
||||
linkToken.attrs.unshift(["class", split[i]]);
|
||||
} else if ((data = extractDataAttribute(split[i]))) {
|
||||
linkToken.attrs.push(data);
|
||||
} else {
|
||||
contentSplit.push(split[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentSplit.length > 0) {
|
||||
textToken.content = contentSplit.join("|");
|
||||
}
|
||||
|
||||
return slf.renderToken(tokens, idx, options);
|
||||
|
|
|
@ -53,8 +53,11 @@ function _loadCachedShortUrls($uploads) {
|
|||
|
||||
if (url !== MISSING) {
|
||||
$upload.attr("href", url);
|
||||
const content = $upload.text().split("|");
|
||||
|
||||
// Replace "|attachment" with class='attachment'
|
||||
// TODO: This is a part of the cooking process now and should be
|
||||
// removed in the future.
|
||||
const content = $upload.text().split("|");
|
||||
if (content[1] === ATTACHMENT_CSS_CLASS) {
|
||||
$upload.addClass(ATTACHMENT_CSS_CLASS);
|
||||
$upload.text(content[0]);
|
||||
|
|
|
@ -115,7 +115,7 @@ export default class WhiteLister {
|
|||
|
||||
// Only add to `default` when you always want your whitelist to occur. In other words,
|
||||
// don't change this for a plugin or a feature that can be disabled
|
||||
const DEFAULT_LIST = [
|
||||
export const DEFAULT_LIST = [
|
||||
"a.attachment",
|
||||
"a.hashtag",
|
||||
"a.mention",
|
||||
|
|
|
@ -1450,7 +1450,7 @@ HTML
|
|||
|
||||
cooked = <<~HTML
|
||||
<p><img src="/images/transparent.png" alt="upload" data-orig-src="upload://abcABC.png"></p>
|
||||
<p><a href="/404" data-orig-href="upload://abcdefg.png">some attachment|attachment</a></p>
|
||||
<p><a class="attachment" href="/404" data-orig-href="upload://abcdefg.png">some attachment</a></p>
|
||||
HTML
|
||||
|
||||
expect(PrettyText.cook(raw)).to eq(cooked.strip)
|
||||
|
|
|
@ -34,6 +34,6 @@ QUnit.test("attachments are cooked properly", async assert => {
|
|||
find(".d-editor-preview:visible")
|
||||
.html()
|
||||
.trim(),
|
||||
'<p><a href="/uploads/short-url/asdsad.png" class="attachment">test</a></p>'
|
||||
'<p><a class="attachment" href="/uploads/short-url/asdsad.png">test</a></p>'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
applyCachedInlineOnebox,
|
||||
deleteCachedInlineOnebox
|
||||
} from "pretty-text/inline-oneboxer";
|
||||
import { extractDataAttribute } from "pretty-text/engines/discourse-markdown-it";
|
||||
|
||||
QUnit.module("lib:pretty-text");
|
||||
|
||||
|
@ -1365,3 +1366,11 @@ QUnit.test("emoji - emojiSet", assert => {
|
|||
`<p><img src="/images/emoji/twitter/smile.png?v=${v}" title=":smile:" class="emoji" alt=":smile:"></p>`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("extractDataAttribute", assert => {
|
||||
assert.deepEqual(extractDataAttribute("foo="), ["data-foo", ""]);
|
||||
assert.deepEqual(extractDataAttribute("foo=bar"), ["data-foo", "bar"]);
|
||||
|
||||
assert.notOk(extractDataAttribute("foo?=bar"));
|
||||
assert.notOk(extractDataAttribute("https://discourse.org/?q=hello"));
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user