Add quote extra markdown it plugin (#1020)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Tilman Vatteroth 2021-02-08 18:29:02 +01:00 committed by GitHub
parent 7f6e0e53a7
commit 5b1940f0ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 309 additions and 160 deletions

View file

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
describe('Quote extra tags', function () {
beforeEach(() => {
cy.visitTestEditor()
})
describe('Name quote tag', () => {
it('renders correctly', () => {
cy.codemirrorFill('[name=testy mctestface]')
cy.getMarkdownBody()
.find('.quote-extra')
.should('be.visible')
.find('.fa-user')
.should('be.visible')
cy.getMarkdownBody()
.find('.quote-extra')
.should('be.visible')
.contains('testy mctestface')
})
})
describe('Time quote tag', () => {
it('renders correctly', () => {
cy.codemirrorFill(`[time=always]`)
cy.getMarkdownBody()
.find('.quote-extra')
.should('be.visible')
.find('.fa-clock-o')
.should('be.visible')
cy.getMarkdownBody()
.find('.quote-extra')
.should('be.visible')
.contains('always')
})
})
describe('Color quote tag', () => {
it('renders correctly', () => {
cy.codemirrorFill(`[color=#b51f08]`)
cy.getMarkdownBody()
.find('.quote-extra')
.should('be.visible')
.find('.fa-tag')
.should('be.visible')
cy.getMarkdownBody()
.find('.quote-extra')
.should('be.visible')
.should('have.css', 'color', 'rgb(181, 31, 8)')
})
it('doesn\'t render in a blockquote and dyes the blockquote border', () => {
cy.codemirrorFill(`> [color=#b51f08] HedgeDoc`)
cy.getMarkdownBody()
.find('.quote-extra')
.should('not.exist')
cy.getMarkdownBody()
.find('blockquote')
.should('be.visible')
.should('have.css', 'border-left-color', 'rgb(181, 31, 8)')
})
})
})

View file

@ -34,6 +34,7 @@ Cypress.Commands.add('codemirrorFill', (content: string) => {
.fill(content)
if (line) {
cy.get('.CodeMirror')
.contains('.CodeMirror-line', line)
.find('.CodeMirror-line')
.should('contain.text', line)
}
})

View file

@ -4,123 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type IconName = '500px' | 'activitypub' | 'address-book-o' | 'address-book' | 'address-card-o' | 'address-card'
| 'adjust' | 'adn' | 'align-center' | 'align-justify' | 'align-left' | 'align-right' | 'amazon'
| 'ambulance' | 'american-sign-language-interpreting' | 'anchor' | 'android' | 'angellist'
| 'angle-double-down' | 'angle-double-left' | 'angle-double-right' | 'angle-double-up'
| 'angle-down' | 'angle-left' | 'angle-right' | 'angle-up' | 'apple' | 'archive-org' | 'archive'
| 'archlinux' | 'area-chart' | 'arrow-circle-down' | 'arrow-circle-left' | 'arrow-circle-o-down'
| 'arrow-circle-o-left' | 'arrow-circle-o-right' | 'arrow-circle-o-up' | 'arrow-circle-right'
| 'arrow-circle-up' | 'arrow-down' | 'arrow-left' | 'arrow-right' | 'arrows-alt' | 'arrows-h'
| 'arrows' | 'arrows-v' | 'arrow-up' | 'artstation' | 'assistive-listening-systems' | 'asterisk'
| 'at' | 'att' | 'audio-description' | 'backward' | 'balance-scale' | 'bandcamp' | 'ban'
| 'bar-chart' | 'barcode' | 'bars' | 'bath' | 'battery-empty' | 'battery-full' | 'battery-half'
| 'battery-quarter' | 'battery-three-quarters' | 'bed' | 'beer' | 'behance-square' | 'behance'
| 'bell-o' | 'bell-rigning-o' | 'bell-ringing' | 'bell-slash-o' | 'bell-slash' | 'bell'
| 'bicycle' | 'binoculars' | 'biometric' | 'birthday-cake' | 'bitbucket-square' | 'bitbucket'
| 'black-tie' | 'blind' | 'bluetooth-b' | 'bluetooth' | 'bold' | 'bolt' | 'bomb' | 'bookmark-o'
| 'bookmark' | 'book' | 'bootstrap' | 'braille' | 'briefcase' | 'btc' | 'bug' | 'building-o'
| 'building' | 'bullhorn' | 'bullseye' | 'bus' | 'buysellads' | 'calculator' | 'calendar-check-o'
| 'calendar-minus-o' | 'calendar-o' | 'calendar-plus-o' | 'calendar' | 'calendar-times-o'
| 'camera-retro' | 'camera' | 'caret-down' | 'caret-left' | 'caret-right' | 'caret-square-o-down'
| 'caret-square-o-left' | 'caret-square-o-right' | 'caret-square-o-up' | 'caret-up' | 'car'
| 'cart-arrow-down' | 'cart-plus' | 'cc-amex' | 'cc-diners-club' | 'cc-discover' | 'cc-jcb'
| 'cc-mastercard' | 'cc-paypal' | 'cc-stripe' | 'cc' | 'cc-visa' | 'certificate' | 'chain-broken'
| 'check-circle-o' | 'check-circle' | 'check-square-o' | 'check-square' | 'check'
| 'chevron-circle-down' | 'chevron-circle-left' | 'chevron-circle-right' | 'chevron-circle-up'
| 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'child' | 'chrome'
| 'circle-o-notch' | 'circle-o' | 'circle' | 'circle-thin' | 'classicpress-circle'
| 'classicpress' | 'clipboard' | 'clock-o' | 'clone' | 'cloud-download' | 'cloud'
| 'cloud-upload' | 'code-fork' | 'codepen' | 'code' | 'codiepie' | 'coffee' | 'cogs' | 'cog'
| 'columns' | 'commenting-o' | 'commenting' | 'comment-o' | 'comments-o' | 'comments' | 'comment'
| 'compass' | 'compress' | 'connectdevelop' | 'contao' | 'copyright' | 'creative-commons'
| 'credit-card-alt' | 'credit-card' | 'crop' | 'crosshairs' | 'css3' | 'c' | 'cubes' | 'cube'
| 'cutlery' | 'dashcube' | 'database' | 'deaf' | 'debian' | 'delicious' | 'desktop'
| 'deviantart' | 'dev-to' | 'diamond' | 'diaspora' | 'digg' | 'digitalocean' | 'discord-alt'
| 'discord' | 'dogmazic' | 'dot-circle-o' | 'download' | 'dribbble' | 'dropbox' | 'drupal'
| 'edge' | 'eercast' | 'eject' | 'ellipsis-h' | 'ellipsis-v' | 'emby' | 'empire'
| 'envelope-open-o' | 'envelope-open' | 'envelope-o' | 'envelope-square' | 'envelope' | 'envira'
| 'eraser' | 'ethereum' | 'etsy' | 'eur' | 'exchange' | 'exclamation-circle' | 'exclamation'
| 'exclamation-triangle' | 'expand' | 'expeditedssl' | 'external-link-square' | 'external-link'
| 'eyedropper' | 'eye-slash' | 'eye' | 'facebook-messenger' | 'facebook-official'
| 'facebook-square' | 'facebook' | 'fast-backward' | 'fast-forward' | 'fax' | 'f-droid'
| 'female' | 'ffmpeg' | 'fighter-jet' | 'file-archive-o' | 'file-audio-o' | 'file-code-o'
| 'file-epub' | 'file-excel-o' | 'file-image-o' | 'file-o' | 'file-pdf-o' | 'file-powerpoint-o'
| 'files-o' | 'file' | 'file-text-o' | 'file-text' | 'file-video-o' | 'file-word-o' | 'film'
| 'filter' | 'fire-extinguisher' | 'firefox' | 'fire' | 'first-order' | 'flag-checkered'
| 'flag-o' | 'flag' | 'flask' | 'flickr' | 'floppy-o' | 'folder-open-o' | 'folder-open'
| 'folder-o' | 'folder' | 'font-awesome' | 'fonticons' | 'font' | 'fork-awesome' | 'fort-awesome'
| 'forumbee' | 'forward' | 'foursquare' | 'free-code-camp' | 'freedombox' | 'friendica'
| 'frown-o' | 'funkwhale' | 'futbol-o' | 'gamepad' | 'gavel' | 'gbp' | 'genderless'
| 'get-pocket' | 'gg-circle' | 'gg' | 'gift' | 'gimp' | 'gitea' | 'github-alt' | 'github-square'
| 'github' | 'gitlab' | 'git-square' | 'git' | 'glass' | 'glide-g' | 'glide' | 'globe-e'
| 'globe' | 'globe-w' | 'gnupg' | 'gnu-social' | 'google-plus-official' | 'google-plus-square'
| 'google-plus' | 'google' | 'google-wallet' | 'graduation-cap' | 'gratipay' | 'grav'
| 'hackaday' | 'hacker-news' | 'hackster' | 'hal' | 'hand-lizard-o' | 'hand-o-down'
| 'hand-o-left' | 'hand-o-right' | 'hand-o-up' | 'hand-paper-o' | 'hand-peace-o'
| 'hand-pointer-o' | 'hand-rock-o' | 'hand-scissors-o' | 'handshake-o' | 'hand-spock-o'
| 'hashnode' | 'hashtag' | 'hdd-o' | 'header' | 'headphones' | 'heartbeat' | 'heart-o' | 'heart'
| 'history' | 'home' | 'hospital-o' | 'hourglass-end' | 'hourglass-half' | 'hourglass-o'
| 'hourglass-start' | 'hourglass' | 'houzz' | 'h-square' | 'html5' | 'hubzilla' | 'i-cursor'
| 'id-badge' | 'id-card-o' | 'id-card' | 'ils' | 'imdb' | 'inbox' | 'indent' | 'industry'
| 'info-circle' | 'info' | 'inkscape' | 'inr' | 'instagram' | 'internet-explorer' | 'ioxhost'
| 'italic' | 'jirafeau' | 'joomla' | 'joplin' | 'jpy' | 'jsfiddle' | 'julia' | 'jupyter'
| 'keybase' | 'keyboard-o' | 'key-modern' | 'key' | 'krw' | 'language' | 'laptop' | 'laravel'
| 'lastfm-square' | 'lastfm' | 'leaf' | 'leanpub' | 'lemon-o' | 'level-down' | 'level-up'
| 'liberapay-square' | 'liberapay' | 'life-ring' | 'lightbulb-o' | 'line-chart'
| 'linkedin-square' | 'linkedin' | 'link' | 'linode' | 'linux' | 'list-alt' | 'list-ol' | 'list'
| 'list-ul' | 'location-arrow' | 'lock' | 'long-arrow-down' | 'long-arrow-left'
| 'long-arrow-right' | 'long-arrow-up' | 'low-vision' | 'magic' | 'magnet' | 'male'
| 'map-marker' | 'map-o' | 'map-pin' | 'map-signs' | 'map' | 'mars-double' | 'mars-stroke-h'
| 'mars-stroke' | 'mars-stroke-v' | 'mars' | 'mastodon-alt' | 'mastodon-square' | 'mastodon'
| 'matrix-org' | 'maxcdn' | 'meanpath' | 'medium-square' | 'medium' | 'medkit' | 'meetup'
| 'meh-o' | 'mercury' | 'microchip' | 'microphone-slash' | 'microphone' | 'minus-circle'
| 'minus-square-o' | 'minus-square' | 'minus' | 'mixcloud' | 'mobile' | 'modx' | 'money'
| 'moon-o' | 'moon' | 'motorcycle' | 'mouse-pointer' | 'music' | 'neuter' | 'newspaper-o'
| 'nextcloud-square' | 'nextcloud' | 'nodejs' | 'object-group' | 'object-ungroup'
| 'odnoklassniki-square' | 'odnoklassniki' | 'opencart' | 'open-collective' | 'openid' | 'opera'
| 'optin-monster' | 'orcid' | 'outdent' | 'pagelines' | 'paint-brush' | 'paperclip'
| 'paper-plane-o' | 'paper-plane' | 'paragraph' | 'patreon' | 'pause-circle-o' | 'pause-circle'
| 'pause' | 'paw' | 'paypal' | 'peertube' | 'pencil-square-o' | 'pencil-square' | 'pencil'
| 'percent' | 'phone-square' | 'phone' | 'php' | 'picture-o' | 'pie-chart' | 'pinterest-p'
| 'pinterest-square' | 'pinterest' | 'pixelfed' | 'plane' | 'play-circle-o' | 'play-circle'
| 'play' | 'pleroma' | 'plug' | 'plus-circle' | 'plus-square-o' | 'plus-square' | 'plus'
| 'podcast' | 'power-off' | 'print' | 'product-hunt' | 'puzzle-piece' | 'python' | 'qq'
| 'qrcode' | 'question-circle-o' | 'question-circle' | 'question' | 'quora' | 'quote-left'
| 'quote-right' | 'random' | 'ravelry' | 'react' | 'rebel' | 'recycle' | 'reddit-alien'
| 'reddit-square' | 'reddit' | 'refresh' | 'registered' | 'renren' | 'repeat' | 'reply-all'
| 'reply' | 'researchgate' | 'retweet' | 'road' | 'rocket' | 'rss-square' | 'rss' | 'rub'
| 'safari' | 'scissors' | 'scribd' | 'scuttlebutt' | 'search-minus' | 'search-plus' | 'search'
| 'sellsy' | 'server' | 'shaarli-o' | 'shaarli' | 'share-alt-square' | 'share-alt'
| 'share-square-o' | 'share-square' | 'share' | 'shield' | 'ship' | 'shirtsinbulk'
| 'shopping-bag' | 'shopping-basket' | 'shopping-cart' | 'shower' | 'signalapp' | 'signal'
| 'sign-in' | 'sign-language' | 'sign-out' | 'simplybuilt' | 'sitemap' | 'skyatlas' | 'skype'
| 'slack' | 'sliders' | 'slideshare' | 'smile-o' | 'snapchat-ghost' | 'snapchat-square'
| 'snapchat' | 'snowdrift' | 'snowflake-o' | 'social-home' | 'sort-alpha-asc' | 'sort-alpha-desc'
| 'sort-amount-asc' | 'sort-amount-desc' | 'sort-asc' | 'sort-desc' | 'sort-numeric-asc'
| 'sort-numeric-desc' | 'sort' | 'soundcloud' | 'space-shuttle' | 'spell-check' | 'spinner'
| 'spoon' | 'spotify' | 'square-o' | 'square' | 'stack-exchange' | 'stack-overflow'
| 'star-half-o' | 'star-half' | 'star-o' | 'star' | 'steam-square' | 'steam' | 'step-backward'
| 'step-forward' | 'stethoscope' | 'sticky-note-o' | 'sticky-note' | 'stop-circle-o'
| 'stop-circle' | 'stop' | 'street-view' | 'strikethrough' | 'stumbleupon-circle' | 'stumbleupon'
| 'subscript' | 'subway' | 'suitcase' | 'sun-o' | 'sun' | 'superpowers' | 'superscript'
| 'syncthing' | 'table' | 'tablet' | 'tachometer' | 'tags' | 'tag' | 'tasks' | 'taxi'
| 'telegram' | 'television' | 'tencent-weibo' | 'terminal' | 'text-height' | 'text-width'
| 'themeisle' | 'thermometer-empty' | 'thermometer-full' | 'thermometer-half'
| 'thermometer-quarter' | 'thermometer-three-quarters' | 'th-large' | 'th-list' | 'th'
| 'thumbs-down' | 'thumbs-o-down' | 'thumbs-o-up' | 'thumbs-up' | 'thumb-tack' | 'ticket'
| 'times-circle-o' | 'times-circle' | 'times' | 'tint' | 'tipeee' | 'toggle-off' | 'toggle-on'
| 'tor-onion' | 'trademark' | 'train' | 'transgender-alt' | 'transgender' | 'trash-o' | 'trash'
| 'tree' | 'trello' | 'tripadvisor' | 'trophy' | 'truck' | 'try' | 'tty' | 'tumblr-square'
| 'tumblr' | 'twitch' | 'twitter-square' | 'twitter' | 'umbrella' | 'underline' | 'undo'
| 'universal-access' | 'university' | 'unlock-alt' | 'unlock' | 'unslpash' | 'upload' | 'usb'
| 'usd' | 'user-circle-o' | 'user-circle' | 'user-md' | 'user-o' | 'user-plus' | 'user-secret'
| 'users' | 'user' | 'user-times' | 'venus-double' | 'venus-mars' | 'venus' | 'viacoin'
| 'viadeo-square' | 'viadeo' | 'video-camera' | 'vimeo-square' | 'vimeo' | 'vine' | 'vk'
| 'volume-control-phone' | 'volume-down' | 'volume-mute' | 'volume-off' | 'volume-up' | 'weibo'
| 'weixin' | 'whatsapp' | 'wheelchair-alt' | 'wheelchair' | 'wifi' | 'wikidata' | 'wikipedia-w'
| 'window-close-o' | 'window-close' | 'window-maximize' | 'window-minimize' | 'window-restore'
| 'windows' | 'wire' | 'wordpress' | 'wpbeginner' | 'wpexplorer' | 'wpforms' | 'wrench'
| 'xing-square' | 'xing' | 'xmpp' | 'yahoo' | 'y-combinator' | 'yelp' | 'yoast' | 'youtube-play'
| 'youtube-square' | 'youtube' | 'zotero'
import { ForkAwesomeIcons } from '../../editor-page/editor-pane/tool-bar/emoji-picker/icon-names'
export type IconName = keyof typeof ForkAwesomeIcons
export type IconSize = '2x' | '3x' | '4x' | '5x'

View file

@ -19,7 +19,7 @@ import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-
import { LinkReplacer } from '../replace-components/link-replacer/link-replacer'
import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
import { QuoteOptionsReplacer } from '../replace-components/quote-options/quote-options-replacer'
import { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-replacer'
import { SequenceDiagramReplacer } from '../replace-components/sequence-diagram/sequence-diagram-replacer'
import { TaskListReplacer } from '../replace-components/task-list/task-list-replacer'
import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer'
@ -45,7 +45,7 @@ export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMark
new MarkmapReplacer(),
new VegaReplacer(),
new HighlightedCodeReplacer(),
new QuoteOptionsReplacer(),
new ColoredBlockquoteReplacer(),
new KatexReplacer(),
new TaskListReplacer(onTaskCheckedChange)
], [onImageClick, onTaskCheckedChange, baseUrl])

