How to create a Markdown table of contents - javascript

I want to create a table of contents from a markdown file in next.js and I want it to be in markdown format as well.
To achieve that I have made this function:
const getMarkdownTOC = (slug) => {
const folder = process.env.PATH_ARTICULOS
const file = `${folder}${slug}.md`
const content = fs.readFileSync(file, 'utf8')
const matterResult = matter(content)
let prevLevel = 99
let spaces = ''
let toc = ''
for (const [, value] of Object.entries(compiler(matterResult.content).props.children)) {
if (value.type !== undefined && value.type.startsWith('h') && !isNaN(value.type[1])) {
const level = Number(value.type[1])
const title = value.props.children[0]
const id = value.props.id
if (level > prevLevel) {
spaces += ' '
} else {
if (level < prevLevel) {
spaces = spaces.slice(0, spaces.length - 2 * (prevLevel - level))
}
}
toc += `${spaces}* [${title}](#${id})\n`
prevLevel = level
}
}
And the function does actually work, it returns a table of contents in markdown format that can be rendered wherever I want.
However, I think there has to be some easier way to achieve that, but I've been searching and haven't found anything. There is a plugin called remark-toc which is almost what I want, but it seems to create the table of contents at the beginning of the document and I want to keep it apart.
Any ideas? Thank you!

Related

Reduce bandwidth for JSON fetch for autocomplete suggestions Large data

I am new to Javascript and this is my first question. I want to make autocomplete suggestions purely in HTML, CSS and JS using below JS code. I have gotten this code from GitHub.
The problem is when I type, it downloads the whole JSON file before showing suggestions. I want to reduce this JSON file by splitting the JSON data to multiple JSON files like states1.json, states2.json, states3.json etc. I have tried but failed as I'm new. Here is the code.
Please answer simply to help me understand.
const matchList = document.getElementById("match-list");
//Search states.json and filter it
const searchStates = async searchText => {
const res = await fetch("./data/States1.json");
const states = await res.json();
//Get Matches to current text input
let matches = states.filter(state => {
const regex = new RegExp(`^${searchText}`, "gi");
return state.wr.match(regex);
});
if (searchText.length === 0) {
matches = [];
matchList.innerHTML = "";
}
outputHtml(matches);
};
//Show Results in HTML
const outputHtml = matches => {
if (matches.length > 0) {
const html = matches
.map(
match => `
<div class="card card-body mb-4">
<h4>${match.wr} <span class="text-primary">${
match.mn
}</span></h4>
</div>`
)
.join("");
matchList.innerHTML = html;
}
};
search.addEventListener("input", () => searchStates(search.value));

How to index similar object values and show it beside them? Javascript/React/ES6

I am currently receiving object segregatedData as props. This object contains several properties but the one we will focus on is segregatedData.type1 Basically this is just an array of strings like this ['water','fire','water',earth'] What I'd like to is to be able to print out each string and then beside it is how many instances this value has been repeated. So in the example case the expected output would be water-2, fire-1, earth-1 Of course we'd have to delete the duplicates.
Here's what I have so far:
import React from "react";
import { Typography } from "#material-ui/core";
function TypesText(props) {
const { segregatedData } = props;
return (
<>
<Typography>Type1:</Typography>
{segregatedData.length && segregatedData.map((data) => data.type1 + " ")}
</>
);
}
export default TypesText;
Fairly basic, it just prints every type1 property with a blank string as its seperator. Anything that can point me in the right direction is appreciated.
use segregatedData.type1.reduce to output array that finds occurrence in output array via mapping it and using string.search to find occurrence and then incrementing integer at the end.
Take a look at following code sample. test it
const arr = ['water','fire','water', 'earth'];
let val = arr.reduce((resultarr, item) => {
const val = resultarr.find(item1 => item1.search(item) !== -1)
const ind = resultarr.findIndex(item1 => item1.search(item) !== -1)
if (val == undefined) {
resultarr.push(item + "-" + 1)
}
else {
const i = val.search("-")
const num = parseInt(val.substring(i+1))
const nval = item + "-" + (num+1)
resultarr[ind] = nval;
}
return resultarr
}, [])
alert(val);

ReadableStream prefix of undefined

I'm trying to use the reader of a ReadableStream from a stream returned from a blob. Whenever I try to write the contents of the files it prefixes with undefined followed by the chars from the stream.
Example:
\\undefinedContents from the files. The undefined prefix is not part of the bytes returned from the stream. If I print them one by one I only see the expected content from the blob.
My code
function readStream(e) {
const file = e.target.files[0]
const stream = file.stream()
const reader = stream.getReader()
function readTheStream() {
reader.read().then(({ done, value }) => {
if(done) {
reader.cancel()
}
const ul = document.createElement('ul')
let column;
let tableRow = document.createElement('tr')
for (let i = 0; i < value.buffer.byteLength; i++) {
const char = String.fromCharCode(value[i])
if(i < 10) console.log(char)
if (char === " ") {
continue
}
if (char === "\n") {
ul.appendChild(tableRow)
column = ""
tableRow = document.createElement('tr')
continue
}
if (char === "\t") {
const td = document.createElement('td')
td.textContent = column
if (tableRow) {
tableRow.appendChild(td)
}
column = ""
continue
}
column += char
}
//Last row appended here cause no new line for creating it.
ul.appendChild(tableRow)
document.body.appendChild(ul)
})
}
readTheStream()
The file is a normal textfile of 4 columns separated by tabs, and every column gets extracted and placed as expected with in my list, except for the first entry, which gets prefix with undefinedContents.
What could be causing this prefix?
You can see in this image how it displays:
Well this was a no brainer really. It has to do with me trying to append to an uninitialized string. The column var was simply declared. When I initialized it as an empty string, undefined disappeared.

How to extract the content of the first paragraph in html string react native

I am working on a react native project and I have an html string json api response.
I am using react-native-render-html to render it, and I can get all paragraphs and apply specific things like number of lines ,etc.. . However I want to get only the first paragraph in the response.
str response='<p>text1</p> <p>text2</p> <p>text3</p>';
Is it possible to write a regular expression to get only the content of first paragraph which is for example text1 ?
I don't use React Native but in javascript you could do something like that:
const paragraphs = response.split("</p>")
const firstParagraph = paragraphs[0]+'</p>';
Or with a regex you can do something like that:
// extract all paragraphe from the string
const matches = [];
response.replace(/<p>(.*?)<\/p>/g, function () {
//use arguments[0] if you need to keep <p></p> html tags
matches.push(arguments[1]);
});
// get first paragraph
const firstParagraph = (matches.length) ? matches[0] : ""
Or like that (I think it is the best way in your case)
const response='<p>text1</p> <p>text2</p> <p>text3</p>';
const regex = /<p>(.*?)<\/p>/;
const corresp = regex.exec(response);
const firstParagraph = (corresp) ? corresp[0] : "" // <p>text1</p>
const firstParagraphWithoutHtml = (corresp) ? corresp[1] : "" // text1
Hope it will help
var response='<p>text1</p> <p>text2</p> <p>text3</p>';
var firstParagraphElement=response.split("</p>")[0] //firstparagraphElement="<p>text1"
var paragraphContent=firstParagraphElement.replace("<p>","") //paragraphContent="text1"
javascript split() function reference click
javascript replace() function reference click
In React Native you can also use parse5 to extract a string from HTML code. I have used this code in a project for doing so:
import parse5 from 'parse5'
const isText = (tagName): Boolean => tagName === '#text'
const processNode = (node): String => {
const nodeName = node.nodeName
if (isText(nodeName)) {
return node.value
}
if (!node.childNodes) {
return ''
}
return node.childNodes.map((child, index) => processNode(child)).join(' ')
}
export const htmlToText = (html): String => {
const root = parse5.parseFragment(html)
return processNode(root).replace(/\s+/g, ' ').trim()
}
Here is a simple JEST test for the function above:
test('when full document htmlToText should get text', () => {
const htmlToText1 = htmlToText("<html><head><title>titleTest</title></head><body><a href='test0'>test01</a><a href='test1'>test02</a><a href='test2'>test03</a></body></html>")
expect(htmlToText1)
.toBe(`titleTest test01 test02 test03`);
});

Add arrays into multi-dimensional array or object

I'm parsing content generated by a wysiwyg into a table of contents widget in React.
So far I'm looping through the headers and adding them into an array.
How can I get them all into one multi-dimensional array or object (what's the best way) so that it looks more like:
h1-1
h2-1
h3-1
h1-2
h2-2
h3-2
h1-3
h2-3
h3-3
and then I can render it with an ordered list in the UI.
const str = "<h1>h1-1</h1><h2>h2-1</h2><h3>h3-1</h3><p>something</p><h1>h1-2</h1><h2>h2-2</h2><h3>h3-2</h3>";
const patternh1 = /<h1>(.*?)<\/h1>/g;
const patternh2 = /<h2>(.*?)<\/h2>/g;
const patternh3 = /<h3>(.*?)<\/h3>/g;
let h1s = [];
let h2s = [];
let h3s = [];
let matchh1, matchh2, matchh3;
while (matchh1 = patternh1.exec(str))
h1s.push(matchh1[1])
while (matchh2 = patternh2.exec(str))
h2s.push(matchh2[1])
while (matchh3 = patternh3.exec(str))
h3s.push(matchh3[1])
console.log(h1s)
console.log(h2s)
console.log(h3s)
I don't know about you, but I hate parsing HTML using regexes. Instead, I think it's a better idea to let the DOM handle this:
const str = `<h1>h1-1</h1>
<h3>h3-1</h3>
<h3>h3-2</h3>
<p>something</p>
<h1>h1-2</h1>
<h2>h2-2</h2>
<h3>h3-2</h3>`;
const wrapper = document.createElement('div');
wrapper.innerHTML = str.trim();
let tree = [];
let leaf = null;
for (const node of wrapper.querySelectorAll("h1, h2, h3, h4, h5, h6")) {
const nodeLevel = parseInt(node.tagName[1]);
const newLeaf = {
level: nodeLevel,
text: node.textContent,
children: [],
parent: leaf
};
while (leaf && newLeaf.level <= leaf.level)
leaf = leaf.parent;
if (!leaf)
tree.push(newLeaf);
else
leaf.children.push(newLeaf);
leaf = newLeaf;
}
console.log(tree);
This answer does not require h3 to follow h2; h3 can follow h1 if you so please. If you want to turn this into an ordered list, that can also be done:
const str = `<h1>h1-1</h1>
<h3>h3-1</h3>
<h3>h3-2</h3>
<p>something</p>
<h1>h1-2</h1>
<h2>h2-2</h2>
<h3>h3-2</h3>`;
const wrapper = document.createElement('div');
wrapper.innerHTML = str.trim();
let tree = [];
let leaf = null;
for (const node of wrapper.querySelectorAll("h1, h2, h3, h4, h5, h6")) {
const nodeLevel = parseInt(node.tagName[1]);
const newLeaf = {
level: nodeLevel,
text: node.textContent,
children: [],
parent: leaf
};
while (leaf && newLeaf.level <= leaf.level)
leaf = leaf.parent;
if (!leaf)
tree.push(newLeaf);
else
leaf.children.push(newLeaf);
leaf = newLeaf;
}
const ol = document.createElement("ol");
(function makeOl(ol, leaves) {
for (const leaf of leaves) {
const li = document.createElement("li");
li.appendChild(new Text(leaf.text));
if (leaf.children.length > 0) {
const subOl = document.createElement("ol");
makeOl(subOl, leaf.children);
li.appendChild(subOl);
}
ol.appendChild(li);
}
})(ol, tree);
// add it to the DOM
document.body.appendChild(ol);
// or get it as text
const result = ol.outerHTML;
Since the HTML is parsed by the DOM and not by a regex, this solution will not encounter any errors if the h1 tags have attributes, for example.
You can simply gather all h* and then iterate over them to construct a tree as such:
Using ES6 (I inferred this is ok from your usage of const and let)
const str = `
<h1>h1-1</h1>
<h2>h2-1</h2>
<h3>h3-1</h3>
<p>something</p>
<h1>h1-2</h1>
<h2>h2-2</h2>
<h3>h3-2</h3>
`
const patternh = /<h(\d)>(.*?)<\/h(\d)>/g;
let hs = [];
let matchh;
while (matchh = patternh.exec(str))
hs.push({ lev: matchh[1], text: matchh[2] })
console.log(hs)
// constructs a tree with the format [{ value: ..., children: [{ value: ..., children: [...] }, ...] }, ...]
const add = (res, lev, what) => {
if (lev === 0) {
res.push({ value: what, children: [] });
} else {
add(res[res.length - 1].children, lev - 1, what);
}
}
// reduces all hs found into a tree using above method starting with an empty list
const tree = hs.reduce((res, { lev, text }) => {
add(res, lev-1, text);
return res;
}, []);
console.log(tree);
But because your html headers are not in a tree structure themselves (which I guess is your use case) this only works under certain assumptions, e.g. you cannot have a <h3> unless there's a <h2> above it and a <h1> above that. It will also assume a lower-level header will always belong to the latest header of an immediately higher level.
If you want to further use the tree structure for e.g. rendering a representative ordered-list for a TOC, you can do something like:
// function to render a bunch of <li>s
const renderLIs = children => children.map(child => `<li>${renderOL(child)}</li>`).join('');
// function to render an <ol> from a tree node
const renderOL = tree => tree.children.length > 0 ? `<ol>${tree.value}${renderLIs(tree.children)}</ol>` : tree.value;
// use a root node for the TOC
const toc = renderOL({ value: 'TOC', children: tree });
console.log(toc);
Hope it helps.
What you want to do is known as (a variant of a) document outline, eg. creating a nested list from the headings of a document, honoring their hierarchy.
A simple implementation for the browser using the DOM and DOMParser APIs goes as follows (put into a HTML page and coded in ES5 for easy testing):
<!DOCTYPE html>
<html>
<head>
<title>Document outline</title>
</head>
<body>
<div id="outline"></div>
<script>
// test string wrapped in a document (and body) element
var str = "<html><body><h1>h1-1</h1><h2>h2-1</h2><h3>h3-1</h3><p>something</p><h1>h1-2</h1><h2>h2-2</h2><h3>h3-2</h3></body></html>";
// util for traversing a DOM and emit SAX startElement events
function emitSAXLikeEvents(node, handler) {
handler.startElement(node)
for (var i = 0; i < node.children.length; i++)
emitSAXLikeEvents(node.children.item(i), handler)
handler.endElement(node)
}
var outline = document.getElementById('outline')
var rank = 0
var context = outline
emitSAXLikeEvents(
(new DOMParser()).parseFromString(str, "text/html").body,
{
startElement: function(node) {
if (/h[1-6]/.test(node.localName)) {
var newRank = +node.localName.substr(1, 1)
// set context li node to append
while (newRank <= rank--)
context = context.parentNode.parentNode
rank = newRank
// create (if 1st li) or
// get (if 2nd or subsequent li) ol element
var ol
if (context.children.length > 0)
ol = context.children[0]
else {
ol = document.createElement('ol')
context.appendChild(ol)
}
// create and append li with text from
// heading element
var li = document.createElement('li')
li.appendChild(
document.createTextNode(node.innerText))
ol.appendChild(li)
context = li
}
},
endElement: function(node) {}
})
</script>
</body>
</html>
I'm first parsing your fragment into a Document, then traverse it to create SAX-like startElement() calls. In the startElement() function, the rank of a heading element is checked against the rank of the most recently created list item (if any). Then a new list item is appended at the correct hierarchy level, and possibly an ol element is created as container for it. Note the algorithm as it is won't work with "jumping" from h1 to h3 in the hierarchy, but can be easily adapted.
If you want to create an outline/table of content on node.js, the code could be made to run server-side, but requires a decent HTML parsing lib (a DOMParser polyfill for node.js, so to speak). There are also the https://github.com/h5o/h5o-js and the https://github.com/hoyois/html5outliner packages for creating outlines, though I haven't tested those. These packages supposedly can also deal with corner cases such as heading elements in iframe and quote elements which you generally don't want in the the outline of your document.
The topic of creating an HTML5 outline has a long history; see eg. http://html5doctor.com/computer-says-no-to-html5-document-outline/. HTML4's practice of using no sectioning roots (in HTML5 parlance) wrapper elements for sectioning and placing headings and content at the same hierarchy level is known as "flat-earth markup". SGML has the RANK feature for dealing with H1, H2, etc. ranked elements, and can be made to infer omitted section elements, thus automatically create an outline, from HTML4-like "flat earth markup" in simple cases (eg. where only section or another single element is allowed as sectioning root).
I'll use a single regex to get the <hx></hx> contents and then sort them by x using methods Array.reduce.
Here is the base but it's not over yet :
// The string you need to parse
const str = "\
<h1>h1-1</h1>\
<h2>h2-1</h2>\
<h3>h3-1</h3>\
<p>something</p>\
<h1>h1-2</h1>\
<h2>h2-2</h2>\
<h3>h3-2</h3>";
// The regex that will cut down the <hx>something</hx>
const regex = /<h[0-9]{1}>(.*?)<\/h[0-9]{1}>/g;
// We get the matches now
const matches = str.match(regex);
// We match the hx togethers as requested
const matchesSorted = Object.values(matches.reduce((tmp, x) => {
// We get the number behind hx ---> the x
const hNumber = x[2];
// If the container do not exist, create it
if (!tmp[hNumber]) {
tmp[hNumber] = [];
}
// Push the new parsed content into the array
// 4 is to start after <hx>
// length - 9 is to get all except <hx></hx>
tmp[hNumber].push(x.substr(4, x.length - 9));
return tmp;
}, {}));
console.log(matchesSorted);
As you are parsing html content I want to aware you about special cases like presency of \n or space. For example look at the following non-working snippet :
// The string you need to parse
const str = "\
<h1>h1-1\n\
</h1>\
<h2> h2-1</h2>\
<h3>h3-1</h3>\
<p>something</p>\
<h1>h1-2 </h1>\
<h2>h2-2 \n\
</h2>\
<h3>h3-2</h3>";
// The regex that will cut down the <hx>something</hx>
const regex = /<h[0-9]{1}>(.*?)<\/h[0-9]{1}>/g;
// We get the matches now
const matches = str.match(regex);
// We match the hx togethers as requested
const matchesSorted = Object.values(matches.reduce((tmp, x) => {
// We get the number behind hx ---> the x
const hNumber = x[2];
// If the container do not exist, create it
if (!tmp[hNumber]) {
tmp[hNumber] = [];
}
// Push the new parsed content into the array
// 4 is to start after <hx>
// length - 9 is to get all except <hx></hx>
tmp[hNumber].push(x.substr(4, x.length - 9));
return tmp;
}, {}));
console.log(matchesSorted);
We gotta add .replace() and .trim() in order to remove unwanted \n and spaces.
Use this snippet
// The string you need to parse
const str = "\
<h1>h1-1\n\
</h1>\
<h2> h2-1</h2>\
<h3>h3-1</h3>\
<p>something</p>\
<h1>h1-2 </h1>\
<h2>h2-2 \n\
</h2>\
<h3>h3-2</h3>";
// Remove all unwanted \n
const preparedStr = str.replace(/(\r\n\t|\n|\r\t)/gm, "");
// The regex that will cut down the <hx>something</hx>
const regex = /<h[0-9]{1}>(.*?)<\/h[0-9]{1}>/g;
// We get the matches now
const matches = preparedStr.match(regex);
// We match the hx togethers as requested
const matchesSorted = Object.values(matches.reduce((tmp, x) => {
// We get the number behind hx ---> the x
const hNumber = x[2];
// If the container do not exist, create it
if (!tmp[hNumber]) {
tmp[hNumber] = [];
}
// Push the new parsed content into the array
// 4 is to start after <hx>
// length - 9 is to get all except <hx></hx>
// call trim() to remove unwanted spaces
tmp[hNumber].push(x.substr(4, x.length - 9).trim());
return tmp;
}, {}));
console.log(matchesSorted);
I write this code works with JQuery. (Please don't DV. Maybe someone needs a jquery answer later)
This recursive function creates lis of string and if one item has some childern, it will convert them to an ol.
const str =
"<div><h1>h1-1</h1><h2>h2-1</h2><h3>h3-1</h3></div><p>something</p><h1>h1-2</h1><h2>h2-2</h2><h3>h3-2</h3>";
function strToList(stri) {
const tags = $(stri);
function partToList(el) {
let output = "<li>";
if ($(el).children().length) {
output += "<ol>";
$(el)
.children()
.each(function() {
output += partToList($(this));
});
output += "</ol>";
} else {
output += $(el).text();
}
return output + "</li>";
}
let output = "<ol>";
tags.each(function(itm) {
output += partToList($(this));
});
return output + "</ol>";
}
$("#output").append(strToList(str));
li {
padding: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>
(This code can be converted to pure JS easily)

Categories

Resources