frontmatter {
scope
}
+ headings {
+ depth
+ value
+ }
fields {
slug
}
title: Branches
---
+## Table of Contents
+
<!-- sonarqube -->
_Branch analysis is available as part of [Developer Edition](https://redirect.sonarsource.com/editions/developer.html)_
export default ({ data }) => {
const page = data.markdownRemark;
- const htmlWithInclusions = cutSonarCloudContent(page.html).replace(
+ let htmlWithInclusions = cutSonarCloudContent(page.html).replace(
/\<p\>@include (.*)\<\/p\>/,
(_, path) => {
const chunk = data.allMarkdownRemark.edges.find(edge => edge.node.fields.slug === path);
}
);
+ if (
+ page.headings &&
+ page.headings.length > 0 &&
+ page.html.match(/<h[1-9]>Table Of Contents<\/h[1-9]>/i)
+ ) {
+ htmlWithInclusions = generateTableOfContents(htmlWithInclusions, page.headings);
+ }
+
return (
<div css={{ paddingTop: 24, paddingBottom: 24 }}>
<Helmet title={page.frontmatter.title} />
}
markdownRemark(fields: { slug: { eq: $slug } }) {
html
+ headings {
+ depth
+ value
+ }
frontmatter {
title
}
}
`;
+function generateTableOfContents(content, headings) {
+ let html = '<h2>Table Of Contents</h2>';
+ let depth = headings[0].depth - 1;
+ for (let i = 1; i < headings.length; i++) {
+ while (headings[i].depth > depth) {
+ html += '<ul>';
+ depth++;
+ }
+ while (headings[i].depth < depth) {
+ html += '</ul>';
+ depth--;
+ }
+ html += `<li><a href="#header-${i}">${headings[i].value}</a></li>`;
+ content = content.replace(
+ new RegExp(`<h${headings[i].depth}>${headings[i].value}</h${headings[i].depth}>`, 'gi'),
+ `<h${headings[i].depth} id="header-${i}">${headings[i].value}</h${headings[i].depth}>`
+ );
+ }
+ return content.replace(/<h[1-9]>Table Of Contents<\/h[1-9]>/, html);
+}
+
function cutSonarCloudContent(content) {
const beginning = '<!-- sonarcloud -->';
const ending = '<!-- /sonarcloud -->';
"redux": "3.7.2",
"redux-logger": "3.0.6",
"redux-thunk": "2.2.0",
+ "remark-toc": "5.0.0",
"whatwg-fetch": "2.0.4"
},
"devDependencies": {
--- /dev/null
+/*
+ * 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-toc' {
+ interface Options {
+ /** Heading to look for */
+ heading?: string;
+ /** Maximum heading depth, inclusive */
+ maxDepth?: number;
+ /** Display list-item tightly */
+ tight?: boolean;
+ }
+
+ export default function remarkToc(options?: Options): JSX.Element;
+}
const entryRoot = getEntryRoot(entry.relativeName);
entry.children = entryRoot !== '' ? this.getMenuEntriesHierarchy(entryRoot) : [];
});
- return toplevelEntries;
+ return toplevelEntries.sort((a, b) => parseInt(a.order, 10) - parseInt(b.order, 10));
};
renderEntry = (entry: DocumentationEntry, depth: number): React.ReactNode => {
import { Link } from 'react-router';
import DetachIcon from '../icons-components/DetachIcon';
+interface OwnProps {
+ customProps?: {
+ [k: string]: any;
+ };
+}
+
+type Props = OwnProps & React.AnchorHTMLAttributes<HTMLAnchorElement>;
+
const SONARCLOUD_LINK = '/#sonarcloud#/';
-export default function DocLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
- const { children, href, ...other } = props;
- if (href && href.startsWith('/')) {
- let url = `/documentation/${href.substr(1)}`;
- if (href.startsWith(SONARCLOUD_LINK)) {
- url = `/${href.substr(SONARCLOUD_LINK.length)}`;
+export default class DocLink extends React.PureComponent<Props> {
+ handleClickOnAnchor = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ const { customProps, href = '#' } = this.props;
+ if (customProps && customProps.onAnchorClick) {
+ customProps.onAnchorClick(href, event);
+ }
+ };
+
+ render() {
+ const { children, href, customProps, ...other } = this.props;
+ if (href && href.startsWith('#')) {
+ return (
+ <a href="#" onClick={this.handleClickOnAnchor}>
+ {children}
+ </a>
+ );
}
+
+ if (href && href.startsWith('/')) {
+ let url = `/documentation/${href.substr(1)}`;
+ if (href.startsWith(SONARCLOUD_LINK)) {
+ url = `/${href.substr(SONARCLOUD_LINK.length)}`;
+ }
+ return (
+ <Link to={url} {...other}>
+ {children}
+ </Link>
+ );
+ }
+
return (
- <Link to={url} {...other}>
- {children}
- </Link>
+ <>
+ <a href={href} rel="noopener noreferrer" target="_blank" {...other}>
+ {children}
+ </a>
+ <DetachIcon
+ className="text-muted little-spacer-left little-spacer-right vertical-baseline"
+ size={12}
+ />
+ </>
);
}
-
- return (
- <>
- <a href={href} rel="noopener noreferrer" target="_blank" {...other}>
- {children}
- </a>
- <DetachIcon
- className="text-muted little-spacer-left little-spacer-right vertical-baseline"
- size={12}
- />
- </>
- );
}
import * as classNames from 'classnames';
import remark from 'remark';
import reactRenderer from 'remark-react';
+import remarkToc from 'remark-toc';
import DocLink from './DocLink';
import DocParagraph from './DocParagraph';
import DocImg from './DocImg';
import DocTooltipLink from './DocTooltipLink';
import { separateFrontMatter } from '../../helpers/markdown';
import { isSonarCloud } from '../../helpers/system';
+import { scrollToElement } from '../../helpers/scrolling';
interface Props {
childProps?: { [k: string]: string };
isTooltip?: boolean;
}
-export default function DocMarkdownBlock({
- childProps,
- className,
- content,
- displayH1,
- isTooltip
-}: Props) {
- const parsed = separateFrontMatter(content || '');
- return (
- <div className={classNames('markdown', className)}>
- {displayH1 && <h1>{parsed.frontmatter.title}</h1>}
- {
- remark()
- // .use(remarkInclude)
- .use(reactRenderer, {
- remarkReactComponents: {
- // do not render outer <div />
- div: React.Fragment,
- // use custom link to render documentation anchors
- a: isTooltip ? withChildProps(DocTooltipLink, childProps) : DocLink,
- // used to handle `@include`
- p: DocParagraph,
- // use custom img tag to render documentation images
- img: DocImg
- },
- toHast: {}
- })
- .processSync(filterContent(parsed.content)).contents
+export default class DocMarkdownBlock extends React.PureComponent<Props> {
+ node: HTMLElement | null = null;
+
+ handleAnchorClick = (href: string, event: React.MouseEvent<HTMLAnchorElement>) => {
+ if (this.node) {
+ const element = this.node.querySelector(`#user-content-${href.substr(1)}`);
+ if (element) {
+ event.preventDefault();
+ scrollToElement(element, { bottomOffset: window.innerHeight - 80 });
}
- </div>
- );
+ }
+ };
+
+ render() {
+ const { childProps, content, className, displayH1, isTooltip } = this.props;
+ const parsed = separateFrontMatter(content || '');
+ return (
+ <div className={classNames('markdown', className)} ref={ref => (this.node = ref)}>
+ {displayH1 && <h1>{parsed.frontmatter.title}</h1>}
+ {
+ remark()
+ // .use(remarkInclude)
+ .use(remarkToc, { maxDepth: 3 })
+ .use(reactRenderer, {
+ remarkReactComponents: {
+ // do not render outer <div />
+ div: React.Fragment,
+ // use custom link to render documentation anchors
+ a: isTooltip
+ ? withChildProps(DocTooltipLink, childProps)
+ : withChildProps(DocLink, { onAnchorClick: this.handleAnchorClick }),
+ // used to handle `@include`
+ p: DocParagraph,
+ // use custom img tag to render documentation images
+ img: DocImg
+ },
+ toHast: {}
+ })
+ .processSync(filterContent(parsed.content)).contents
+ }
+ </div>
+ );
+ }
}
function withChildProps<P>(
- WrappedComponent: React.ComponentType<P & { customProps?: { [k: string]: string } }>,
- childProps?: { [k: string]: string }
+ WrappedComponent: React.ComponentType<P & { customProps?: { [k: string]: any } }>,
+ childProps?: { [k: string]: any }
) {
return function withChildProps(props: P) {
return <WrappedComponent customProps={childProps} {...props} />;
expect(shallow(<DocMarkdownBlock content="this is *bold* text" />)).toMatchSnapshot();
});
-it('should render use custom component for links', () => {
+it('should use custom component for links', () => {
expect(
- shallow(<DocMarkdownBlock content="some [link](#quality-profiles)" />).find('DocLink')
+ shallow(<DocMarkdownBlock content="some [link](/quality-profiles)" />).find('withChildProps')
).toMatchSnapshot();
});
</div>
`;
-exports[`should render use custom component for links 1`] = `
-<DocLink
- href="#quality-profiles"
+exports[`should use custom component for links 1`] = `
+<withChildProps
+ href="/quality-profiles"
key="h-3"
>
link
-</DocLink>
+</withChildProps>
`;
exports[`should render with custom props for links 1`] = `
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
+"emoji-regex@>=6.0.0 <=6.1.1":
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e"
+
emoji-regex@^6.1.0:
version "6.5.1"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
dependencies:
assert-plus "^1.0.0"
+github-slugger@^1.0.0, github-slugger@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.2.0.tgz#8ada3286fd046d8951c3c952a8d7854cfd90fd9a"
+ dependencies:
+ emoji-regex ">=6.0.0 <=6.1.1"
+
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
unist-util-visit "^1.1.0"
xtend "^4.0.1"
+mdast-util-to-string@^1.0.0, mdast-util-to-string@^1.0.2:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.0.4.tgz#5c455c878c9355f0c1e7f3e8b719cf583691acfb"
+
+mdast-util-toc@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/mdast-util-toc/-/mdast-util-toc-2.0.1.tgz#b1d2cb23bfb01f812fa7b55bffe8b0a8bedf6f21"
+ dependencies:
+ github-slugger "^1.1.1"
+ mdast-util-to-string "^1.0.2"
+ unist-util-visit "^1.1.0"
+
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
hast-util-sanitize "^1.0.0"
mdast-util-to-hast "^3.0.0"
+remark-slug@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-5.0.0.tgz#9de71fcdc2bfae33ebb4a41eb83035288a829980"
+ dependencies:
+ github-slugger "^1.0.0"
+ mdast-util-to-string "^1.0.0"
+ unist-util-visit "^1.0.0"
+
remark-stringify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-5.0.0.tgz#336d3a4d4a6a3390d933eeba62e8de4bd280afba"
unherit "^1.0.4"
xtend "^4.0.1"
+remark-toc@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/remark-toc/-/remark-toc-5.0.0.tgz#f1e13ed11062ad4d102b02e70168bd85015bf129"
+ dependencies:
+ mdast-util-toc "^2.0.0"
+ remark-slug "^5.0.0"
+
remark@9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/remark/-/remark-9.0.0.tgz#c5cfa8ec535c73a67c4b0f12bfdbd3a67d8b2f60"