diff --git a/asset/css/markbind.css b/asset/css/markbind.css index 56850ec7cf..bd323dbb86 100644 --- a/asset/css/markbind.css +++ b/asset/css/markbind.css @@ -23,6 +23,19 @@ pre > code.hljs { outline: none !important; } +.fa.fa-anchor { + color: #ccc; + display: none; + font-size: 14px; + margin-left: 10px; + padding: 3px; + text-decoration: none; +} + +.fa.fa-anchor:hover { + color: #555; +} + .markbind-table { width: auto; } diff --git a/asset/js/setup.js b/asset/js/setup.js index 094a68f313..168c00424e 100644 --- a/asset/js/setup.js +++ b/asset/js/setup.js @@ -15,9 +15,21 @@ function flattenModals() { }); } +function setupAnchorVisibility() { + jQuery('h1, h2, h3, h4, h5, h6').each((index, heading) => { + jQuery(heading).on('mouseenter', function () { + jQuery(this).children('.fa.fa-anchor').show(); + }); + jQuery(heading).on('mouseleave', function () { + jQuery(this).children('.fa.fa-anchor').hide(); + }); + }); +} + function executeAfterMountedRoutines() { flattenModals(); scrollToUrlAnchorHeading(); + setupAnchorVisibility(); } function setupSiteNav() { diff --git a/docs/userGuide/contentAuthoring.md b/docs/userGuide/contentAuthoring.md index 7286c9a715..7ff2df21ae 100644 --- a/docs/userGuide/contentAuthoring.md +++ b/docs/userGuide/contentAuthoring.md @@ -589,4 +589,12 @@ Note: - Footers should not be nested in other components or HTML tags. If found inside, they will be shifted outside to be rendered properly. - [MarkBind components](#use-components) and [includes](#include-contents) are not supported in footers. - +### Heading Anchors + +Your site's readers may want to obtain a link to a heading within a page, to share with someone else for example. + +MarkBind automatically generates heading anchors for your page. + +When the reader hovers over a heading in your page, a small anchor icon will become visible next to the heading. Clicking this icon will redirect the page to that heading, producing the desired URL in the URL bar that the reader can share with someone else. Try it with the headings on this page! + + \ No newline at end of file diff --git a/lib/Page.js b/lib/Page.js index 3d685dfd21..f819bba803 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -25,6 +25,7 @@ const PAGE_CONTENT_ID = 'page-content'; const SITE_NAV_ID = 'site-nav'; const TITLE_PREFIX_SEPARATOR = ' - '; +const ANCHOR_HTML = ''; const DROPDOWN_BUTTON_ICON_HTML = '\n' + '\n' + ''; @@ -219,16 +220,25 @@ Page.prototype.collectHeadingsAndKeywords = function () { this.collectHeadingsAndKeywordsInContent($(`#${CONTENT_WRAPPER_ID}`).html(), null); }; +/** + * Generates a heading selector based on the indexing level + * @param headingIndexingLevel to generate + */ +function generateHeadingSelector(headingIndexingLevel) { + let headingsSelector = 'h1'; + for (let i = 2; i <= headingIndexingLevel; i += 1) { + headingsSelector += `, h${i}`; + } + return headingsSelector; +} + /** * Records headings and keywords inside content into this.headings and this.keywords respectively * @param content that contains the headings and keywords */ Page.prototype.collectHeadingsAndKeywordsInContent = function (content, lastHeading) { let $ = cheerio.load(content); - let headingsSelector = 'h1'; - for (let i = 2; i <= this.headingIndexingLevel; i += 1) { - headingsSelector += `, h${i}`; - } + const headingsSelector = generateHeadingSelector(this.headingIndexingLevel); $('modal').remove(); $('panel').not('panel panel') .each((index, panel) => { @@ -309,6 +319,21 @@ Page.prototype.concatenateHeadingsAndKeywords = function () { }); }; +/** + * Adds anchor links to headings in the page + * @param content of the page + */ +Page.prototype.addAnchors = function (content) { + const $ = cheerio.load(content, { xmlMode: false }); + if (this.headingIndexingLevel > 0) { + const headingsSelector = generateHeadingSelector(this.headingIndexingLevel); + $(headingsSelector).each((i, heading) => { + $(heading).append(ANCHOR_HTML.replace('#', `#${$(heading).attr('id')}`)); + }); + } + return $.html(); +}; + /** * Records the dynamic or static included files into this.includedFiles * @param dependencies array of maps of the external dependency and where it is included @@ -471,6 +496,7 @@ Page.prototype.generate = function (builtFiles) { .then(result => markbinder.resolveBaseUrl(result, fileConfig)) .then(result => fs.outputFileAsync(this.tempPath, result)) .then(() => markbinder.renderFile(this.tempPath, fileConfig)) + .then(result => this.addAnchors(result)) .then((result) => { this.content = htmlBeautify(result, { indent_size: 2 }); diff --git a/test/test_site/expected/bugs/index.html b/test/test_site/expected/bugs/index.html index 8eba5ec171..3afa0d69a0 100644 --- a/test/test_site/expected/bugs/index.html +++ b/test/test_site/expected/bugs/index.html @@ -26,7 +26,7 @@
Repro:
@@ -40,7 +40,7 @@
Repro:
Repro:
This is a link. [ diff --git a/test/test_site/expected/index.html b/test/test_site/expected/index.html index 4d8acec1c7..4b9b5fc461 100644 --- a/test/test_site/expected/index.html +++ b/test/test_site/expected/index.html @@ -26,12 +26,12 @@
This variable can be referenced.
References can be several levels deep.
-keyword 1 keyword 2
-included keyword
There are many techniques used during a requirements gathering. The following are some of the techniques.
-Brainstorming is a group activity designed to generate a large number of diverse and creative ideas for the solution of a problem. In a brainstorming session there are no "bad" ideas. The aim is to generate ideas; not to validate them. Brainstorming encourages you to "think outside the box" and put "crazy" ideas on the table without fear of rejection.
-Carefully designed questionnaires can be used to solicit responses and opinions from a large number of users regarding any current system or a new innovation.
-Focus groups are a kind of informal interview within an interactive group setting. A
Requirements gathering, requirements elicitation, requirements analysis, requirements capture are some of the terms commonly and interchangeably used to represent the activity of understanding what a software product should do.
This is a HTML document
It is possible to use Markdown in HTML
MarkBind supports .mbd files.
MarkBind supports .mbdf files.
This is a page from another Markbind site.
-It is a list of features (or functionalities) grouped according to some criteria such as priority (e.g. must-have, nice-to-have, etc. ), order of delivery, object or process related (e.g. order-related, invoice-related, etc.). Here is a sample feature list from Minesweeper (only a brief description has been provided to save space).
