diff --git a/docs/migration/migrate_6_0.asciidoc b/docs/migration/migrate_6_0.asciidoc index 15666cda1b8c..e88ddf1d9f23 100644 --- a/docs/migration/migrate_6_0.asciidoc +++ b/docs/migration/migrate_6_0.asciidoc @@ -72,3 +72,10 @@ This is no longer the case. Now, only commas are a valid query separator: e.g. ` *Details:* Since 4.3, index patterns could be configured to do a pre-flight field_stats request before a search in order to determine exact indices that could contain matching documents. Elasticsearch now optimizes searches internally in a similar way and has also removed the field_stats API, so this option was removed from Kibana entirely. *Impact:* No change is required for existing Kibana index patterns. Those previously configured with this option will gracefully use the new Elasticsearch optimizations instead, as will all new index patterns. + +[float] +=== Replace markdown parser `marked` with `markdown-it` +*Details:* Starting in 6.0.0, Kibana will use `markdown-it` to parse markdown text. Kibana switched to `markdown-it` because `marked` is no longer actively maintained. Markdown-it supports CommonMark and GFM (GitHub Flavored Markdown) Tables and Strikethrough. + +*Impact:* There may be slight changes in parsed markdown. Review markdown as needed. + diff --git a/package.json b/package.json index 724bc7c73b42..79cc86a90180 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "less": "2.7.1", "less-loader": "2.2.3", "lodash": "3.10.1", - "marked": "0.3.6", + "markdown-it": "8.3.2", "minimatch": "2.0.10", "mkdirp": "0.5.1", "moment": "2.13.0", diff --git a/src/core_plugins/kibana/public/management/sections/settings/advanced_row.html b/src/core_plugins/kibana/public/management/sections/settings/advanced_row.html index b1b62c526ebf..3ccc1f28d0e7 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/advanced_row.html +++ b/src/core_plugins/kibana/public/management/sections/settings/advanced_row.html @@ -53,6 +53,7 @@ ng-model="conf.unsavedValue" ng-keyup="maybeCancel($event, conf)" elastic-textarea + data-test-subj="unsavedValueMarkdownTextArea" > + diff --git a/src/ui/public/filters/markdown.js b/src/ui/public/filters/markdown.js index 14d9ff7c4a98..9a5c0dc676cc 100644 --- a/src/ui/public/filters/markdown.js +++ b/src/ui/public/filters/markdown.js @@ -1,14 +1,14 @@ -import marked from 'marked'; +import MarkdownIt from 'markdown-it'; import { uiModules } from 'ui/modules'; import 'angular-sanitize'; -marked.setOptions({ - gfm: true, // GitHub-flavored markdown - sanitize: true // Sanitize HTML tags +const markdownIt = new MarkdownIt({ + html: false, + linkify: true }); uiModules .get('kibana', ['ngSanitize']) .filter('markdown', function ($sanitize) { - return md => md ? $sanitize(marked(md)) : ''; + return md => md ? $sanitize(markdownIt.render(md)) : ''; }); diff --git a/src/ui/public/vis/map/service_settings.js b/src/ui/public/vis/map/service_settings.js index c0e8428d96c4..3a670c918a53 100644 --- a/src/ui/public/vis/map/service_settings.js +++ b/src/ui/public/vis/map/service_settings.js @@ -1,17 +1,18 @@ import { uiModules } from 'ui/modules'; import _ from 'lodash'; -import marked from 'marked'; +import MarkdownIt from 'markdown-it'; import { modifyUrl } from 'ui/url'; -marked.setOptions({ - gfm: true, // Github-flavored markdown - sanitize: true // Sanitize HTML tags + +const markdownIt = new MarkdownIt({ + html: false, + linkify: true }); uiModules.get('kibana') .service('serviceSettings', function ($http, $sanitize, mapConfig, tilemapsConfig, kbnVersion) { - const attributionFromConfig = $sanitize(marked(tilemapsConfig.deprecated.config.options.attribution || '')); + const attributionFromConfig = $sanitize(markdownIt.render(tilemapsConfig.deprecated.config.options.attribution || '')); const tmsOptionsFromConfig = _.assign({}, tilemapsConfig.deprecated.config.options, { attribution: attributionFromConfig }); const extendUrl = (url, props) => ( @@ -69,7 +70,7 @@ uiModules.get('kibana') const layers = manifest.data.layers.filter(layer => layer.format === 'geojson'); layers.forEach((layer) => { layer.url = this._extendUrlWithParams(layer.url); - layer.attribution = $sanitize(marked(layer.attribution)); + layer.attribution = $sanitize(markdownIt.render(layer.attribution)); }); return layers; }); @@ -93,7 +94,7 @@ uiModules.get('kibana') } - firstService.attribution = $sanitize(marked(firstService.attribution)); + firstService.attribution = $sanitize(markdownIt.render(firstService.attribution)); firstService.subdomains = firstService.subdomains || []; firstService.url = this._extendUrlWithParams(firstService.url); return firstService; diff --git a/src/ui/public/vislib/lib/handler.js b/src/ui/public/vislib/lib/handler.js index 91fe48497709..8932072eef50 100644 --- a/src/ui/public/vislib/lib/handler.js +++ b/src/ui/public/vislib/lib/handler.js @@ -1,7 +1,7 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import marked from 'marked'; +import MarkdownIt from 'markdown-it'; import { NoResults } from 'ui/errors'; import { Binder } from 'ui/binder'; import { VislibLibLayoutLayoutProvider } from './layout/layout'; @@ -11,6 +11,11 @@ import { VislibLibAxisProvider } from './axis/axis'; import { VislibGridProvider } from './chart_grid'; import { VislibVisualizationsVisTypesProvider } from '../visualizations/vis_types'; +const markdownIt = new MarkdownIt({ + html: false, + linkify: true +}); + export function VisHandlerProvider(Private) { const chartTypes = Private(VislibVisualizationsVisTypesProvider); const Layout = Private(VislibLibLayoutLayoutProvider); @@ -204,7 +209,7 @@ export function VisHandlerProvider(Private) { div.append('div').attr('class', 'item bottom'); } else { - div.append('h4').text(marked.inlineLexer(message, [])); + div.append('h4').text(markdownIt.renderInline(message)); } $(this.el).trigger('renderComplete'); diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.js index 5c1fb567a2c1..419f1079eddc 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.js @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }) { it('should allow setting advanced settings', async function () { await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.setAdvancedSettings('dateFormat:tz', 'America/Phoenix'); + await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'America/Phoenix'); const advancedSetting = await PageObjects.settings.getAdvancedSettings('dateFormat:tz'); expect(advancedSetting).to.be('America/Phoenix'); }); @@ -80,9 +80,24 @@ export default function ({ getService, getPageObjects }) { }); }); + describe('notifications:banner', () => { + it('Should convert notification banner markdown into HTML', async function () { + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.setAdvancedSettingsInput('notifications:banner', '# Welcome to Kibana', 'unsavedValueMarkdownTextArea'); + const bannerValue = await PageObjects.settings.getAdvancedSettings('notifications:banner'); + expect(bannerValue).to.equal('Welcome to Kibana'); + }); + + after('navigate to settings page and clear notifications:banner', async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.clearAdvancedSettings('notifications:banner'); + }); + }); + after(async function () { await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.setAdvancedSettings('dateFormat:tz', 'UTC'); + await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'UTC'); }); }); } diff --git a/test/functional/apps/visualize/_markdown_vis.js b/test/functional/apps/visualize/_markdown_vis.js new file mode 100644 index 000000000000..0a129323df82 --- /dev/null +++ b/test/functional/apps/visualize/_markdown_vis.js @@ -0,0 +1,34 @@ +import expect from 'expect.js'; + +export default function ({ getPageObjects }) { + const PageObjects = getPageObjects(['common', 'visualize', 'header']); + const markdown = ` +# Heading 1 + +

