Get text of previous header in HTML - javascript

I have a HTML which looks like this:
<h1>Title</h1>
<p>Some additional content, can be multiple, various tags</p>
<h2><a id="123"></a>Foo</h2>
<p>Some additional content, can be multiple, various tags</p>
<h3><a id="456"></a>Bar</h3>
Now, for each anchor with id, I want to find out the header hierarchy, e.g. for the anchor with id="123" I would like to get something like [{level: 1, title: "Title"}, {level: 2, title: "Foo"}], similarly for anchor with id="456", I would like to get [{level: 1, title: "Title"}, {level: 2, title: "Foo"}, {level: 3, title: "Bar"}].
My code looks like this so far:
const linkModel: IDictionary<ILinkModelEntry> = {};
const $ = cheerio.load(html);
$("a").each((_i, elt) => {
const anchor = $(elt);
const id = anchor.attr().id;
if (id) {
const parent = anchor.parent();
const parentTag = parent.prop("tagName");
let headerHierarchy: any[] = [];
if (["H1", "H2", "H3", "H4", "H5", "H6"].includes(parentTag)) {
let level = parseInt(parentTag[1]);
headerHierarchy = [{level, text: parent.text()}];
level--;
while (level > 0) {
const prevHeader = parent.prev("h" + level);
const text = prevHeader.text();
headerHierarchy.unshift({level, text});
level--;
}
}
linkModel["#" + id] = {originalId: id, count: count++, headerHierarchy};
}
});
What am I doing wrong, since
const prevHeader = parent.prev("h" + level);
const text = prevHeader.text();
always returns an empty string (i.e. "")?

If I understand correctly, you're looking to capture hierarchy. If your example had another <h1> followed by more <h2> and <h3>s below it, you'd want to pop the stack of parents back down to that new <h1> level for linking future <h2> and <h3> children rather than have an array of all elements back up to that first <h1>Title</h1>.
Here's one approach:
const cheerio = require("cheerio"); // ^1.0.0-rc.12
const html = `
<h1>Title</h1>
<p>Some additional content, can be multiple, various tags</p>
<h2><a id="123"></a>Foo</h2>
<p>Some additional content, can be multiple, various tags</p>
<h3><a id="456"></a>Bar</h3>
<h1>Another Title</h1>
<h2><a id="xxx"></a>Foo 2</h2>
<h3><a id="yyy"></a>Bar 2</h3>`;
const $ = cheerio.load(html);
const result = {};
const stack = [];
[...$("h1,h2,h3,h4,h5,h6")].forEach(e => {
const level = +$(e).prop("tagName")[1];
while (stack.length && level <= stack.at(-1).level) {
stack.pop();
}
if (!stack.length || level >= stack.at(-1).level) {
stack.push({level, title: $(e).text()});
}
if ($(e).has("a[id]").length) {
const id = $(e).find("a[id]").attr("id");
result[`#${id}`] = [...stack];
}
});
console.log(result);
Output:
{
'#123': [ { level: 1, title: 'Title' }, { level: 2, title: 'Foo' } ],
'#456': [
{ level: 1, title: 'Title' },
{ level: 2, title: 'Foo' },
{ level: 3, title: 'Bar' }
],
'#xxx': [
{ level: 1, title: 'Another Title' },
{ level: 2, title: 'Foo 2' }
],
'#yyy': [
{ level: 1, title: 'Another Title' },
{ level: 2, title: 'Foo 2' },
{ level: 3, title: 'Bar 2' }
]
}
If you actually want the whole chain of ancestors linearly back to the first, then remove the while loop (unlikely your intent).

Related

Combine two one dimensional arrays to create a cartesian table

I hope you are good.
I am struggling to create a compatible data type in javascript to display a cartesian like table where we have a vertical and a horizontal header.
Basically I have 3 one dimensional arrays where the first two are the table headers, and the third has the combination of those two by id's (basically the table cells).
let horizontal_header = [
{ id: 1, name: 'h1' },
{ id: 2, name: 'h2' },
];
let vertical_header = [
{ id: 10, name: 'r1' },
{ id: 11, name: 'r2' },
];
let cells = [
{ hid: 1, vid: 10, id: 7, name: 'c1' },
{ hid: 1, vid: 11, id: 8, name: 'c2' },
{ hid: 2, vid: 10, id: 9, name: 'c3' },
{ hid: 2, vid: 11, id: 10, name: 'c4' },
],
Also it can happen that a combination might not exists in that case, I want to enter an empty cell or something obvious that this cell is missing.
I want to create a table like below:
h1
h2
r1
c1
c3
r2
c2
c4
I would appreciate any suggestion and be very thankful to help me solve this complex use-case using Angular for rendering the table template.
Thank you.
I'd approach this problem by parsing the cells into more table-render friendly format like below. Note: I used ### separator, you can use anything that suits for coding practice.
let output = {};
cells.forEach(cell => {
output[cell.hid + '###' + cell.vid] = {
id: cell.id,
name: cell.name,
};
});
After that, you can use the output object to render the table cell as you already know the combination of hid and vid. You can prepare/render your table rows as below.
const rows = [];
for (let i = 0; i < horizontal_header.length; i++) {
const row = [];
for (let j = 0; j < vertical_header.length; j++) {
const hid = horizontal_header[i];
const vid = vertical_header[j];
if (output[hid + '###' + vid]) {
row.push(output[hid + '###' + vid].name);
} else {
row.push('-');
}
}
rows.push(row);
}

