example import type { PluginInfo , MdNode , PluginContext } from '@toast-ui/editor' ; import Chart , { BaseOptions , LineChart , AreaChart , BarChart , PieChart , ColumnChart , } from '@toast-ui/chart' ; import isString from 'tui-code-snippet/type/isString' ; import isUndefined from 'tui-code-snippet/type/isUndefined' ; import inArray from 'tui-code-snippet/array/inArray' ; import extend from 'tui-code-snippet/object/extend' ; import ajax from 'tui-code-snippet/ajax/index.js' ; import { PluginOptions } from '@t/index' ; import csv from './csv' ; import { trimKeepingTabs , isNumeric , clamp } from './util' ; csv . IGNORE_QUOTE_WHITESPACE = false ; csv . IGNORE_RECORD_LENGTH = true ; csv . DETECT_TYPES = false ; const reEOL = /[\n\r]/ ; const reGroupByDelimiter = /([^:]+)?:?(.*)/ ; const DEFAULT_DELIMITER = /\s+/ ; const DELIMITERS = [ ',' , '\t' ] ; const MINIMUM_DELIM_CNT = 2 ; const SUPPORTED_CHART_TYPES = [ 'bar' , 'column' , 'line' , 'area' , 'pie' ] ; const CATEGORY_CHART_TYPES = [ 'line' , 'area' ] ; const DEFAULT_DIMENSION_OPTIONS = { minWidth : 0 , maxWidth : Infinity , minHeight : 0 , maxHeight : Infinity , height : 'auto' , width : 'auto' , } ; const RESERVED_KEYS = [ 'type' , 'url' ] ; const chart = { bar : Chart . barChart , column : Chart . columnChart , area : Chart . areaChart , line : Chart . lineChart , pie : Chart . pieChart , } ; const chartMap = { } ; type ChartType = keyof typeof chart ; export type ChartOptions = BaseOptions & { editorChart : { type ? : ChartType ; url ? : string ; } ; } ; type ChartInstance = BarChart | ColumnChart | AreaChart | LineChart | PieChart ; type ChartData = { categories : string [ ] ; series : { data : number [ ] ; name ? : string ; } [ ] ; } ; type ParserCallback = ( parsedInfo ? : { data : ChartData ; options ? : ChartOptions ; } ) => void ; type OnSuccess = ( res : { data : any ; } ) => void ; export function parse ( text , callback ) { text = trimKeepingTabs ( text ) ; const [ firstTexts , secondTexts ] = text . split ( /\n{2,}/ ) ; const urlOptions = parseToChartOption ( firstTexts ) ; const url = urlOptions ?. editorChart ?. url ; if ( isString ( url ) ) { const success = ( { data } ) => { callback ( { data : parseToChartData ( data ) , options : parseToChartOption ( firstTexts ) } ) ; } ; const error = ( ) => callback ( ) ; ajax . get ( url , { success , error } ) ; } else { const data = parseToChartData ( firstTexts ) ; const options = parseToChartOption ( secondTexts ) ; callback ( { data , options } ) ; } } export function detectDelimiter ( text ) { let delimiter = DEFAULT_DELIMITER ; let delimCnt = 0 ; text = trimKeepingTabs ( text ) ; DELIMITERS . forEach ( ( delim ) => { const matched = text . match ( new RegExp ( delim , 'g' ) ) ! ; if ( matched ?. length > Math . max ( MINIMUM_DELIM_CNT , delimCnt ) ) { delimiter = delim ; delimCnt = matched . length ; } } ) ; return delimiter ; } export function parseToChartData ( text , delimiter ? ) { text = trimKeepingTabs ( text ) ; csv . COLUMN_SEPARATOR = delimiter || detectDelimiter ( text ) ; let dsv = csv . parse ( text ) ; dsv = dsv . map ( ( arr ) => arr . map ( ( val ) => val . trim ( ) ) ) ; const hasLegends = dsv [ 0 ] . filter ( ( _ , i ) => i > 0 ) . reduce ( ( hasNaN , item ) => hasNaN || ! isNumeric ( item ) , false ) ; const legends = hasLegends ? dsv . shift ( ) ! : [ ] ; const hasCategories = dsv . slice ( 1 ) . reduce ( ( hasNaN , row ) => hasNaN || ! isNumeric ( row [ 0 ] ) , false ) ; const categories = hasCategories ? dsv . map ( ( arr ) => arr . shift ( ) ! ) : [ ] ; if ( hasCategories ) { legends . shift ( ) ; } const tdsv = dsv [ 0 ] . map ( ( _ , i ) => dsv . map ( ( x ) => parseFloat ( x [ i ] ) ) ) ; const series = tdsv . map ( ( data , i ) => hasLegends ? { name : legends [ i ] , data , } : { data , } ) ; return { categories , series } ; } function createOptionKeys ( keyString ) { const keys = keyString . trim ( ) . split ( '.' ) ; const [ topKey ] = keys ; if ( inArray ( topKey , RESERVED_KEYS ) >= 0 ) { keys . unshift ( 'editorChart' ) ; } else if ( keys . length === 1 ) { keys . unshift ( 'chart' ) ; } else if ( topKey === 'x' || topKey === 'y' ) { keys [ 0 ] = `${ topKey }Axis` ; } return keys ; } export function parseToChartOption ( text ) { const options = { } ; if ( ! isUndefined ( text ) ) { const lineTexts = text . split ( reEOL ) ; lineTexts . forEach ( ( lineText ) => { const matched = lineText . match ( reGroupByDelimiter ) ; if ( matched ) { let [ , keyString , value ] = matched ; if ( value ) { try { value = JSON . parse ( value . trim ( ) ) ; } catch ( e ) { value = value . trim ( ) ; } const keys = createOptionKeys ( keyString ) ; let refOptions = options ; keys . forEach ( ( key , index ) => { refOptions [ key ] = refOptions [ key ] || ( keys . length - 1 === index ? value : { } ) ; refOptions = refOptions [ key ] ; } ) ; } } } ) ; } return options as ChartOptions ; } function getAdjustedDimension ( size , containerWidth ) { return size === 'auto' ? containerWidth : size ; } function getChartDimension ( chartOptions , pluginOptions , chartContainer ) { const dimensionOptions = extend ( { ... DEFAULT_DIMENSION_OPTIONS } , pluginOptions ) ; const { maxWidth , minWidth , maxHeight , minHeight } = dimensionOptions ; const { width : containerWidth } = chartContainer . getBoundingClientRect ( ) ; let { width = dimensionOptions . width , height = dimensionOptions . height } = chartOptions . chart ! ; width = getAdjustedDimension ( width , containerWidth ) ; height = getAdjustedDimension ( height , containerWidth ) ; return { width : clamp ( width , minWidth , maxWidth ) , height : clamp ( height , minHeight , maxHeight ) , } ; } export function setDefaultOptions ( chartOptions , pluginOptions , chartContainer ) { chartOptions = extend ( { editorChart : { } , chart : { } , exportMenu : { } , } , chartOptions ) ; const { width , height } = getChartDimension ( chartOptions , pluginOptions , chartContainer ) ; chartOptions . chart ! . width = width ; chartOptions . chart ! . height = height ; chartOptions . editorChart . type = chartOptions . editorChart . type || 'column' ; chartOptions . exportMenu ! . visible = ! ! chartOptions . exportMenu ! . visible ; return chartOptions ; } function destroyChart ( ) { Object . keys ( chartMap ) . forEach ( ( id ) => { const container = document . querySelector < HTMLElement > ( `[data-chart-id=${ id }]` ) ; if ( ! container ) { chartMap [ id ] . destroy ( ) ; delete chartMap [ id ] ; } } ) ; } function renderChart ( id , text , usageStatistics , pluginOptions ) { const chartContainer = document . querySelector < HTMLElement > ( `[data-chart-id=${ id }]` ) ! ; destroyChart ( ) ; if ( chartContainer ) { try { parse ( text , ( parsedInfo ) => { const { data , options } = parsedInfo || { } ; const chartOptions = setDefaultOptions ( options ! , pluginOptions , chartContainer ) ; const chartType = chartOptions . editorChart . type ! ; if ( ! data || ( CATEGORY_CHART_TYPES . indexOf ( chartType ) > - 1 && data . categories . length !== data . series [ 0 ] . data . length ) ) { chartContainer . innerHTML = 'invalid chart data' ; } else if ( SUPPORTED_CHART_TYPES . indexOf ( chartType ) < 0 ) { chartContainer . innerHTML = `invalid chart type. type: bar, column, line, area, pie` ; } else { const toastuiChart = chart [ chartType ] ; chartOptions . usageStatistics = usageStatistics ; chartMap [ id ] = toastuiChart ( { el : chartContainer , data , options : chartOptions } ) ; } } ) ; } catch ( e ) { chartContainer . innerHTML = 'invalid chart data' ; } } } function generateId ( ) { return `chart-${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 10 ) }` ; } param { Object } context param options param { number } options . minWidth param { number } options . minHeight param { number } options . maxWidth param { number } options . maxHeight param { number | string } options . width param { number | string } options . height export default function chartPlugin ( { usageStatistics = true } , options ) { return { toHTMLRenderers : { chart ( node ) { const id = generateId ( ) ; setTimeout ( ( ) => { renderChart ( id , node . literal ! , usageStatistics , options ) ; } ) ; return [ { type : 'openTag' , tagName : 'div' , outerNewLine : true , attributes : { 'data-chart-id' : id } , } , { type : 'closeTag' , tagName : 'div' , outerNewLine : true } , ] ; } , } , } ; }