Inline HTML that should not be rendered as html

+ `; + + describe('visualize app', async () => { + before(async function () { + await PageObjects.common.navigateToUrl('visualize', 'new'); + await PageObjects.visualize.clickMarkdownWidget(); + await PageObjects.visualize.setMarkdownTxt(markdown); + await PageObjects.visualize.clickGo(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + describe('markdown vis', async () => { + + it('should render markdown as html', async function () { + const h1Txt = await PageObjects.visualize.getMarkdownBodyDescendentText('h1'); + expect(h1Txt).to.equal('Heading 1'); + }); + + it('should not render html in markdown as html', async function () { + const expected = 'Heading 1\n

Inline HTML that should not be rendered as html

'; + const actual = await PageObjects.visualize.getMarkdownText(); + expect(actual).to.equal(expected); + }); + }); + }); +} diff --git a/test/functional/apps/visualize/index.js b/test/functional/apps/visualize/index.js index f66712109576..932fe575f044 100644 --- a/test/functional/apps/visualize/index.js +++ b/test/functional/apps/visualize/index.js @@ -27,6 +27,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_vertical_bar_chart')); loadTestFile(require.resolve('./_heatmap_chart')); loadTestFile(require.resolve('./_point_series_options')); + loadTestFile(require.resolve('./_markdown_vis')); loadTestFile(require.resolve('./_shared_item')); }); } diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index efd828cc495b..9ee95f323e06 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -33,11 +33,16 @@ export function SettingsPageProvider({ getService, getPageObjects }) { } async getAdvancedSettings(propertyName) { - log.debug('in setAdvancedSettings'); + log.debug('in getAdvancedSettings'); return await testSubjects.getVisibleText(`advancedSetting-${propertyName}-currentValue`); } - async setAdvancedSettings(propertyName, propertyValue) { + async clearAdvancedSettings(propertyName) { + await testSubjects.click(`advancedSetting-${propertyName}-clearButton`); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + + async setAdvancedSettingsSelect(propertyName, propertyValue) { await testSubjects.click(`advancedSetting-${propertyName}-editButton`); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1000); @@ -48,6 +53,16 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); } + async setAdvancedSettingsInput(propertyName, propertyValue, inputSelector) { + await testSubjects.click(`advancedSetting-${propertyName}-editButton`); + await PageObjects.header.waitUntilLoadingHasFinished(); + const input = await testSubjects.find(inputSelector); + await input.clearValue(); + await input.type(propertyValue); + await testSubjects.click(`advancedSetting-${propertyName}-saveButton`); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + async toggleAdvancedSettingCheckbox(propertyName) { await testSubjects.click(`advancedSetting-${propertyName}-editButton`); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index 5315c16a0b12..2dbfa0ca3a90 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -112,6 +112,23 @@ export function VisualizePageProvider({ getService, getPageObjects }) { defaultFindTimeout * 2); } + async setMarkdownTxt(markdownTxt) { + const input = await testSubjects.find('markdownTextarea'); + await input.clearValue(); + await input.type(markdownTxt); + } + + async getMarkdownText() { + const markdownContainer = await testSubjects.find('markdownBody'); + return markdownContainer.getVisibleText(); + } + + async getMarkdownBodyDescendentText(selector) { + const markdownContainer = await testSubjects.find('markdownBody'); + const element = await find.descendantDisplayedByCssSelector(selector, markdownContainer); + return element.getVisibleText(); + } + async setFromTime(timeString) { const input = await find.byCssSelector('input[ng-model="absolute.from"]', defaultFindTimeout * 2); await input.clearValue();