How to limit an array field in javascript object to a certain length

I know this may a simple problem but I have a the following javascript object:
const categories = {
title: 'cat1',
contents: [
{
name: 'cont1'
},
{
name: 'cont2'
},
{
name: 'cont3'
}
]
}
How can a transform this categories object so it has only 2 contents element as an example?
const transformedCategories = {
title: 'cat1',
contents: [
{
name: 'cont1'
},
{
name: 'cont2'
}
]
}
A couple of ways to do it:
const transformedCategories = {
title: categories.title,
contents: categories.contents.slice(0,2) // you can also use splice here
}
Another, slightly cheeky way:
const contents = [...categories.contents];
contents.length = 2;
const transformedCategories = {...categories, contents}

Finding nested elements Typescript Array object

I have a bunch of content in an array with type:
export interface StaticContent {
level : number,
id : string,
title: string,
content : string[] | StaticContent[],
}
I'm trying to search for an object where id to query is a sub-element to a content object. I think I've probably set the interface up wrong but I'm having a mental block.
Any ideas?
Example:
data = [
{
level: 0,
id: 'first-element',
title: 'Title of the first element'
content: [
`I am some random string of content`
]
},
{
level: 1,
id: 'second-element',
title: 'Title of the second element'
content: [
{
level: 0,
id: 'first-sub-element',
title: 'Title of first sub element'
content: [
` I am some content attached to the first sub element `
]
}
}]
let idToFind = 'first-sub-element'
let element = data.find(t => t.id === idToFind)
let title = element.title
In this example the result is an undefined element.
I expect
element = {
level: 0,
id: 'first-sub-element',
title: 'Title of first sub element'
content: [
` I am some content attached to the first sub element `
]
}
}
You need to use recursion or do a recursive search in place.
Here is recursion example:
function searchInContent(idToFind: string, content: StaticContent[] | string[]): StaticContent | null {
for (const c of content) {
if (typeof c === 'string') continue
// now we can assume that c is StaticContent
if (c.id === idToFind) {
// FOUND RESULT
return c
}
// Perform deep search in content
const result = searchInContent(idToFind, c.content)
if (result !== null) {
// stop searching and pass found result
return result
}
}
return null
}
let idToFind = 'first-sub-element'
let element = searchInContent(data)
let title = element.title
You are trying to find the content object for a specific id. What you could do is flatMap() the contents and then use find() to find the correct matching object.
const data = [{
level: 0,
id: 'first-element',
title: 'Title of the first element',
content: [
`I am some random string of content`
]
},
{
level: 1,
id: 'second-element',
title: 'Title of the second element',
content: [{
level: 0,
id: 'first-sub-element',
title: 'Title of first sub element',
content: [
` I am some content attached to the first sub element `
]
}, {
level: 0,
id: 'first-sub-elemeeent',
title: 'Title of first sub element',
content: [
` I am some content attached to the first sub element `
]
}]
}
];
const idToFind = 'first-sub-element';
const result = data.flatMap(d => d.content).find(c => c.id === idToFind);
console.log(result);

Create a key map for all paths in a recursive/nested object array

