1 | /**
|
2 | * Styledown is available as a Node.js package.
|
3 | *
|
4 | * var Styledown = require('styledown');
|
5 | */
|
6 |
|
7 | var Marked = require('marked'),
|
8 | Cheerio = require('cheerio'),
|
9 | extend = require('util')._extend,
|
10 | mdextract = require('mdextract');
|
11 |
|
12 | module.exports = Styledown;
|
13 |
|
14 | var addClasses = require('./lib/filters').addClasses;
|
15 | var sectionize = require('./lib/filters').sectionize;
|
16 | var unpackExample = require('./lib/filters').unpackExample;
|
17 | var processConfig = require('./lib/filters').processConfig;
|
18 | var removeConfig = require('./lib/filters').removeConfig;
|
19 | var isolateTextBlocks = require('./lib/filters').isolateTextBlocks;
|
20 | var htmlize = require('./lib/utils').htmlize;
|
21 | var prefixClass = require('./lib/utils').prefixClass;
|
22 |
|
23 | /**
|
24 | * Styledown.parse() : Styledown.parse(source, [options])
|
25 | * Generates HTML from a given `source`.
|
26 | *
|
27 | * Styledown.parse('### hello *world*');
|
28 | * => "<!doctype html><html>..."
|
29 | *
|
30 | * `source` can be a String or an Array. as a string, it's assumed to be a
|
31 | * Markdown document. As an array, it's assumed to be a list of files. It's
|
32 | * expected that it contains objects with `name` and `data` keys.
|
33 | *
|
34 | * In array mode, Styledown treats each file differently. Inline comments are
|
35 | * extracted from those with that end in CSS extensions (css, less, sass, etc),
|
36 | * while the rest are assumed to be Markdown documents.
|
37 | *
|
38 | * var docs = [
|
39 | * { name: 'css/style.css', data: '...' },
|
40 | * { name: 'config.md', data: '...' }
|
41 | * ];
|
42 | *
|
43 | * Styledown.parse(docs);
|
44 | * => "<!doctype html><html>..."
|
45 | *
|
46 | * You may pass `options` as the second parameter. Available options are:
|
47 | *
|
48 | * ~ prefix (String): prefix for classnames. Defaults to `sg`.
|
49 | * ~ template (String): HTML template. Defaults to a simple HTML template.
|
50 | * ~ head (String): HTML to put in the head. Default to `false`.
|
51 | * ~ body (String): HTML to put in the body. Defaults to `<div sg-content></div>`.
|
52 | * ~ indentSize (Number): Number of spaces to indent. Defaults to `2`.
|
53 | * ~ inline (Boolean): if `true`, then inline CSS mode is forced.
|
54 | *
|
55 | * This is shorthand for `new Styledown().toHTML()`. You can use `Styledown` as a class.
|
56 | */
|
57 |
|
58 | Styledown.parse = function (source, options) {
|
59 | return new Styledown(source, options).toHTML();
|
60 | };
|
61 |
|
62 | /**
|
63 | * Styledown.version:
|
64 | * The version number in semver format.
|
65 | */
|
66 |
|
67 | Styledown.version = require('./package.json').version;
|
68 |
|
69 | /**
|
70 | * Styledown.defaults:
|
71 | * The returns the default configuration file, JS file and CSS files.
|
72 | *
|
73 | * Styledown.defaults.conf()
|
74 | * Styledown.defaults.js()
|
75 | * Styledown.defaults.css()
|
76 | */
|
77 |
|
78 | Styledown.defaults = {
|
79 | conf: function () {
|
80 | return require('./lib/default_conf');
|
81 | },
|
82 | js: function () {
|
83 | return require('fs').readFileSync(__dirname + '/data/styledown.js');
|
84 | },
|
85 | css: function () {
|
86 | return require('fs').readFileSync(__dirname + '/data/styledown.css');
|
87 | },
|
88 | };
|
89 |
|
90 | /***
|
91 | * Styledown() : new Styledown(source, [options])
|
92 | * Parses the source `source` into a Styledown document. `source` can be a
|
93 | * Markdown document.
|
94 | *
|
95 | * doc = new Styledown(markdown);
|
96 | * doc.toHTML();
|
97 | *
|
98 | * You may also use `Styledown.parse()` as a shorthand.
|
99 | */
|
100 |
|
101 | function Styledown (src, options) {
|
102 | this.options = extend(extend({}, Styledown.defaultOptions), options || {});
|
103 | this.raw = this.extract(src);
|
104 | this.$ = Cheerio.load(Marked(this.raw));
|
105 |
|
106 | this.process();
|
107 | }
|
108 |
|
109 | Styledown.defaultOptions = {
|
110 |
|
111 | /* HTML template */
|
112 | template: [
|
113 | "<!doctype html>",
|
114 | "<html>",
|
115 | "<head>",
|
116 | "<meta charset='utf-8'>",
|
117 | "<title>Styledown</title>",
|
118 | "</head>",
|
119 | "<body>",
|
120 | "</body>",
|
121 | "</html>"
|
122 | ].join("\n"),
|
123 |
|
124 | /* Things to put into `head` */
|
125 | head: false,
|
126 |
|
127 | /* Force inline mode */
|
128 | inline: false,
|
129 |
|
130 | /* Things to put into `body` */
|
131 | body: "<div sg-content></div>",
|
132 |
|
133 | /* Prefix for classnames */
|
134 | prefix: 'sg',
|
135 |
|
136 | /* Indentation spaces */
|
137 | indentSize: 2
|
138 | };
|
139 |
|
140 | Styledown.prototype = {
|
141 |
|
142 | /**
|
143 | * toHTML() : doc.toHTML()
|
144 | * Returns the full HTML source based on the Styledown document.
|
145 | *
|
146 | * doc.toHTML()
|
147 | * => "<!doctype html><html>..."
|
148 | */
|
149 |
|
150 | toHTML: function() {
|
151 | var html = this.toBareHTML();
|
152 |
|
153 | if (this.options.head !== false) {
|
154 | // Unpack template
|
155 | var $ = Cheerio.load(htmlize(this.options.template));
|
156 | $('body').append(htmlize(this.options.body));
|
157 | $('[sg-content]').append(html).removeAttr('sg-content');
|
158 | $('html, body').addClass(this.options.prefix);
|
159 | $('head').append(htmlize(this.options.head));
|
160 |
|
161 | html = $.html();
|
162 | }
|
163 |
|
164 | html = this.prettyprint(html, { wrap_line_length: 0 });
|
165 | return html;
|
166 | },
|
167 |
|
168 | /**
|
169 | * toBareHTML() : doc.toBareHTML()
|
170 | * Returns the bare HTML without the head/body templates.
|
171 | *
|
172 | * doc.toBareHTML()
|
173 | * => "<div><h3>Your document</h3>..."
|
174 | */
|
175 |
|
176 | toBareHTML: function () {
|
177 | return this.$.html();
|
178 | },
|
179 |
|
180 | /**
|
181 | * extract():
|
182 | * (private) extracts a Markdown source from given `src`.
|
183 | */
|
184 |
|
185 | extract: function (src) {
|
186 | var self = this;
|
187 |
|
188 | if (typeof src === 'string')
|
189 | return src;
|
190 |
|
191 | if (Array.isArray(src)) {
|
192 | return src.map(function (f) {
|
193 | if (self.options.inline || f.name && f.name.match(/(sass|scss|styl|less|css)$/)) {
|
194 | return mdextract(f.data, { lang: 'css' }).toMarkdown();
|
195 | } else {
|
196 | return f.data;
|
197 | }
|
198 | }).join("\n");
|
199 | }
|
200 | },
|
201 |
|
202 | /**
|
203 | * process() : doc.process()
|
204 | * (private) processes things. Done on the constructor.
|
205 | */
|
206 |
|
207 | process: function () {
|
208 | var highlightHTML = this.highlightHTML.bind(this);
|
209 | var p = this.prefix.bind(this);
|
210 | var src = this.raw;
|
211 |
|
212 | processConfig(src, this.options);
|
213 | removeConfig(this.$);
|
214 |
|
215 | var pre = this.options.prefix;
|
216 | var $ = this.$;
|
217 |
|
218 | addClasses($, p);
|
219 | sectionize($, 'h3', p, { 'class': p('block') });
|
220 | sectionize($, 'h2', p, { 'class': p('section'), until: 'h1, h2' });
|
221 |
|
222 | $('pre').each(function () {
|
223 | unpackExample($(this), p, highlightHTML);
|
224 | });
|
225 |
|
226 | isolateTextBlocks(this.$, p);
|
227 | },
|
228 |
|
229 | /**
|
230 | * prettyprint() : doc.prettyprint(html)
|
231 | * (private) Reindents given `html` based on the indent size option.
|
232 | *
|
233 | * doc.prettyprint('<div><a>hello</a></div>')
|
234 | * => "<div>\n <a>hello</a>\n</div>"
|
235 | */
|
236 |
|
237 | prettyprint: function (html, options) {
|
238 | var beautify = require('js-beautify').html_beautify;
|
239 |
|
240 | var opts = {
|
241 | indent_size: this.options.indentSize,
|
242 | wrap_line_length: 120,
|
243 | unformatted: ['pre']
|
244 | };
|
245 |
|
246 | // js-beautify sometimes trips when the first character isn't a <. not
|
247 | // sure... but might as well do this.
|
248 | html = html.trim();
|
249 |
|
250 | var output = beautify(html, extend(opts, options));
|
251 |
|
252 | // cheerio output tends to have a bunch of extra newlines. kill them.
|
253 | output = output.replace(/\n\n+/g, "\n\n");
|
254 |
|
255 | return output;
|
256 | },
|
257 |
|
258 | /**
|
259 | * highlightHTML():
|
260 | * (private) Syntax highlighting helper
|
261 | */
|
262 |
|
263 | highlightHTML: function (html) {
|
264 | var Hljs = require('highlight.js');
|
265 |
|
266 | html = this.prettyprint(html);
|
267 | html = Hljs.highlight('html', html).value;
|
268 | return html;
|
269 | },
|
270 |
|
271 | /**
|
272 | * prefix():
|
273 | * (private) Prefix classnames. Takes `options.prefix` into account.
|
274 | *
|
275 | * prefix('block')
|
276 | * => 'sg-block'
|
277 | */
|
278 |
|
279 | prefix: function(klass) {
|
280 | return klass ?
|
281 | prefixClass(klass, this.options.prefix) :
|
282 | this.options.prefix;
|
283 | }
|
284 | };
|