@@ -0,0 +1,22 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
declare module 'remark-slug' { | |||
export default function slug(): any; | |||
} |
@@ -65,11 +65,13 @@ | |||
line-height: 1.5; | |||
} | |||
.cut-margins > *:first-child { | |||
.cut-margins > *:first-child, | |||
.cut-margins > .markdown-content > *:first-child { | |||
margin-top: 0 !important; | |||
} | |||
.cut-margins > *:last-child { | |||
.cut-margins > *:last-child, | |||
.cut-margins > .markdown-content > *:last-child { | |||
margin-bottom: 0 !important; | |||
} | |||
@@ -100,6 +100,7 @@ export default class App extends React.PureComponent<Props> { | |||
className="documentation-content cut-margins boxed-group-inner" | |||
content={page.content} | |||
displayH1={true} | |||
stickyToc={true} | |||
/> | |||
</div> | |||
</div> |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import SearchResultEntry, { | |||
SearchResult, | |||
SearchResultText, | |||
SearchResultTitle, | |||
SearchResultTokens | |||
@@ -86,7 +87,7 @@ describe('SearchResultTokens', () => { | |||
}); | |||
}); | |||
function mockSearchResult(overrides = {}) { | |||
function mockSearchResult(overrides: Partial<SearchResult> = {}) { | |||
return { | |||
page: { | |||
content: '', |
@@ -31,11 +31,12 @@ | |||
} | |||
.documentation-content.markdown { | |||
position: relative; | |||
font-size: 16px; | |||
line-height: 1.7; | |||
} | |||
.documentation-content.markdown > h1 { | |||
.documentation-content.markdown .documentation-title { | |||
font-size: 24px; | |||
padding-top: var(--gridSize); | |||
margin-bottom: 2em; | |||
@@ -116,3 +117,47 @@ | |||
.documentation-content.markdown .collapse-container *:last-child { | |||
margin-bottom: 0; | |||
} | |||
.markdown.has-toc { | |||
display: flex; | |||
padding-right: var(--gridSize); | |||
} | |||
.markdown.has-toc .markdown-content { | |||
flex-shrink: 1; | |||
} | |||
.markdown-toc { | |||
flex: 0 0 240px; | |||
} | |||
.markdown-toc-content { | |||
margin-left: calc(4 * var(--gridSize)); | |||
padding: 0 var(--gridSize); | |||
font-size: var(--baseFontSize); | |||
background: white; | |||
position: sticky; | |||
top: calc(20px + var(--globalNavHeight)); | |||
} | |||
.markdown-toc-content h4 { | |||
margin: 0 var(--gridSize) var(--gridSize) var(--gridSize); | |||
font-size: var(--mediumFontSize); | |||
} | |||
.markdown-toc-content a { | |||
display: block; | |||
color: var(--sonarcloudBlack900); | |||
padding: calc(var(--gridSize) / 2) var(--gridSize); | |||
border: 1px solid white; | |||
line-height: 1.2; | |||
transition: none; | |||
} | |||
.markdown-toc a:hover { | |||
border-color: var(--blue); | |||
} | |||
.markdown-toc a.active { | |||
font-weight: bold; | |||
} |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import { mount } from 'enzyme'; | |||
import ScreenPositionFixer from '../ScreenPositionFixer'; | |||
import { resizeWindowTo } from '../../../helpers/testUtils'; | |||
import { resizeWindowTo, setNodeRect } from '../../../helpers/testUtils'; | |||
jest.mock('lodash', () => { | |||
const lodash = require.requireActual('lodash'); | |||
@@ -33,7 +33,7 @@ jest.mock('react-dom', () => ({ | |||
})); | |||
beforeEach(() => { | |||
setNodeRect({ width: 50, height: 50, left: 50, top: 50 }); | |||
setNodeRect({ left: 50, top: 50 }); | |||
resizeWindowTo(1000, 1000); | |||
}); | |||
@@ -41,18 +41,18 @@ it('should fix position', () => { | |||
const renderer = jest.fn(() => <div />); | |||
mount(<ScreenPositionFixer>{renderer}</ScreenPositionFixer>); | |||
setNodeRect({ width: 50, height: 50, left: 50, top: 50 }); | |||
setNodeRect({ left: 50, top: 50 }); | |||
resizeWindowTo(75, 1000); | |||
expect(renderer).toHaveBeenLastCalledWith({ leftFix: -29, topFix: 0 }); | |||
resizeWindowTo(1000, 75); | |||
expect(renderer).toHaveBeenLastCalledWith({ leftFix: 0, topFix: -29 }); | |||
setNodeRect({ width: 50, height: 50, left: -10, top: 50 }); | |||
setNodeRect({ left: -10, top: 50 }); | |||
resizeWindowTo(1000, 1000); | |||
expect(renderer).toHaveBeenLastCalledWith({ leftFix: 14, topFix: 0 }); | |||
setNodeRect({ width: 50, height: 50, left: 50, top: -10 }); | |||
setNodeRect({ left: 50, top: -10 }); | |||
resizeWindowTo(); | |||
expect(renderer).toHaveBeenLastCalledWith({ leftFix: 0, topFix: 14 }); | |||
}); | |||
@@ -87,10 +87,3 @@ it('should re-position when window is resized', () => { | |||
resizeWindowTo(); | |||
expect(renderer).toHaveBeenCalledTimes(3); | |||
}); | |||
function setNodeRect(rect: { width: number; height: number; left: number; top: number }) { | |||
const findDOMNode = require('react-dom').findDOMNode as jest.Mock<any>; | |||
const element = document.createElement('div'); | |||
Object.defineProperty(element, 'getBoundingClientRect', { value: () => rect }); | |||
findDOMNode.mockReturnValue(element); | |||
} |
@@ -21,9 +21,11 @@ import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import remark from 'remark'; | |||
import reactRenderer from 'remark-react'; | |||
import slug from 'remark-slug'; | |||
import remarkCustomBlocks from 'remark-custom-blocks'; | |||
import DocLink from './DocLink'; | |||
import DocImg from './DocImg'; | |||
import DocToc from './DocToc'; | |||
import DocTooltipLink from './DocTooltipLink'; | |||
import remarkToc from './plugins/remark-toc'; | |||
import DocCollapsibleBlock from './DocCollapsibleBlock'; | |||
@@ -36,6 +38,7 @@ interface Props { | |||
content: string | undefined; | |||
displayH1?: boolean; | |||
isTooltip?: boolean; | |||
stickyToc?: boolean; | |||
} | |||
export default class DocMarkdownBlock extends React.PureComponent<Props> { | |||
@@ -43,46 +46,62 @@ export default class DocMarkdownBlock extends React.PureComponent<Props> { | |||
handleAnchorClick = (href: string, event: React.MouseEvent<HTMLAnchorElement>) => { | |||
if (this.node) { | |||
const element = this.node.querySelector(`#${href.substr(1)}`); | |||
const element = this.node.querySelector(href); | |||
if (element) { | |||
event.preventDefault(); | |||
scrollToElement(element, { bottomOffset: window.innerHeight - 80 }); | |||
if (history.pushState) { | |||
history.pushState(null, '', href); | |||
} | |||
} | |||
} | |||
}; | |||
render() { | |||
const { childProps, content, className, displayH1, isTooltip } = this.props; | |||
const { childProps, content, className, displayH1, stickyToc, isTooltip } = this.props; | |||
const parsed = separateFrontMatter(content || ''); | |||
let filteredContent = filterContent(parsed.content); | |||
const tocContent = filteredContent; | |||
const md = remark(); | |||
// TODO find a way to replace these custom blocks with real Alert components | |||
md.use(remarkCustomBlocks, { | |||
danger: { classes: 'alert alert-danger' }, | |||
warning: { classes: 'alert alert-warning' }, | |||
info: { classes: 'alert alert-info' }, | |||
success: { classes: 'alert alert-success' }, | |||
collapse: { classes: 'collapse' } | |||
}) | |||
.use(reactRenderer, { | |||
remarkReactComponents: { | |||
div: Block, | |||
// use custom link to render documentation anchors | |||
a: isTooltip | |||
? withChildProps(DocTooltipLink, childProps) | |||
: withChildProps(DocLink, { onAnchorClick: this.handleAnchorClick }), | |||
// use custom img tag to render documentation images | |||
img: DocImg | |||
}, | |||
toHast: {}, | |||
sanitize: false | |||
}) | |||
.use(slug); | |||
if (stickyToc) { | |||
filteredContent = filteredContent.replace(/#*\s*(toc|table[ -]of[ -]contents?).*/i, ''); | |||
} else { | |||
md.use(remarkToc, { maxDepth: 3 }); | |||
} | |||
return ( | |||
<div className={classNames('markdown', className)} ref={ref => (this.node = ref)}> | |||
{displayH1 && <h1>{parsed.frontmatter.title}</h1>} | |||
{ | |||
remark() | |||
.use(remarkToc, { maxDepth: 3 }) | |||
// TODO find a way to replace these custom blocks with real Alert components | |||
.use(remarkCustomBlocks, { | |||
danger: { classes: 'alert alert-danger' }, | |||
warning: { classes: 'alert alert-warning' }, | |||
info: { classes: 'alert alert-info' }, | |||
success: { classes: 'alert alert-success' }, | |||
collapse: { classes: 'collapse' } | |||
}) | |||
.use(reactRenderer, { | |||
remarkReactComponents: { | |||
div: Block, | |||
// use custom link to render documentation anchors | |||
a: isTooltip | |||
? withChildProps(DocTooltipLink, childProps) | |||
: withChildProps(DocLink, { onAnchorClick: this.handleAnchorClick }), | |||
// use custom img tag to render documentation images | |||
img: DocImg | |||
}, | |||
toHast: {}, | |||
sanitize: false | |||
}) | |||
.processSync(filterContent(parsed.content)).contents | |||
} | |||
<div | |||
className={classNames('markdown', className, { 'has-toc': stickyToc })} | |||
ref={ref => (this.node = ref)}> | |||
<div className="markdown-content"> | |||
{displayH1 && <h1 className="documentation-title">{parsed.frontmatter.title}</h1>} | |||
{md.processSync(filteredContent).contents} | |||
</div> | |||
{stickyToc && <DocToc content={tocContent} onAnchorClick={this.handleAnchorClick} />} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,155 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import remark from 'remark'; | |||
import reactRenderer from 'remark-react'; | |||
import { findDOMNode } from 'react-dom'; | |||
import * as classNames from 'classnames'; | |||
import { debounce, memoize } from 'lodash'; | |||
import onlyToc from './plugins/remark-only-toc'; | |||
import { translate } from '../../helpers/l10n'; | |||
interface Props { | |||
content: string; | |||
onAnchorClick: (href: string, event: React.MouseEvent<HTMLAnchorElement>) => void; | |||
} | |||
interface State { | |||
anchors: AnchorObject[]; | |||
highlightAnchor?: string; | |||
} | |||
interface AnchorObject { | |||
href: string; | |||
title: string; | |||
} | |||
export default class DocToc extends React.PureComponent<Props, State> { | |||
debouncedScrollHandler: () => void; | |||
node: HTMLDivElement | null = null; | |||
state: State = { anchors: [] }; | |||
static getAnchors = memoize(content => { | |||
const file: { contents: JSX.Element } = remark() | |||
.use(reactRenderer) | |||
.use(onlyToc) | |||
.processSync(content); | |||
if (file && file.contents.props.children) { | |||
let list = file.contents; | |||
let limit = 10; | |||
while (limit && list.props.children.length && list.type !== 'ul') { | |||
list = list.props.children[0]; | |||
limit--; | |||
} | |||
if (list.type === 'ul' && list.props.children.length) { | |||
return list.props.children | |||
.map((li: JSX.Element | string) => { | |||
if (typeof li === 'string') { | |||
return null; | |||
} | |||
const anchor = li.props.children[0]; | |||
return { | |||
href: anchor.props.href, | |||
title: anchor.props.children[0] | |||
} as AnchorObject; | |||
}) | |||
.filter((item: AnchorObject | null) => item); | |||
} | |||
} | |||
return []; | |||
}); | |||
static getDerivedStateFromProps(props: Props) { | |||
const { content } = props; | |||
return { anchors: DocToc.getAnchors(content) }; | |||
} | |||
constructor(props: Props) { | |||
super(props); | |||
this.debouncedScrollHandler = debounce(this.scrollHandler); | |||
} | |||
componentDidMount() { | |||
window.addEventListener('scroll', this.debouncedScrollHandler, true); | |||
this.scrollHandler(); | |||
} | |||
componentWillUnmount() { | |||
window.removeEventListener('scroll', this.debouncedScrollHandler, true); | |||
} | |||
scrollHandler = () => { | |||
// eslint-disable-next-line react/no-find-dom-node | |||
const node = findDOMNode(this) as HTMLElement; | |||
if (!node || !node.parentNode) { | |||
return; | |||
} | |||
const headings: NodeListOf<HTMLHeadingElement> = node.parentNode.querySelectorAll('h2[id]'); | |||
const scrollTop = window.pageYOffset || document.body.scrollTop; | |||
let highlightAnchor; | |||
for (let i = 0, len = headings.length; i < len; i++) { | |||
if (headings.item(i).offsetTop > scrollTop + 120) { | |||
break; | |||
} | |||
highlightAnchor = `#${headings.item(i).id}`; | |||
} | |||
this.setState({ | |||
highlightAnchor | |||
}); | |||
}; | |||
render() { | |||
const { anchors, highlightAnchor } = this.state; | |||
if (anchors.length === 0) { | |||
return null; | |||
} | |||
return ( | |||
<div className="markdown-toc"> | |||
<div className="markdown-toc-content"> | |||
<h4>{translate('documentation.on_this_page')}</h4> | |||
{anchors.map(anchor => { | |||
return ( | |||
<a | |||
className={classNames({ active: highlightAnchor === anchor.href })} | |||
href={anchor.href} | |||
key={anchor.title} | |||
onClick={event => { | |||
this.props.onAnchorClick(anchor.href, event); | |||
}}> | |||
{anchor.title} | |||
</a> | |||
); | |||
})} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -22,7 +22,29 @@ import { shallow } from 'enzyme'; | |||
import DocMarkdownBlock from '../DocMarkdownBlock'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
// mock `remark` and `remark-react` to work around the issue with cjs imports | |||
const CONTENT_WITH_TOC = ` | |||
## Table of Contents | |||
## Lorem ipsum | |||
Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
## Sit amet | |||
### Maecenas diam | |||
Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. | |||
### Integer | |||
At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. | |||
## Nam blandit | |||
Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
`; | |||
// mock `remark` & co to work around the issue with cjs imports | |||
jest.mock('remark', () => { | |||
const remark = require.requireActual('remark'); | |||
return { default: remark }; | |||
@@ -33,18 +55,23 @@ jest.mock('remark-react', () => { | |||
return { default: remarkReact }; | |||
}); | |||
jest.mock('remark-slug', () => { | |||
const remarkSlug = require.requireActual('remark-slug'); | |||
return { default: remarkSlug }; | |||
}); | |||
jest.mock('../../../helpers/system', () => ({ | |||
getInstance: jest.fn(), | |||
isSonarCloud: jest.fn() | |||
})); | |||
it('should render simple markdown', () => { | |||
expect(shallow(<DocMarkdownBlock content="this is *bold* text" />)).toMatchSnapshot(); | |||
expect(shallowRender({ content: 'this is *bold* text' })).toMatchSnapshot(); | |||
}); | |||
it('should use custom component for links', () => { | |||
expect( | |||
shallow(<DocMarkdownBlock content="some [link](/quality-profiles)" />).find('withChildProps') | |||
shallowRender({ content: 'some [link](/quality-profiles)' }).find('withChildProps') | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -73,20 +100,34 @@ static | |||
text`; | |||
(isSonarCloud as jest.Mock).mockImplementation(() => false); | |||
expect(shallow(<DocMarkdownBlock content={content} />)).toMatchSnapshot(); | |||
expect(shallowRender({ content })).toMatchSnapshot(); | |||
(isSonarCloud as jest.Mock).mockImplementation(() => true); | |||
expect(shallow(<DocMarkdownBlock content={content} />)).toMatchSnapshot(); | |||
expect(shallowRender({ content })).toMatchSnapshot(); | |||
}); | |||
it('should render with custom props for links', () => { | |||
expect( | |||
shallow( | |||
<DocMarkdownBlock | |||
childProps={{ foo: 'bar' }} | |||
content="some [link](#quality-profiles)" | |||
isTooltip={true} | |||
/> | |||
).find('withChildProps') | |||
shallowRender({ | |||
childProps: { foo: 'bar' }, | |||
content: 'some [link](#quality-profiles)', | |||
isTooltip: true | |||
}).find('withChildProps') | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render a TOC if available', () => { | |||
const wrapper = shallowRender({ content: CONTENT_WITH_TOC }); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('ul').exists()).toBe(true); | |||
}); | |||
it('should render a sticky TOC if available', () => { | |||
const wrapper = shallowRender({ content: CONTENT_WITH_TOC, stickyToc: true }); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('DocToc').exists()).toBe(true); | |||
}); | |||
function shallowRender(props: Partial<DocMarkdownBlock['props']> = {}) { | |||
return shallow(<DocMarkdownBlock content="" {...props} />); | |||
} |
@@ -0,0 +1,131 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { mount } from 'enzyme'; | |||
import DocToc from '../DocToc'; | |||
import { click, scrollTo } from '../../../helpers/testUtils'; | |||
const OFFSET = 300; | |||
const CONTENT_NO_TOC = ` | |||
## Lorem ipsum | |||
Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
## Sit amet | |||
### Maecenas diam | |||
Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. | |||
### Integer | |||
At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. | |||
## Nam blandit | |||
Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
`; | |||
const CONTENT_WITH_TOC = ` | |||
## toc | |||
${CONTENT_NO_TOC} | |||
`; | |||
jest.mock('remark', () => { | |||
const remark = require.requireActual('remark'); | |||
return { default: remark }; | |||
}); | |||
jest.mock('remark-react', () => { | |||
const remarkReact = require.requireActual('remark-react'); | |||
return { default: remarkReact }; | |||
}); | |||
jest.mock('lodash', () => { | |||
const lodash = require.requireActual('lodash'); | |||
lodash.debounce = (fn: any) => fn; | |||
return lodash; | |||
}); | |||
jest.mock('react-dom', () => ({ | |||
findDOMNode: jest.fn() | |||
})); | |||
it('should render correctly', () => { | |||
const wrapper = renderComponent(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render correctly if no TOC is available', () => { | |||
const wrapper = renderComponent({ content: CONTENT_NO_TOC }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should trigger the handler when an anchor is clicked', () => { | |||
const onAnchorClick = jest.fn(); | |||
const wrapper = renderComponent({ onAnchorClick }); | |||
click(wrapper.find('a[href="#sit-amet"]')); | |||
expect(onAnchorClick).toBeCalled(); | |||
}); | |||
it('should highlight anchors when scrolling', () => { | |||
mockDomEnv(); | |||
const wrapper = renderComponent(); | |||
scrollTo({ top: OFFSET }); | |||
expect(wrapper.state('highlightAnchor')).toEqual('#lorem-ipsum'); | |||
scrollTo({ top: OFFSET * 3 }); | |||
expect(wrapper.state('highlightAnchor')).toEqual('#nam-blandit'); | |||
}); | |||
function renderComponent(props: Partial<DocToc['props']> = {}) { | |||
return mount(<DocToc content={CONTENT_WITH_TOC} onAnchorClick={jest.fn()} {...props} />); | |||
} | |||
function mockDomEnv() { | |||
const findDOMNode = require('react-dom').findDOMNode as jest.Mock<any>; | |||
const parent = document.createElement('div'); | |||
const element = document.createElement('div'); | |||
parent.appendChild(element); | |||
let offset = OFFSET; | |||
(CONTENT_WITH_TOC.match(/^## .+$/gm) as Array<string>).forEach(match => { | |||
if (/toc/.test(match)) { | |||
return; | |||
} | |||
const slug = match | |||
.replace(/^#+ */, '') | |||
.replace(' ', '-') | |||
.toLowerCase() | |||
.trim(); | |||
const heading = document.createElement('h2'); | |||
heading.id = slug; | |||
Object.defineProperty(heading, 'offsetTop', { value: offset }); | |||
offset += OFFSET; | |||
parent.appendChild(heading); | |||
}); | |||
findDOMNode.mockReturnValue(element); | |||
} |
@@ -4,43 +4,47 @@ exports[`should cut sonarqube/sonarcloud/static content 1`] = ` | |||
<div | |||
className="markdown" | |||
> | |||
<Block | |||
key="h-1" | |||
<div | |||
className="markdown-content" | |||
> | |||
<p | |||
key="h-2" | |||
<Block | |||
key="h-1" | |||
> | |||
some | |||
</p> | |||
<p | |||
key="h-2" | |||
> | |||
some | |||
</p> | |||
<p | |||
key="h-3" | |||
> | |||
sonarqube | |||
</p> | |||
<p | |||
key="h-3" | |||
> | |||
sonarqube | |||
</p> | |||
<p | |||
key="h-4" | |||
> | |||
long | |||
</p> | |||
<p | |||
key="h-4" | |||
> | |||
long | |||
</p> | |||
<p | |||
key="h-5" | |||
> | |||
multiline | |||
</p> | |||
<p | |||
key="h-5" | |||
> | |||
multiline | |||
</p> | |||
<p | |||
key="h-6" | |||
> | |||
text | |||
</p> | |||
</Block> | |||
<p | |||
key="h-6" | |||
> | |||
text | |||
</p> | |||
</Block> | |||
</div> | |||
</div> | |||
`; | |||
@@ -48,29 +52,311 @@ exports[`should cut sonarqube/sonarcloud/static content 2`] = ` | |||
<div | |||
className="markdown" | |||
> | |||
<Block | |||
key="h-1" | |||
<div | |||
className="markdown-content" | |||
> | |||
<p | |||
key="h-2" | |||
<Block | |||
key="h-1" | |||
> | |||
some | |||
</p> | |||
<p | |||
key="h-2" | |||
> | |||
some | |||
</p> | |||
<p | |||
key="h-3" | |||
<p | |||
key="h-3" | |||
> | |||
sonarcloud | |||
</p> | |||
<p | |||
key="h-4" | |||
> | |||
text | |||
</p> | |||
</Block> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render a TOC if available 1`] = ` | |||
<div | |||
className="markdown" | |||
> | |||
<div | |||
className="markdown-content" | |||
> | |||
<Block | |||
key="h-1" | |||
> | |||
sonarcloud | |||
</p> | |||
<h2 | |||
id="table-of-contents" | |||
key="h-2" | |||
> | |||
Table of Contents | |||
</h2> | |||
<ul | |||
key="h-3" | |||
> | |||
<li | |||
key="h-4" | |||
> | |||
<withChildProps | |||
href="#lorem-ipsum" | |||
key="h-5" | |||
> | |||
Lorem ipsum | |||
</withChildProps> | |||
</li> | |||
<li | |||
key="h-6" | |||
> | |||
<p | |||
key="h-7" | |||
> | |||
<withChildProps | |||
href="#sit-amet" | |||
key="h-8" | |||
> | |||
Sit amet | |||
</withChildProps> | |||
</p> | |||
<ul | |||
key="h-9" | |||
> | |||
<li | |||
key="h-10" | |||
> | |||
<withChildProps | |||
href="#maecenas-diam" | |||
key="h-11" | |||
> | |||
Maecenas diam | |||
</withChildProps> | |||
</li> | |||
<li | |||
key="h-12" | |||
> | |||
<withChildProps | |||
href="#integer" | |||
key="h-13" | |||
> | |||
Integer | |||
</withChildProps> | |||
</li> | |||
</ul> | |||
</li> | |||
<li | |||
key="h-14" | |||
> | |||
<withChildProps | |||
href="#nam-blandit" | |||
key="h-15" | |||
> | |||
Nam blandit | |||
</withChildProps> | |||
</li> | |||
</ul> | |||
<h2 | |||
id="lorem-ipsum" | |||
key="h-16" | |||
> | |||
Lorem ipsum | |||
</h2> | |||
<p | |||
key="h-17" | |||
> | |||
Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
</p> | |||
<h2 | |||
id="sit-amet" | |||
key="h-18" | |||
> | |||
Sit amet | |||
</h2> | |||
<h3 | |||
id="maecenas-diam" | |||
key="h-19" | |||
> | |||
Maecenas diam | |||
</h3> | |||
<p | |||
key="h-4" | |||
<p | |||
key="h-20" | |||
> | |||
Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. | |||
</p> | |||
<h3 | |||
id="integer" | |||
key="h-21" | |||
> | |||
Integer | |||
</h3> | |||
<p | |||
key="h-22" | |||
> | |||
At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. | |||
</p> | |||
<h2 | |||
id="nam-blandit" | |||
key="h-23" | |||
> | |||
Nam blandit | |||
</h2> | |||
<p | |||
key="h-24" | |||
> | |||
Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
</p> | |||
</Block> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render a sticky TOC if available 1`] = ` | |||
<div | |||
className="markdown has-toc" | |||
> | |||
<div | |||
className="markdown-content" | |||
> | |||
<Block | |||
key="h-1" | |||
> | |||
text | |||
</p> | |||
</Block> | |||
<h2 | |||
id="lorem-ipsum" | |||
key="h-2" | |||
> | |||
Lorem ipsum | |||
</h2> | |||
<p | |||
key="h-3" | |||
> | |||
Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
</p> | |||
<h2 | |||
id="sit-amet" | |||
key="h-4" | |||
> | |||
Sit amet | |||
</h2> | |||
<h3 | |||
id="maecenas-diam" | |||
key="h-5" | |||
> | |||
Maecenas diam | |||
</h3> | |||
<p | |||
key="h-6" | |||
> | |||
Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. | |||
</p> | |||
<h3 | |||
id="integer" | |||
key="h-7" | |||
> | |||
Integer | |||
</h3> | |||
<p | |||
key="h-8" | |||
> | |||
At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. | |||
</p> | |||
<h2 | |||
id="nam-blandit" | |||
key="h-9" | |||
> | |||
Nam blandit | |||
</h2> | |||
<p | |||
key="h-10" | |||
> | |||
Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
</p> | |||
</Block> | |||
</div> | |||
<DocToc | |||
content=" | |||
## Table of Contents | |||
## Lorem ipsum | |||
Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
## Sit amet | |||
### Maecenas diam | |||
Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. | |||
### Integer | |||
At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. | |||
## Nam blandit | |||
Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
" | |||
onAnchorClick={[Function]} | |||
/> | |||
</div> | |||
`; | |||
@@ -78,21 +364,25 @@ exports[`should render simple markdown 1`] = ` | |||
<div | |||
className="markdown" | |||
> | |||
<Block | |||
key="h-1" | |||
<div | |||
className="markdown-content" | |||
> | |||
<p | |||
key="h-2" | |||
<Block | |||
key="h-1" | |||
> | |||
this is | |||
<em | |||
key="h-3" | |||
<p | |||
key="h-2" | |||
> | |||
bold | |||
</em> | |||
text | |||
</p> | |||
</Block> | |||
this is | |||
<em | |||
key="h-3" | |||
> | |||
bold | |||
</em> | |||
text | |||
</p> | |||
</Block> | |||
</div> | |||
</div> | |||
`; | |||
@@ -0,0 +1,91 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<DocToc | |||
content=" | |||
## toc | |||
## Lorem ipsum | |||
Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
## Sit amet | |||
### Maecenas diam | |||
Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. | |||
### Integer | |||
At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. | |||
## Nam blandit | |||
Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
" | |||
onAnchorClick={[MockFunction]} | |||
> | |||
<div | |||
className="markdown-toc" | |||
> | |||
<div | |||
className="markdown-toc-content" | |||
> | |||
<h4> | |||
documentation.on_this_page | |||
</h4> | |||
<a | |||
className="" | |||
href="#lorem-ipsum" | |||
key="Lorem ipsum" | |||
onClick={[Function]} | |||
> | |||
Lorem ipsum | |||
</a> | |||
<a | |||
className="" | |||
href="#sit-amet" | |||
key="Sit amet" | |||
onClick={[Function]} | |||
> | |||
Sit amet | |||
</a> | |||
<a | |||
className="" | |||
href="#nam-blandit" | |||
key="Nam blandit" | |||
onClick={[Function]} | |||
> | |||
Nam blandit | |||
</a> | |||
</div> | |||
</div> | |||
</DocToc> | |||
`; | |||
exports[`should render correctly if no TOC is available 1`] = ` | |||
<DocToc | |||
content=" | |||
## Lorem ipsum | |||
Quisque vitae tincidunt felis. Nam blandit risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
## Sit amet | |||
### Maecenas diam | |||
Velit, vestibulum nec ultrices id, mollis eget arcu. Sed dapibus, sapien ut auctor consectetur, mi tortor vestibulum ante, eget dapibus lacus risus. | |||
### Integer | |||
At cursus turpis. Aenean at elit fringilla, porttitor mi eget, dapibus nisi. Donec quis congue odio. | |||
## Nam blandit | |||
Risus placerat, efficitur enim ut, pellentesque sem. Mauris non lorem auctor, consequat neque eget, dignissim augue. | |||
" | |||
onAnchorClick={[MockFunction]} | |||
/> | |||
`; |
@@ -0,0 +1,41 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import slug from 'remark-slug'; | |||
import util from 'mdast-util-toc'; | |||
/** | |||
* This is a simplified version of the remark-toc plugin: https://github.com/remarkjs/remark-toc | |||
* It *only* renders the TOC, and leaves all the rest out. | |||
*/ | |||
export default function onlyToc() { | |||
this.use(slug); | |||
return transformer; | |||
function transformer(node) { | |||
const result = util(node, { heading: 'toc|table[ -]of[ -]contents?', maxDepth: 2 }); | |||
if (result.index === null || result.index === -1 || !result.map) { | |||
node.children = []; | |||
} else { | |||
node.children = [result.map]; | |||
} | |||
} | |||
} |
@@ -101,6 +101,22 @@ export function resizeWindowTo(width?: number, height?: number) { | |||
window.dispatchEvent(resizeEvent); | |||
} | |||
export function scrollTo({ left = 0, top = 0 }) { | |||
Object.defineProperty(window, 'pageYOffset', { value: top }); | |||
Object.defineProperty(window, 'pageXOffset', { value: left }); | |||
const resizeEvent = new Event('scroll'); | |||
window.dispatchEvent(resizeEvent); | |||
} | |||
export function setNodeRect({ width = 50, height = 50, left = 0, top = 0 }) { | |||
const { findDOMNode } = require('react-dom'); | |||
const element = document.createElement('div'); | |||
Object.defineProperty(element, 'getBoundingClientRect', { | |||
value: () => ({ width, height, left, top }) | |||
}); | |||
findDOMNode.mockReturnValue(element); | |||
} | |||
export function doAsync(fn?: Function): Promise<void> { | |||
return new Promise(resolve => { | |||
setImmediate(() => { |
@@ -2553,6 +2553,7 @@ api_documentation.search=Search by name... | |||
#------------------------------------------------------------------------------ | |||
documentation.page=Documentation | |||
documentation.page_title=SonarCloud Docs | |||
documentation.on_this_page=On this page | |||
#------------------------------------------------------------------------------ | |||
@@ -2687,8 +2688,7 @@ organization.members.add_to_members=Add to members | |||
organization.paid_plan.badge=Paid plan | |||
organization.default_visibility_of_new_projects=Default visibility of new projects: | |||
organization.change_visibility_form.header=Set Default Visibility of New Projects | |||
organization.change_visibility_form.warning=This will not change the visibility of already existing projects. | |||
organization.change_visibility_form.submit=Change Default Visibility | |||
organization.change_visibility_form.warning=This will not change the visibility of already existing projects.organization.change_visibility_form.submit=Change Default Visibility | |||
#------------------------------------------------------------------------------ |