Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Markdown headings extraction possibility #8

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@ Metalsmith(__dirname)
.build();
```

### Enhanced mode (Markdown)

Use an array and even decide, if it should already parse your Markdown files:

```js
var Metalsmith = require('metalsmith');
var headings = require('headings');
var markdown = require('metalsmith-markdown');

Metalsmith(__dirname)
.use(headings({
mode: 'md', // set to anything else or remove this property for HTML-mode
selectors: ['h1', 'h2'],
}))
.use(markdown()) // Invoke this function somewhere after 'headings'
.build();
```

Bear in mind to include this function somewhere before your `.use(markdown())` function.
Otherwise, `metalsmith` would compile your files into HTML before it would extract your headings.

One other thing to mention would be that `mode: 'md'` orders your headings according to their position in the `selectors` array in the options object, whereas the *legacy* HTML-mode orders your headings according to their position in your files. You choose.

## License

MIT
86 changes: 70 additions & 16 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,95 @@

var cheerio = require('cheerio');
var debug = require('debug')('metalsmith-headings');
var extname = require('path').extname;
var slug = require('slug');

/**
* Expose `plugin`.
*/

module.exports = plugin;

/**
* Filters selectors out of a Markdown file
* @param {Object} data
* @param {string[]} selectors
*/
function filterMarkdown(data, selectors) {
const headers = {
h1: /^#{1}\s+(.+?)$/gim,
h2: /^#{2}\s+(.+?)$/gim,
h3: /^#{3}\s+(.+?)$/gim,
h4: /^#{4}\s+(.+?)$/gim,
h5: /^#{5}\s+(.+?)$/gim,
h6: /^#{6}\s+(.+?)$/gim,
};
const regexes = selectors.map((selector) => headers[selector]).filter((entry) => entry !== null || typeof entry !== 'undefined');
const contents = data.contents.toString();
data.headings = [];

regexes.forEach((regex, index) => {
let found;
while ((found = regex.exec(contents)) !== null) {
if (found.index === regex.lastIndex) {
regex.lastIndex++;
}

if (found[1].length) {
data.headings.push({
id: slug(found[1]),
tag: selectors[index],
text: found[1],
});
}
}
});
return data;
}

/**
* Filters selectors out of an HTML file
* @param {Object} data the file object
* @param {string[]} selectors The selectors array
*/
function filterHTML(data, selectors) {
var contents = data.contents.toString();
var $ = cheerio.load(contents);
data.headings = [];
$(selectors.join(',')).each(function() {
data.headings.push({
id: $(this).attr('id'),
tag: $(this)[0].name,
text: $(this).text()
});
});
return data;
}

/**
* Get the headings from any html files.
*
* @param {String or Object} options (optional)
* @property {Array} selectors
*/

function plugin(options){
function plugin(options) {
if ('string' == typeof options) options = { selectors: [options] };
options = options || {};
options = options || { };
var selectors = options.selectors || ['h2'];

return function(files, metalsmith, done){
setImmediate(done);
Object.keys(files).forEach(function(file){
if ('.html' != extname(file)) return;
Object.keys(files).forEach(function(file) {
var data = files[file];
var contents = data.contents.toString();
var $ = cheerio.load(contents);
data.headings = [];

$(selectors.join(',')).each(function(){
data.headings.push({
id: $(this).attr('id'),
tag: $(this)[0].name,
text: $(this).text()
});
});
debug(`Currently processing: ${file}\nMode: ${options.mode || 'html (default)'}`);
if (options.mode !== 'md' && '.html' === extname(file)) {
data = filterHTML(data, selectors);
}
if (options.mode === 'md' && '.md' === extname(file)) {
data = filterMarkdown(data, selectors);
}
debug(`Found: ${data.headings.length} headings.`);
return;
});
};
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
"test": "mocha --reporter spec"
},
"dependencies": {
"cheerio": "^0.14.0"
"cheerio": "^0.14.0",
"debug": "^3.1.0",
"slug": "^0.9.1"
},
"devDependencies": {
"mocha": "1.x",
"metalsmith": "0.x",
"metalsmith-markdown": "^0.2.1"
"metalsmith-markdown": "^0.2.1",
"mocha": "1.x"
}
}
30 changes: 30 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,36 @@ describe('metalsmith-headings', function(){
});
});

it('should parse headings from Markdown only for h2', function(done){
Metalsmith('test/fixture')
.use(headings({ selectors: ['h2'], mode: 'md' }))
.use(markdown())
.build(function(err, files){
if (err) return done(err);
assert.deepEqual(files['index.html'].headings, [
{ id: 'two-one', tag: 'h2', text: 'two one' },
{ id: 'two-two', tag: 'h2', text: 'two two' }
]);
done();
});
});

it('should parse headings from Markdown for h1 and h2', function(done){
Metalsmith('test/fixture')
.use(headings({ selectors: ['h1', 'h2'], mode: 'md' }))
.use(markdown())
.build(function(err, files){
if (err) return done(err);
assert.deepEqual(files['index.html'].headings, [
{ id: 'one-one', tag: 'h1', text: 'one one' },
{ id: 'one-two', tag: 'h1', text: 'one two' },
{ id: 'two-one', tag: 'h2', text: 'two one' },
{ id: 'two-two', tag: 'h2', text: 'two two' }
]);
done();
});
});

it('should accept a string shorthand', function(done){
Metalsmith('test/fixture')
.use(markdown())
Expand Down
Loading