View file

@ -22,6 +22,7 @@ import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
import { BasicMarkdownItConfigurator } from './BasicMarkdownItConfigurator'
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
constructor(
@ -57,7 +58,15 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
legacySpeakerdeckShortCode,
AsciinemaReplacer.markdownItPlugin,
highlightedCode,
quoteExtra,
quoteExtraColor,
quoteExtra({
quoteLabel: 'name',
icon: 'user'
}),
quoteExtra({
quoteLabel: 'time',
icon: 'clock-o'
}),
(markdownIt) => documentToc(markdownIt, this.onToc))
if (this.onLineMarkers) {
const callback = this.onLineMarkers

View file

@ -10,7 +10,7 @@ export const MarkdownItParserDebugger: MarkdownIt.PluginSimple = (md: MarkdownIt
if (process.env.NODE_ENV !== 'production') {
md.core.ruler.push('test', (state) => {
console.log(state)
return true
return false
})
}
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MarkdownIt from 'markdown-it/lib'
import { parseQuoteExtraTag } from './quote-extra'
const cssColorRegex = /(#(?:[0-9a-f]{2}){2,4}|(?:#[0-9a-f]{3})|black|silver|gray|whitesmoke|maroon|red|purple|fuchsia|green|lime|olivedrab|yellow|navy|blue|teal|aquamarine|orange|aliceblue|antiquewhite|aqua|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|currentcolor|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|goldenrod|gold|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavenderblush|lavender|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olive|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|transparent|turquoise|violet|wheat|white|yellowgreen)/i
export const quoteExtraColor: MarkdownIt.PluginSimple = (md) => {
md.inline.ruler.push(`extraQuote_color`, (state) => {
const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax)
if (!quoteExtraTagValues || quoteExtraTagValues.label !== 'color') {
return false
}
state.pos = quoteExtraTagValues.valueEndIndex + 1
if (!cssColorRegex.exec(quoteExtraTagValues.value)) {
return false
}
state.pos = quoteExtraTagValues.valueEndIndex + 1
const token = state.push('quote-extra-color', '', 0)
token.attrSet('color', quoteExtraTagValues.value)
return true
})
md.renderer.rules['quote-extra-color'] = (tokens, idx) => {
const token = tokens[idx]
const color = token.attrGet('color') ?? ''
return `<span class="quote-extra" data-color='${ color }' style='color: ${ color }'><i class="fa fa-tag"></i></span>`
}
}

View file

@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { parseQuoteExtraTag, QuoteExtraTagValues } from './quote-extra'
describe('Quote extra syntax parser', () => {
it('should parse a valid tag', () => {
const expected: QuoteExtraTagValues = {
labelStartIndex: 1,
labelEndIndex: 4,
valueStartIndex: 5,
valueEndIndex: 8,
label: 'abc',
value: 'def'
}
expect(parseQuoteExtraTag('[abc=def]', 0, 1000))
.toEqual(expected)
})
it('shouldn\'t parse a tag with no opener bracket', () => {
expect(parseQuoteExtraTag('abc=def]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with no closing bracket', () => {
expect(parseQuoteExtraTag('[abc=def', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with no separation character', () => {
expect(parseQuoteExtraTag('[abcdef]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with an empty label', () => {
expect(parseQuoteExtraTag('[=def]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with an empty value', () => {
expect(parseQuoteExtraTag('[abc=]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with an empty body', () => {
expect(parseQuoteExtraTag('[]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with an empty body', () => {
expect(parseQuoteExtraTag('[]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a correct tag if start index isn\'t at the opening bracket', () => {
expect(parseQuoteExtraTag('[abc=def]', 1, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a correct tag if maxPos ends before tag end', () => {
expect(parseQuoteExtraTag('[abc=def]', 0, 1))
.toEqual(undefined)
})
it('shouldn\'t parse a correct tag if start index is after maxPos', () => {
expect(parseQuoteExtraTag(' [abc=def]', 3, 2))
.toEqual(undefined)
})
})

View file

@ -4,44 +4,112 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
import MarkdownIt from 'markdown-it/lib'
import Token from 'markdown-it/lib/token'
import { IconName } from '../../common/fork-awesome/types'
export const quoteExtra: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceQuoteExtraAuthor)
markdownItRegex(markdownIt, replaceQuoteExtraColor)
markdownItRegex(markdownIt, replaceQuoteExtraTime)
export interface QuoteExtraOptions {
quoteLabel: string
icon: IconName
}
const replaceQuoteExtraTime: RegexOptions = {
name: 'quote-extra-time',
regex: /\[time=([^\]]+)]/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra"><i class="fa fa-clock-o mx-1"></i> ${ match }</span>`
export const quoteExtra: (pluginOptions: QuoteExtraOptions) => MarkdownIt.PluginSimple =
(pluginOptions) => (md) => {
md.inline.ruler.push(`extraQuote_${ pluginOptions.quoteLabel }`, (state) => {
const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax)
if (!quoteExtraTagValues || quoteExtraTagValues.label !== pluginOptions.quoteLabel) {
return false
}
state.pos = quoteExtraTagValues.valueEndIndex + 1
const tokens: Token[] = []
state.md.inline.parse(
quoteExtraTagValues.value,
state.md,
state.env,
tokens
)
const token = state.push('quote-extra', '', 0)
token.attrSet('icon', pluginOptions.icon)
token.children = tokens
return true
})
if (md.renderer.rules['quote-extra']) {
return
}
md.renderer.rules['quote-extra'] = (tokens, idx, options: MarkdownIt.Options, env: unknown) => {
const token = tokens[idx]
const innerTokens = token.children
if (!innerTokens) {
return ''
}
const innerHtml = md.renderer.renderInline(innerTokens, options, env)
return `<span class="quote-extra"><i class="fa fa-${ token.attrGet('icon') ?? '' } mx-1"></i>${ innerHtml }</span>`
}
}
export interface QuoteExtraTagValues {
labelStartIndex: number,
labelEndIndex: number,
valueStartIndex: number,
valueEndIndex: number,
label: string,
value: string
}
export const parseQuoteExtraTag = (line: string, start: number, maxPos: number): QuoteExtraTagValues | undefined => {
if (line[start] !== '[') {
return
}
const labelStartIndex = start + 1
const labelEndIndex = parseLabel(line, labelStartIndex, maxPos)
if (!labelEndIndex || labelStartIndex === labelEndIndex) {
return
}
const valueStartIndex = labelEndIndex + 1
const valueEndIndex = parseValue(line, valueStartIndex, maxPos)
if (!valueEndIndex || valueStartIndex === valueEndIndex) {
return
}
return {
labelStartIndex,
labelEndIndex,
valueStartIndex,
valueEndIndex,
label: line.substr(labelStartIndex, labelEndIndex - labelStartIndex),
value: line.substr(valueStartIndex, valueEndIndex - valueStartIndex)
}
}
const cssColorRegex = /\[color=(#(?:[0-9a-f]{2}){2,4}|(?:#[0-9a-f]{3})|black|silver|gray|whitesmoke|maroon|red|purple|fuchsia|green|lime|olivedrab|yellow|navy|blue|teal|aquamarine|orange|aliceblue|antiquewhite|aqua|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|currentcolor|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|goldenrod|gold|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavenderblush|lavender|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olive|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|transparent|turquoise|violet|wheat|white|yellowgreen)]/i
const replaceQuoteExtraColor: RegexOptions = {
name: 'quote-extra-color',
regex: cssColorRegex,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra" data-color='${ match }' style='color: ${ match }'><i class="fa fa-tag"></i></span>`
const parseValue = (line: string, start: number, maxPos: number): number | undefined => {
let level = 1
for (let pos = start; pos <= maxPos; pos += 1) {
const currentCharacter = line[pos]
if (currentCharacter === ']') {
level -= 1
if (level === 0) {
return pos
}
} else if (currentCharacter === '[') {
level += 1
}
}
}
const replaceQuoteExtraAuthor: RegexOptions = {
name: 'quote-extra-name',
regex: /\[name=([^\]]+)]/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra"><i class="fa fa-user mx-1"></i> ${ match }</span>`
const parseLabel = (line: string, start: number, maxPos: number): number | undefined => {
for (let pos = start; pos <= maxPos; pos += 1) {
if (line[pos] === '=') {
return pos
}
}
}

View file

@ -1,12 +1,12 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DomElement } from 'domhandler'
import { ReactElement } from 'react'
import { ComponentReplacer } from '../ComponentReplacer'
import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../ComponentReplacer'
const isColorExtraElement = (node: DomElement | undefined): boolean => {
if (!node || !node.attribs || !node.attribs.class || !node.attribs['data-color']) {
@ -24,8 +24,8 @@ const findQuoteOptionsParent = (nodes: DomElement[]): DomElement | undefined =>
})
}
export class QuoteOptionsReplacer extends ComponentReplacer {
public getReplacement(node: DomElement): ReactElement | undefined {
export class ColoredBlockquoteReplacer extends ComponentReplacer {
public getReplacement(node: DomElement, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): ReactElement | undefined {
if (node.name !== 'blockquote' || !node.children || node.children.length < 1) {
return
}
@ -44,5 +44,6 @@ export class QuoteOptionsReplacer extends ComponentReplacer {
return
}
node.attribs = Object.assign(node.attribs || {}, { style: `border-left-color: ${ attributes['data-color'] };` })
return nativeRenderer()
}
}