Skip to content

Commit

Permalink
feat: extended images for custom CSS properties (#1315)
Browse files Browse the repository at this point in the history
* feat: extended images for custom CSS properties

This is a new feature that allows users to extend
images by setting custom CSS properties.
The properties are written with an inline YAML map
(like `{width: 50%, height: 400px}`) placed after the image.
Only certain properties are allowed to avoid any potential abuse.

The extended images work in both preview and publishing.

![](https://i.imgur.com/3KFIsPq.png)

#1273

* spike: allow `display` property
  • Loading branch information
Kaan Genç committed Sep 9, 2021
1 parent 2cb4844 commit f9ed88f
Show file tree
Hide file tree
Showing 11 changed files with 593 additions and 11 deletions.
4 changes: 2 additions & 2 deletions packages/engine-server/src/markdown/remark/dendronPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function plugin(this: Unified.Processor, _opts?: PluginOpts): Transformer {
const proc = this;
function transformer(tree: Node, _file: VFile) {
visit(tree, (node, _index, _parent) => {
if (RemarkUtils.isImage(node)) {
if (RemarkUtils.isImage(node) || RemarkUtils.isExtendedImage(node)) {
return handleImage({ proc, node });
}
});
Expand All @@ -78,7 +78,7 @@ export function dendronHoverPreview(
const proc = this;
function transformer(tree: Node, _file: VFile) {
visit(tree, (node, _index, _parent) => {
if (RemarkUtils.isImage(node)) {
if (RemarkUtils.isImage(node) || RemarkUtils.isExtendedImage(node)) {
// Hover preview can't use API URL's because they are http not https, so we instead have to get the image from disk.
return handleImage({ proc, node, useFullPathUrl: true });
}
Expand Down
21 changes: 20 additions & 1 deletion packages/engine-server/src/markdown/remark/dendronPub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ import {
UserTag,
VaultMissingBehavior,
WikiLinkNoteV4,
ExtendedImage,
} from "../types";
import { MDUtilsV4 } from "../utils";
import { MDUtilsV5, ProcMode } from "../utilsv5";
import { blockAnchor2html } from "./blockAnchors";
import { NoteRefsOpts } from "./noteRefs";
import { convertNoteRefASTV2 } from "./noteRefsV2";
import { extendedImage2html } from "./extendedImage";

type PluginOpts = NoteRefsOpts & {
assetsPrefix?: string;
Expand Down Expand Up @@ -343,7 +345,12 @@ function plugin(this: Unified.Processor, opts?: PluginOpts): Transformer {
return index;
}
}
if (node.type === "image" && dest === DendronASTDest.HTML) {
// The url correction needs to happen for both regular and extended images
if (
(node.type === DendronASTTypes.IMAGE ||
node.type === DendronASTTypes.EXTENDED_IMAGE) &&
dest === DendronASTDest.HTML
) {
const imageNode = node as Image;
if (opts?.assetsPrefix) {
imageNode.url =
Expand All @@ -353,6 +360,18 @@ function plugin(this: Unified.Processor, opts?: PluginOpts): Transformer {
_.trim(imageNode.url, "/");
}
}
if (
node.type === DendronASTTypes.EXTENDED_IMAGE &&
dest === DendronASTDest.HTML
) {
const index = _.indexOf(parent.children, node);
// Replace with the HTML containing the image including custom properties
parent.children.splice(
index,
1,
extendedImage2html(node as ExtendedImage)
);
}
return; // continue traversal
});
return tree;
Expand Down
139 changes: 139 additions & 0 deletions packages/engine-server/src/markdown/remark/extendedImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import _ from "lodash";
import { DendronError } from "@dendronhq/common-all";
import { Eat } from "remark-parse";
import Unified, { Plugin } from "unified";
import { DendronASTDest, DendronASTTypes, ExtendedImage } from "../types";
import { Element } from "hast";
import { html } from "mdast-builder";
import YAML from "yamljs";
import { MDUtilsV5 } from "../utilsv5";

export const EXTENDED_IMAGE_REGEX =
/^!\[(?<alt>[^[\]]*)\]\((?<url>[^()]+)\)(?<props>{[^{}]+})/;
export const EXTENDED_IMAGE_REGEX_LOOSE =
/!\[(?<alt>[^[\]]*)\]\((?<url>[^()]+)\)(?<props>{[^{}]+})/;

export const matchExtendedImage = (
text: string,
matchLoose: boolean = true
): string | undefined => {
const match = (
matchLoose ? EXTENDED_IMAGE_REGEX_LOOSE : EXTENDED_IMAGE_REGEX
).exec(text);
if (match && match.groups?.url && match.groups) return match[1];
return undefined;
};

type PluginOpts = {};

const plugin: Plugin<[PluginOpts?]> = function (
this: Unified.Processor,
opts?: PluginOpts
) {
attachParser(this);
if (this.Compiler != null) {
attachCompiler(this, opts);
}
};

function attachParser(proc: Unified.Processor) {
function locator(value: string, fromIndex: number) {
return value.indexOf("!", fromIndex);
}

function inlineTokenizer(eat: Eat, value: string) {
const match = EXTENDED_IMAGE_REGEX.exec(value);
if (match && match.groups?.url) {
let props: { [key: string]: any } = {};
try {
props = YAML.parse(match.groups.props);
} catch {
// Reject bad props so that it falls back to a regular image
return;
}

return eat(match[0])({
type: DendronASTTypes.EXTENDED_IMAGE,
value,
url: match.groups.url,
alt: match.groups.alt,
props,
});
}
return;
}
inlineTokenizer.locator = locator;

const Parser = proc.Parser;
const inlineTokenizers = Parser.prototype.inlineTokenizers;
const inlineMethods = Parser.prototype.inlineMethods;
inlineTokenizers.extendedImage = inlineTokenizer;
inlineMethods.splice(inlineMethods.indexOf("link"), 0, "extendedImage");
}

function attachCompiler(proc: Unified.Processor, _opts?: PluginOpts) {
const Compiler = proc.Compiler;
const visitors = Compiler.prototype.visitors;

if (visitors) {
visitors.extendedImage = function (node: ExtendedImage): string | Element {
const { dest } = MDUtilsV5.getProcData(proc);
const alt = node.alt ? node.alt : "";
switch (dest) {
case DendronASTDest.MD_DENDRON:
return `![${alt}](${node.url})${YAML.stringify(
node.props,
0 /* Inline-only so we get JSON style {foo: bar} */
)}`;
case DendronASTDest.MD_REGULAR:
return `![${alt}](${node.url})`;
case DendronASTDest.MD_ENHANCED_PREVIEW:
return extendedImage2htmlRaw(node);
default:
throw new DendronError({
message: "Unable to render extended image",
});
}
};
}
}

const ALLOWED_STYLE_PROPS = new Set<string>([
"width",
"height",
"float",
"border",
"margin",
"padding",
"min-width",
"min-height",
"max-width",
"max-height",
"display",
"opacity",
"outline",
"rotate",
"transition",
"transform-origin",
"transform",
"z-index",
]);

export function extendedImage2htmlRaw(node: ExtendedImage, _opts?: PluginOpts) {
const stylesList: string[] = [];
const nodePropsList: string[] = [];
for (const [prop, value] of Object.entries(node.props)) {
if (ALLOWED_STYLE_PROPS.has(prop)) stylesList.push(`${prop}:${value};`);
}
nodePropsList.push(`src="${node.url}"`);
if (node.alt) nodePropsList.push(`alt="${node.alt}"`);

return `<img ${nodePropsList.join(" ")} style="${stylesList.join("")}">`;
}

export function extendedImage2html(node: ExtendedImage, opts?: PluginOpts) {
return html(extendedImage2htmlRaw(node, opts));
}

export { plugin as extendedImage };
export { PluginOpts as ExtendedImageOpts };
6 changes: 6 additions & 0 deletions packages/engine-server/src/markdown/remark/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ export {
userTags,
matchUserTag,
} from "./userTags";
export {
extendedImage,
ExtendedImageOpts,
extendedImage2html,
extendedImage2htmlRaw,
} from "./extendedImage";
5 changes: 5 additions & 0 deletions packages/engine-server/src/markdown/remark/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { WorkspaceUtils } from "../../workspace";
import {
Anchor,
BlockAnchor,
ExtendedImage,
DendronASTDest,
DendronASTNode,
DendronASTRoot,
Expand Down Expand Up @@ -907,6 +908,10 @@ export class RemarkUtils {
return node.type === DendronASTTypes.IMAGE;
}

static isExtendedImage(node: Node): node is ExtendedImage {
return node.type === DendronASTTypes.EXTENDED_IMAGE;
}

static isText(node: Node): node is Text {
return node.type === DendronASTTypes.TEXT;
}
Expand Down
9 changes: 8 additions & 1 deletion packages/engine-server/src/markdown/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
DVault,
NoteProps,
} from "@dendronhq/common-all";
import { Heading, Parent, Root } from "mdast";
import { Heading, Image, Parent, Root } from "mdast";
import { Processor } from "unified";
import { DendronPubOpts } from "./remark/dendronPub";
import { WikiLinksOpts } from "./remark/wikiLinks";
Expand Down Expand Up @@ -38,6 +38,7 @@ export enum DendronASTTypes {
BLOCK_ANCHOR = "blockAnchor",
HASHTAG = "hashtag",
USERTAG = "usertag",
EXTENDED_IMAGE = "extendedImage",
// Not dendron-specific, included here for convenience
ROOT = "root",
HEADING = "heading",
Expand Down Expand Up @@ -155,3 +156,9 @@ export type UserTag = DendronASTNode & {
};

export type Anchor = BlockAnchor | Heading;

export type ExtendedImage = DendronASTNode &
Image & {
/** User provided props, to set things like width and height. */
props: { [key: string]: any };
};
3 changes: 3 additions & 0 deletions packages/engine-server/src/markdown/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
} from "./types";
import { hashtags } from "./remark/hashtag";
import { userTags } from "./remark/userTags";
import { extendedImage } from "./remark/extendedImage";

const toString = require("mdast-util-to-string");
export { nunjucks };
Expand Down Expand Up @@ -339,6 +340,7 @@ export class MDUtilsV4 {
.use(blockAnchors)
.use(hashtags)
.use(userTags)
.use(extendedImage)
.data("errors", errors);
this.setDendronData(_proc, { dest: opts.dest, fname: opts.fname });
this.setEngine(_proc, opts.engine);
Expand Down Expand Up @@ -408,6 +410,7 @@ export class MDUtilsV4 {
.use(blockAnchors, _.merge(opts.blockAnchorsOpts))
.use(hashtags)
.use(userTags)
.use(extendedImage)
.use(noteRefsV2, {
...opts.noteRefOpts,
wikiLinkOpts: opts.wikiLinksOpts,
Expand Down
18 changes: 11 additions & 7 deletions packages/engine-server/src/markdown/utilsv5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { hashtags } from "./remark/hashtag";
import { userTags } from "./remark/userTags";
import { backlinks } from "./remark/backlinks";
import { hierarchies } from "./remark";
import { extendedImage } from "./remark/extendedImage";

/**
* What mode a processor should run in
Expand Down Expand Up @@ -237,6 +238,7 @@ export class MDUtilsV5 {
.use(blockAnchors)
.use(hashtags)
.use(userTags)
.use(extendedImage)
.use(footnotes)
.data("errors", errors);

Expand Down Expand Up @@ -280,6 +282,14 @@ export class MDUtilsV5 {
if (data.dest === DendronASTDest.HTML) {
proc = proc.use(backlinks).use(hierarchies);
}
// Add flavor specific plugins. These need to come before `dendronPub`
// to fix extended image URLs before they get converted to HTML
if (opts.flavor === ProcFlavor.PREVIEW) {
proc = proc.use(dendronPreview);
}
if (opts.flavor === ProcFlavor.HOVER_PREVIEW) {
proc = proc.use(dendronHoverPreview);
}
// add additional plugins
proc = proc.use(dendronPub, {
insertTitle: shouldInsertTitle,
Expand All @@ -290,13 +300,7 @@ export class MDUtilsV5 {
if (data.config?.mermaid) {
proc = proc.use(mermaid, { simple: true });
}
// add flavor specific plugins
if (opts.flavor === ProcFlavor.PREVIEW) {
proc = proc.use(dendronPreview);
}
if (opts.flavor === ProcFlavor.HOVER_PREVIEW) {
proc = proc.use(dendronHoverPreview);
}
// Add remaining flavor specific plugins
if (opts.flavor === ProcFlavor.PUBLISHING) {
proc = proc.use(dendronPub, {
wikiLinkOpts: {
Expand Down

0 comments on commit f9ed88f

Please sign in to comment.