I have an n levels deep nested array of tag objects with title and ID. What I'm trying to create is a an object with IDs as keys and values being an array describing the title-path to that ID.
I'm no master at recursion so my attempt below doesn't exactly provide the result I need.
Here's the original nested tag array:
const tags = [
{
title: 'Wood',
id: 'dkgkeixn',
tags: [
{
title: 'Material',
id: 'ewyherer'
},
{
title: 'Construction',
id: 'cchtfyjf'
}
]
},
{
title: 'Steel',
id: 'drftgycs',
tags: [
{
title: 'Surface',
id: 'sfkstewc',
tags: [
{
title: 'Polished',
id: 'vbraurff'
},
{
title: 'Coated',
id: 'sdusfgsf'
}
]
},
{
title: 'Quality',
id: 'zsasyewe'
}
]
}
]
The output I'm trying to get is this:
{
'dkgkeixn': ['Wood'],
'ewyherer': ['Wood', 'Material'],
'cchtfyjf': ['Wood', 'Construction'],
'drftgycs': ['Steel'],
'sfkstewc': ['Steel', 'Surface'],
'vbraurff': ['Steel', 'Surface', 'Polished'],
'sdusfgsf': ['Steel', 'Surface', 'Coated'],
'zsasyewe': ['Steel', 'Quality']
}
So I'm building this recursive function which is almost doing it's job, but I keep getting the wrong paths in my flat/key map:
function flatMap(tag, acc, pathBefore) {
if (!acc[tag.id]) acc[tag.id] = [...pathBefore];
acc[tag.id].push(tag.title);
if (tag.tags) {
pathBefore.push(tag.title)
tag.tags.forEach(el => flatMap(el, acc, pathBefore))
}
return acc
}
const keyMap = flatMap({ title: 'Root', id: 'root', tags}, {}, []);
console.log("keyMap", keyMap)
I'm trying to get the path until a tag with no tags and then set that path as value for the ID and then push the items 'own' title. But somehow the paths get messed up.
Check this, makePaths arguments are tags, result object and prefixed titles.
const makePaths = (tags, res = {}, prefix = []) => {
tags.forEach(tag => {
const values = [...prefix, tag.title];
Object.assign(res, { [tag.id]: values });
if (tag.tags) {
makePaths(tag.tags, res, values);
}
});
return res;
};
const tags = [
{
title: "Wood",
id: "dkgkeixn",
tags: [
{
title: "Material",
id: "ewyherer"
},
{
title: "Construction",
id: "cchtfyjf"
}
]
},
{
title: "Steel",
id: "drftgycs",
tags: [
{
title: "Surface",
id: "sfkstewc",
tags: [
{
title: "Polished",
id: "vbraurff"
},
{
title: "Coated",
id: "sdusfgsf"
}
]
},
{
title: "Quality",
id: "zsasyewe"
}
]
}
];
console.log(makePaths(tags));

Create randomly generated divs that fill a box jQuery and Javascript

I have a section on my website that is 100% wide and 450 pixels tall.
My html looks like so...
<section class="interactive-banner">
<figure></figure>
</section>
I want each 'figure' element to be 150 pixels wide and 150 pixels tall, I want to generate the 'figure' html automatically and randomly with jQuery, and to consist of some inner html.
I have the following...
$(function(){
var people = [
{ id: 1 },
{ id: 2 }
];
var figure = $('figure');
w = 1500;
h = 450;
var counter = 0;
var data = people[Math.floor(Math.random()*people.length)];
(function nextFade() {
counter++;
figure.clone().html(data.name).appendTo('.interactive-banner').hide().fadeIn(150, function() {
if(counter < 30) nextFade();
});
})();
});
I want each figure element to fade in 1 after the other, in total I will only have 7 original figures, only these 7 will be randomly cloned until i have 30 iterations in total, I want the figure html to contain the data inside each object in my people array, so each figure is an object so to speak, output as so...
<figure>
<img src="[image src from object inside array]" />
<div class="information">
<h5>[name from object inside of array ]</h5>
<p>[job title from object inside of array ]</p>
</div>
</figure>
only at the minute its being output as so...
<figure style="display: block;">
Chris
</figure>
Ive created an example here, as you see however each figure contains the same information...
http://jsfiddle.net/pGmeE/
http://jsbin.com/isopin/1/edit
Don't populate your section initially and don't clone your figure element with jQ. Rather create a new one at every loop iteration.
<section class="interactive-banner"></section>
jQ:
$(function(){
var people = [
{ id: 1, name: 'Justin', title: 'Head Designer', bio: 'This is Justin\'s Biography.', image: 'justin.jpg' },
{ id: 2, name: 'Chris', title: 'Head Developer', bio: 'This is Chris\' Biography.', image: 'chris.jpg' },
{ id: 3, name: 'Sam', title: 'Developer', bio: 'This is Sam\'s Biography.', image: 'sam.jpg' },
{ id: 4, name: 'Haythem', title: 'Developer', bio: 'This is Haythem\'s Biography.', image: 'haythem.jpg' },
{ id: 5, name: 'Geoff', title: 'Designer', bio: 'This is Geoff\'s Biography.', image: 'geoff.jpg' },
{ id: 6, name: 'Liam', title: 'Designer', bio: 'This is Liam\'s Biography.', image: 'liam.jpg' }
];
w = 1500;
h = 450;
var counter = 0;
(function nextFade() {
counter++;
// Give "random" a chance to get random again
var data = people[Math.floor(Math.random()*people.length)];
// Now create a new Figure element:
var figure = $('<figure />');
figure.html(data.name).appendTo('.interactive-banner').hide().fadeIn(150, function() {
if(counter < 30) nextFade();
});
})();
});

Categories

Resources