Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-08-26 14:54:15 +02:00
parent 1d90013344
commit f5736dad0f
37 changed files with 2025 additions and 0 deletions

View file

@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: MIT
*/
import MarkdownIt from 'markdown-it/lib'
import { imageSize } from './index.js'
import { describe, expect, it } from '@jest/globals'
describe('markdown-it-imsize', function () {
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true
}).use(imageSize)
it('renders a image without size or title', () => {
expect(md.renderInline('![test](x)')).toBe('<img src="x" alt="test">')
})
it('renders a image with title', () => {
expect(md.renderInline('![test](x "thisisthetitle")')).toBe('<img src="x" alt="test" title="thisisthetitle">')
})
it('renders an image with absolute width and height', () => {
expect(md.renderInline('![test](x =100x200)')).toBe('<img src="x" alt="test" width="100" height="200">')
})
it('renders an image with relative width and height', () => {
expect(md.renderInline('![test](x =100%x200%)')).toBe('<img src="x" alt="test" width="100%" height="200%">')
})
it('renders an image with title and size', () => {
expect(md.renderInline('![test](x "thisisthetitle" =100x200)')).toBe(
'<img src="x" alt="test" title="thisisthetitle" width="100" height="200">'
)
})
it('renders an image with no size but x', () => {
expect(md.renderInline('![test](x "thisisthetitle" =x)')).toBe('<img src="x" alt="test" title="thisisthetitle">')
})
it("doesn't render an image with invalid size syntax", () => {
expect(md.renderInline('![test](x "thisisthetitle" =xx)')).toBe('![test](x “thisisthetitle” =xx)')
})
it('renders an image with only width', () => {
expect(md.renderInline('![test](x =100x)')).toBe('<img src="x" alt="test" width="100">')
})
it('renders an image with only height', () => {
expect(md.renderInline('![test](x =x200)')).toBe('<img src="x" alt="test" height="200">')
})
})

View file

