mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-07 09:55:43 -04:00
feat: import markdown-it-plugins from https://github.com/hedgedoc/markdown-it-plugins
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
1d90013344
commit
f5736dad0f
37 changed files with 2025 additions and 0 deletions
56
markdown-it-plugins/src/image-size/index.test.ts
Normal file
56
markdown-it-plugins/src/image-size/index.test.ts
Normal 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('')).toBe('<img src="x" alt="test">')
|
||||
})
|
||||
|
||||
it('renders a image with title', () => {
|
||||
expect(md.renderInline('')).toBe('<img src="x" alt="test" title="thisisthetitle">')
|
||||
})
|
||||
|
||||
it('renders an image with absolute width and height', () => {
|
||||
expect(md.renderInline('')).toBe('<img src="x" alt="test" width="100" height="200">')
|
||||
})
|
||||
|
||||
it('renders an image with relative width and height', () => {
|
||||
expect(md.renderInline('')).toBe('<img src="x" alt="test" width="100%" height="200%">')
|
||||
})
|
||||
|
||||
it('renders an image with title and size', () => {
|
||||
expect(md.renderInline('')).toBe(
|
||||
'<img src="x" alt="test" title="thisisthetitle" width="100" height="200">'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders an image with no size but x', () => {
|
||||
expect(md.renderInline('')).toBe('<img src="x" alt="test" title="thisisthetitle">')
|
||||
})
|
||||
|
||||
it("doesn't render an image with invalid size syntax", () => {
|
||||
expect(md.renderInline('')).toBe('')
|
||||
})
|
||||
|
||||
it('renders an image with only width', () => {
|
||||
expect(md.renderInline('')).toBe('<img src="x" alt="test" width="100">')
|
||||
})
|
||||
|
||||
it('renders an image with only height', () => {
|
||||
expect(md.renderInline('')).toBe('<img src="x" alt="test" height="200">')
|
||||
})
|
||||
})
|
245
markdown-it-plugins/src/image-size/index.ts
Normal file
245
markdown-it-plugins/src/image-size/index.ts
Normal 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)
|
||||
}
|
98
markdown-it-plugins/src/image-size/parse-image-size.ts
Normal file
98
markdown-it-plugins/src/image-size/parse-image-size.ts
Normal 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
|
||||
}
|
||||
}
|
19
markdown-it-plugins/src/image-size/specialCharacters.ts
Normal file
19
markdown-it-plugins/src/image-size/specialCharacters.ts
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue