@diplodoc/transform
Version:
A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML
247 lines (211 loc) • 8.33 kB
text/typescript
import type MarkdownIt from 'markdown-it';
import type ParserInline from 'markdown-it/lib/parser_inline';
import type Token from 'markdown-it/lib/token';
import {ImsizeAttr} from './const';
import {parseImageSize} from './helpers';
export type ImsizeOptions = {
enableInlineStyling?: boolean;
};
export const imageWithSize = (md: MarkdownIt, opts?: ImsizeOptions): ParserInline.RuleInline => {
// eslint-disable-next-line complexity
return (state, silent) => {
if (state.src.charCodeAt(state.pos) !== 0x21 /* ! */) {
return false;
}
if (state.src.charCodeAt(state.pos + 1) !== 0x5b /* [ */) {
return false;
}
const labelStart = state.pos + 2;
const labelEnd = md.helpers.parseLinkLabel(state, state.pos + 1, false);
// parser failed to find ']', so it's not a valid link
if (labelEnd < 0) {
return false;
}
let href = '';
let title = '';
let width = '';
let height = '';
const oldPos = state.pos;
const max = state.posMax;
let pos = labelEnd + 1;
if (pos < max && state.src.charCodeAt(pos) === 0x28 /* ( */) {
//
// Inline link
//
// [link]( <href> "title" )
// ^^ skipping these spaces
pos++;
for (; pos < max; pos++) {
const code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0a) {
break;
}
}
if (pos >= max) {
return false;
}
// [link]( <href> "title" )
// ^^^^^^ parsing link destination
let start = pos;
const destResult = md.helpers.parseLinkDestination(state.src, pos, state.posMax);
if (destResult.ok) {
href = state.md.normalizeLink(destResult.str);
if (state.md.validateLink(href)) {
pos = destResult.pos;
} else {
href = '';
}
}
// [link]( <href> "title" )
// ^^ skipping these spaces
start = pos;
for (; pos < max; pos++) {
const code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0a) {
break;
}
}
// [link]( <href> "title" )
// ^^^^^^^ parsing link title
const titleResult = md.helpers.parseLinkTitle(state.src, pos, state.posMax);
if (pos < max && start !== pos && titleResult.ok) {
title = titleResult.str;
pos = titleResult.pos;
// [link]( <href> "title" )
// ^^ skipping these spaces
for (; pos < max; pos++) {
const code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0a) {
break;
}
}
} else {
title = '';
}
// [link]( <href> "title" =WxH )
// ^^^^ parsing image size
if (pos - 1 >= 0) {
const code = state.src.charCodeAt(pos - 1);
// there must be at least one white spaces
// between previous field and the size
if (code === 0x20) {
const sizeResult = parseImageSize(state.src, pos, state.posMax);
if (sizeResult.ok) {
width = sizeResult.width;
height = sizeResult.height;
pos = sizeResult.pos;
// [link]( <href> "title" =WxH )
// ^^ skipping these spaces
for (; pos < max; pos++) {
const code = state.src.charCodeAt(pos);
// eslint-disable-next-line max-depth
if (code !== 0x20 && code !== 0x0a) {
break;
}
}
}
}
}
if (pos >= max || state.src.charCodeAt(pos) !== 0x29 /* ) */) {
state.pos = oldPos;
return false;
}
pos++;
} else {
//
// Link reference
//
if (typeof state.env.references === 'undefined') {
return false;
}
// [foo] [bar]
// ^^ optional whitespace (can include newlines)
for (; pos < max; pos++) {
const code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0a) {
break;
}
}
let label = '';
if (pos < max && state.src.charCodeAt(pos) === 0x5b /* [ */) {
const start = pos + 1;
pos = md.helpers.parseLinkLabel(state, pos);
if (pos >= 0) {
label = state.src.slice(start, pos++);
} else {
pos = labelEnd + 1;
}
} else {
pos = labelEnd + 1;
}
// covers label === '' and label === undefined
// (collapsed reference link and shortcut reference link respectively)
if (!label) {
label = state.src.slice(labelStart, labelEnd);
}
const ref = state.env.references[md.utils.normalizeReference(label)];
if (!ref) {
state.pos = oldPos;
return false;
}
href = ref.href;
title = ref.title;
}
//
// We found the end of the link, and know for a fact it's a valid link;
// so all that's left to do is to call tokenizer.
//
if (!silent) {
state.pos = labelStart;
state.posMax = labelEnd;
const tokens: Token[] = [];
const newState = new state.md.inline.State(
state.src.slice(labelStart, labelEnd),
state.md,
state.env,
tokens,
);
newState.md.inline.tokenize(newState);
const token = state.push('image', 'img', 0);
token.children = tokens;
token.attrs = [
[ImsizeAttr.Src, href],
[ImsizeAttr.Alt, ''],
];
if (title) {
token.attrs.push([ImsizeAttr.Title, title]);
}
if (width !== '') {
token.attrs.push([ImsizeAttr.Width, width]);
}
if (height !== '') {
token.attrs.push([ImsizeAttr.Height, height]);
}
if (opts?.enableInlineStyling) {
let style: string | undefined = '';
const widthWithPercent = width.includes('%');
const heightWithPercent = height.includes('%');
if (width !== '') {
const widthString = widthWithPercent ? width : `${width}px`;
style += `width: ${widthString};`;
}
if (height !== '') {
if (width !== '' && !heightWithPercent && !widthWithPercent) {
style += `aspect-ratio: ${width} / ${height};height: auto;`;
state.env.additionalOptionsCssWhiteList ??= {};
state.env.additionalOptionsCssWhiteList['aspect-ratio'] = true;
} else {
const heightString = heightWithPercent ? height : `${height}px`;
style += `height: ${heightString};`;
}
}
if (style) {
token.attrs.push([ImsizeAttr.Style, style]);
}
}
}
state.pos = pos;
state.posMax = max;
return true;
};
};