@ -0,0 +1,245 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: MIT
*/
import MarkdownIt from 'markdown-it'
import ParserInline from 'markdown-it/lib/parser_inline.js'
import StateInline from 'markdown-it/lib/rules_inline/state_inline.js'
import { ParseImageSize, parseImageSize } from './parse-image-size.js'
import { SpecialCharacters } from './specialCharacters.js'
const checkForImageTagStart = (state: StateInline): boolean => {
return (
state.src.charCodeAt(state.pos) === SpecialCharacters.EXCLAMATION_MARK &&
state.src.charCodeAt(state.pos + 1) === SpecialCharacters.OPENING_BRACKET
)
}
const skipWhiteSpaces = (startPosition: number, state: StateInline): number => {
let position = startPosition
while (position < state.posMax) {
const code = state.src.charCodeAt(position)
if (code !== SpecialCharacters.WHITESPACE && code !== SpecialCharacters.NEW_LINE) {
break
}
position += 1
}
return position
}
function createImageToken(
state: StateInline,
labelStartIndex: number,
labelEndIndex: number,
href: string,
title: string,
width: string,
height: string
) {
state.pos = labelStartIndex
state.posMax = labelEndIndex
const token = state.push('image', 'img', 0)
token.children = []
const newState = new state.md.inline.State(
state.src.slice(labelStartIndex, labelEndIndex),
state.md,
state.env,
token.children
)
newState.md.inline.tokenize(newState)
token.attrSet('src', href)
token.attrSet('alt', '')
if (title) {
token.attrSet('title', title)
}
if (width !== '') {
token.attrSet('width', width)
}
if (height !== '') {
token.attrSet('height', height)
}
}
function parseSizeParameters(startPosition: number, state: StateInline): ParseImageSize | undefined {
// [link]( <href> "title" =WxH )
// ^^^^ parsing image size
if (startPosition - 1 < 0) {
return
}
const code = state.src.charCodeAt(startPosition - 1)
if (code !== SpecialCharacters.WHITESPACE) {
return
}
const res = parseImageSize(state.src, startPosition, state.posMax)
if (!res) {
return
}
// [link]( <href> "title" =WxH )
// ^^ skipping these spaces
return {
position: skipWhiteSpaces(res.position, state),
width: res.width,
height: res.height
}
}
export interface ParseLinkResult {
position: number
href: string
}
// [link]( <href> "title" )
// ^^^^^^ parsing link destination
function parseLink(state: StateInline, startPosition: number): ParseLinkResult | undefined {
const linkParseResult = state.md.helpers.parseLinkDestination(state.src, startPosition, state.posMax)
if (!linkParseResult.ok) {
return
}
const href = state.md.normalizeLink(linkParseResult.str)
if (state.md.validateLink(href)) {
return { position: linkParseResult.pos, href }
} else {
return { position: startPosition, href: '' }
}
}
const imageWithSize: ParserInline.RuleInline = (state, silent) => {
let position,
title,
start,
href = '',
width = '',
height = ''
const oldPos = state.pos,
max = state.posMax
if (!checkForImageTagStart(state)) {
return false
}
const labelStartIndex = state.pos + 2
const labelEndIndex = state.md.helpers.parseLinkLabel(state, state.pos + 1, false)
// parser failed to find ']', so it's not a valid link
if (labelEndIndex < 0) {
return false
}
position = labelEndIndex + 1
if (position < max && state.src.charCodeAt(position) === SpecialCharacters.OPENING_PARENTHESIS) {
//
// Inline link
//
// [link]( <href> "title" )
// ^^ skipping these spaces
position += 1
position = skipWhiteSpaces(position, state)
if (position >= max) {
return false
}
const parseLinkResult = parseLink(state, position)
if (!parseLinkResult) {
return false
}
position = parseLinkResult.position
href = parseLinkResult.href
// [link]( <href> "title" )
// ^^ skipping these spaces
start = position
position = skipWhiteSpaces(position, state)
// [link]( <href> "title" )
// ^^^^^^^ parsing link title
const parseLinkTitleResult = state.md.helpers.parseLinkTitle(state.src, position, state.posMax)
if (position < max && start !== position && parseLinkTitleResult.ok) {
title = parseLinkTitleResult.str
position = parseLinkTitleResult.pos
// [link]( <href> "title" )
// ^^ skipping these spaces
position = skipWhiteSpaces(position, state)
} else {
title = ''
}
const parseSizeParametersResult = parseSizeParameters(position, state)
if (parseSizeParametersResult) {
position = parseSizeParametersResult.position
width = parseSizeParametersResult.width
height = parseSizeParametersResult.height
}
if (position >= max || state.src.charCodeAt(position) !== SpecialCharacters.CLOSING_PARENTHESIS) {
state.pos = oldPos
return false
}
position += 1
} else {
//
// Link reference
//
if (typeof state.env.references === 'undefined') {
return false
}
// [foo] [bar]
// ^^ optional whitespace (can include newlines)
position = skipWhiteSpaces(position, state)
let label
if (position < max && state.src.charCodeAt(position) === SpecialCharacters.OPENING_BRACKET) {
start = position + 1
position = state.md.helpers.parseLinkLabel(state, position)
if (position >= 0) {
label = state.src.slice(start, (position += 1))
} else {
position = labelEndIndex + 1
}
} else {
position = labelEndIndex + 1
}
// covers label === '' and label === undefined
// (collapsed reference link and shortcut reference link respectively)
if (!label) {
label = state.src.slice(labelStartIndex, labelEndIndex)
}
const ref = state.env.references[state.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) {
createImageToken(state, labelStartIndex, labelEndIndex, href, title, width, height)
}
state.pos = position
state.posMax = max
return true
}
export const imageSize: MarkdownIt.PluginSimple = (md: MarkdownIt) => {
md.inline.ruler.before('emphasis', 'image', imageWithSize)
}

View file

@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: MIT
*/
import { SpecialCharacters } from './specialCharacters.js'
export interface ParseImageSize {
position: number
width: string
height: string
}
export interface ParseNextNumber {
position: number
value: string
}
function isCharacterADigit(code: number) {
return code >= SpecialCharacters.NUMBER_ZERO && code <= SpecialCharacters.NUMBER_NINE
}
function findNextNotNumberCharacter(startPosition: number, maximalPosition: number, content: string): number {
for (let position = startPosition; position < maximalPosition; position += 1) {
const code = content.charCodeAt(position)
if (!isCharacterADigit(code) && code !== SpecialCharacters.PERCENTAGE) {
return position
}
}
return maximalPosition
}
function parseNextNumber(content: string, startPosition: number, maximalPosition: number): ParseNextNumber {
const endCharacterIndex = findNextNotNumberCharacter(startPosition, maximalPosition, content)
return {
position: endCharacterIndex,
value: content.slice(startPosition, endCharacterIndex)
}
}
/*
size must follow = without any white spaces as follows
(1) =300x200
(2) =300x
(3) =x200
*/
const checkImageSizeStart = (code: number): boolean => {
return (
code === SpecialCharacters.LOWER_CASE_X ||
(code >= SpecialCharacters.NUMBER_ZERO && code <= SpecialCharacters.NUMBER_NINE)
)
}
export function parseImageSize(
imageSize: string,
startCharacterPosition: number,
maximalCharacterPosition: number
): ParseImageSize | undefined {
if (startCharacterPosition >= maximalCharacterPosition) {
return
}
let currentCharacterPosition = startCharacterPosition
if (imageSize.charCodeAt(currentCharacterPosition) !== SpecialCharacters.EQUALS /* = */) {
return
}
currentCharacterPosition += 1
if (!checkImageSizeStart(imageSize.charCodeAt(currentCharacterPosition))) {
return
}
// parse width
const width = parseNextNumber(imageSize, currentCharacterPosition, maximalCharacterPosition)
currentCharacterPosition = width.position
// next charactor must be 'x'
const code = imageSize.charCodeAt(currentCharacterPosition)
if (code !== SpecialCharacters.LOWER_CASE_X /* x */) {
return
}
currentCharacterPosition += 1
// parse height
const height = parseNextNumber(imageSize, currentCharacterPosition, maximalCharacterPosition)
currentCharacterPosition = height.position
return {
width: width.value,
height: height.value,
position: currentCharacterPosition
}
}

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: MIT
*/
export enum SpecialCharacters {
EXCLAMATION_MARK = 0x21,
OPENING_BRACKET = 0x5b,
OPENING_PARENTHESIS = 0x28,
WHITESPACE = 0x20,
NEW_LINE = 0x0a,
EQUALS = 0x3d,
LOWER_CASE_X = 0x78,
NUMBER_ZERO = 0x30,
NUMBER_NINE = 0x39,
PERCENTAGE = 0x25,
CLOSING_PARENTHESIS = 0x29
}