简化了,就三个命令:
- new:使用线上模板创建一个新的 md 文件
- serve:启动一个 md 文件的 webpack dev server
- build:编译产出一个 md 文件
```bash
# create a new slide with an official template
$ nodeppt new slide.md
# create a new slide straight from a github template
$ nodeppt new slide.md -t username/repo
# start local sever show slide
$ nodeppt serve slide.md
# to build a slide
$ nodeppt build slide.md
```
### 帮助
```bash
# help
nodeppt -h
# 获取帮助
nodeppt serve -h
```
will also get stripped.
var paragraphOpens = 0;
for (var i = blkIdx + 1; i < state.tokens.length; i++) {
if (state.tokens[i].type === 'paragraph_open') {
paragraphOpens++;
continue;
} else if (state.tokens[i].type !== 'paragraph_close') {
continue;
}
if (paragraphOpens > 0) {
paragraphOpens--;
continue;
}
// OK, this is the paragraph_close matching the open we started on.
// What came right before here?
var prevBlkToken = state.tokens[i - 1];
if (prevBlkToken.type !== 'inline') {
break;
}
var prevInlineToken = prevBlkToken.children[prevBlkToken.children.length - 1];
if (prevInlineToken.type !== 'fa_block' && prevInlineToken.type !== 'fa_span') {
break;
}
// If we got this far, we're stripping the surrounding paragraph.
// FIXME Also a hack. The 'inline' JSX that's inside the paragraph should
// now get a linebreak after it in the generated HTML. Easier to test
// and looks better in the HTML.
prevInlineToken.content += '\n';
paragraphTokensToRemove.push(blkIdx, i);
break;
}
}
state.tokens = state.tokens.filter(function(blkToken, idx) {
return paragraphTokensToRemove.indexOf(idx) === -1;
});
});
};
================================================
FILE: packages/nodeppt-parser/lib/markdown/img.js
================================================
const mditr = require('./regexp');
const {getAttrs, getAttrsString} = require('./attrs/utils');
module.exports = md => {
md.use(
mditr(
/!!\[(\w+|)(.*?)?]\((.+?)\)/,
(match, utils) => {
let attrs = match[2] || '';
if (attrs) {
attrs = getAttrsString(
getAttrs(`{${attrs}}`, 0, {
leftDelimiter: '{',
rightDelimiter: '}'
})
);
}
let src = match[3];
let srcs = src.split(' ');
src = srcs.shift();
let imgAttrs = '';
if (srcs.length) {
imgAttrs = getAttrsString(
getAttrs(`{${srcs.join(' ')}}`, 0, {
leftDelimiter: '{',
rightDelimiter: '}'
})
);
}
if (match[1]) {
return `<${match[1]} ${attrs}>${match[1]}>`;
} else {
return ``;
}
},
'img_block'
)
);
md.core.ruler.push('img_blockify', function(state) {
var paragraphTokensToRemove = [];
var lastInlineTokenSeen;
for (var blkIdx = 0; blkIdx < state.tokens.length; blkIdx++) {
if (state.tokens[blkIdx].type !== 'paragraph_open') {
continue;
}
var nextBlkToken = state.tokens[blkIdx + 1];
if (nextBlkToken.type !== 'inline') {
continue;
}
// FIXME Incorrect and a hack:
//
...
will also get stripped.
var paragraphOpens = 0;
for (var i = blkIdx + 1; i < state.tokens.length; i++) {
if (state.tokens[i].type === 'paragraph_open') {
paragraphOpens++;
continue;
} else if (state.tokens[i].type !== 'paragraph_close') {
continue;
}
if (paragraphOpens > 0) {
paragraphOpens--;
continue;
}
var prevBlkToken = state.tokens[i - 1];
if (prevBlkToken.type !== 'inline') {
break;
}
// console.log(prevBlkToken.content)
var prevInlineToken = prevBlkToken.children[prevBlkToken.children.length - 1];
if (prevInlineToken.type !== 'img_block') {
break;
}
prevInlineToken.content += '\n';
paragraphTokensToRemove.push(blkIdx, i);
break;
}
}
state.tokens = state.tokens.filter((blkToken, idx) => {
return paragraphTokensToRemove.indexOf(idx) === -1;
});
});
};
================================================
FILE: packages/nodeppt-parser/lib/markdown/jsx.js
================================================
'use strict';
// from https://github.com/osnr/markdown-it-jsx/
var jsx_inline = require('markdown-it-jsx/lib/jsx_inline');
var escape_code = require('markdown-it-jsx/lib/escape_code');
module.exports = function jsx_plugin(md) {
md.set({xhtmlOut: true});
// JSX should entirely replace embedded HTML.
md.inline.ruler.before('html_inline', 'jsx_inline', jsx_inline);
md.disable('html_inline');
// We'll parse blocks as inline and then strip the surrounding paragraph at the end; it's easier.
md.disable('html_block');
md.core.ruler.push('jsx_blockify', function(state) {
// Look for things like
...
and strip the
,
there.
// FIXME Quadratic time in worst case, I think?
var paragraphTokensToRemove = [];
var lastInlineTokenSeen;
for (var blkIdx = 0; blkIdx < state.tokens.length; blkIdx++) {
if (state.tokens[blkIdx].type !== 'paragraph_open') {
continue;
}
var nextBlkToken = state.tokens[blkIdx + 1];
if (nextBlkToken.type !== 'inline' || nextBlkToken.children[0].type !== 'jsx_inline') {
continue;
}
// FIXME Incorrect and a hack:
//
...
will also get stripped.
var paragraphOpens = 0;
for (var i = blkIdx + 1; i < state.tokens.length; i++) {
if (state.tokens[i].type === 'paragraph_open') {
paragraphOpens++;
continue;
} else if (state.tokens[i].type !== 'paragraph_close') {
continue;
}
if (paragraphOpens > 0) {
paragraphOpens--;
continue;
}
// OK, this is the paragraph_close matching the open we started on.
// What came right before here?
var prevBlkToken = state.tokens[i - 1];
if (prevBlkToken.type !== 'inline') {
break;
}
var prevInlineToken = prevBlkToken.children[prevBlkToken.children.length - 1];
if (prevInlineToken.type !== 'jsx_inline') {
break;
}
// If we got this far, we're stripping the surrounding paragraph.
// FIXME Also a hack. The 'inline' JSX that's inside the paragraph should
// now get a linebreak after it in the generated HTML. Easier to test
// and looks better in the HTML.
prevInlineToken.content += '\n';
paragraphTokensToRemove.push(blkIdx, i);
break;
}
}
state.tokens = state.tokens.filter(function(blkToken, idx) {
return paragraphTokensToRemove.indexOf(idx) === -1;
});
});
// 去掉代码注释
// md.renderer.rules.fence = escape_code(md.renderer.rules.fence)
// md.renderer.rules.code_inline = escape_code(md.renderer.rules.code_inline)
// md.renderer.rules.code_block = escape_code(md.renderer.rules.code_block)
md.renderer.rules['jsx_inline'] = function(tokens, idx) {
// If the span is JSX, just pass the original source for the span
// through to output.
return tokens[idx].content;
};
};
================================================
FILE: packages/nodeppt-parser/lib/markdown/link.js
================================================
module.exports = md => {
const defLinkOpen =
md.renderer.rules.link_open ||
function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
// 增加 target
const aIndex = tokens[idx].attrIndex('target');
const hrefIndex = tokens[idx].attrIndex('href');
if (hrefIndex >= 0) {
// 从在 hrefIndex
const href = tokens[idx].attrs[hrefIndex][1];
if (href && href !== '#') {
if (aIndex < 0) {
if (/^http[s]?:\/\//.test(href)) {
// 只是添加外链
tokens[idx].attrPush(['target', '_blank']); // add new attribute
}
} else {
tokens[idx].attrs[aIndex][1] = '_blank'; // replace value of existing attr
}
} else {
// 空连接不增加 _blank 替换成 JavaScript:void
tokens[idx].attrs[hrefIndex][1] = 'javascript:void(0)';
}
}
// pass token to default renderer.
return defLinkOpen(tokens, idx, options, env, self);
};
};
// module.exports = md => {
// md.use(
// mditr(
// /\[\!(\.\w+\s+)?(.+?)]\((.*?)\)/,
// (match, utils) => {
// let cls = match[1] ? match[1].slice(1).trim() : ''
// if (cls) {
// cls = cls.split('.').join(' ')
// }
// let content = match[2]
// content = md.render(content)
// return `${content}`
// },
// 'button_inline'
// )
// )
// }
================================================
FILE: packages/nodeppt-parser/lib/markdown/mermaid.js
================================================
module.exports = md => {
const temp = md.renderer.rules.fence.bind(md.renderer.rules);
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
const token = tokens[idx];
const code = token.content;
if (token.info === 'mermaid') {
token.attrJoin('class', 'lang-mermaid no-style');
let attrs = token.attrs || [];
attrs = attrs
.map(([key, value]) => {
return `${key}="${value}"`;
})
.join(' ');
// 增加对 mermaidjs 支持,这样就可以画流程图了哦~
return `
${code}
`;
}
return temp(tokens, idx, options, env, slf);
};
};
================================================
FILE: packages/nodeppt-parser/lib/markdown/plus-list.js
================================================
function findChecklists(state) {
// console.log(state.tokens);
state.tokens.forEach((token, i) => {
if (token.type === 'bullet_list_open' && token.markup === '+') {
// console.log(state.tokens[i + 1]);
// applyClass(token);
}
});
}
/**
* Adds class to token
*
* @param {any} token
* @param {string} className
*/
function applyClass(token) {
// init attributes
token.attrs = token.attrs || [];
// get index of class attribute
let keys = token.attrs.map(arr => arr[0]);
let idx = keys.indexOf('class');
if (idx === -1) {
// Add class attribute if not defined
token.attrs.push(['class', className]);
} else {
// Get the current class list to append.
// Watch out for duplicates
let classStr = token.attrs[idx][1] || '';
let classList = classStr.split(' ');
// Add the class if we don't already have it
if (classList.indexOf(className) === -1) {
token.attrs[idx][1] = classStr += ' ' + className;
}
}
}
module.exports = function plugin(md) {
// Default class name
md.block.ruler.after('list', 'list-checkmarks', state => {
findChecklists(state);
});
};
================================================
FILE: packages/nodeppt-parser/lib/markdown/prism.js
================================================
// https://github.com/jGleitz/markdown-it-prism/
// 增加attrs支持
const Prism = require('prismjs');
/**
* A callback that can be used to perform custom initialisation of the Prism instance.
*
* @callback PrismInitialisationCallback
* @param {Prism} prism
* The Prism instance
*/
/**
* The options for the markdown-it-prism plugin
*
* @typedef {Object} MarkdownItPrismOptions
* @property {String[]} plugins
* Names of Prism plugins to load
* @property {PrismInitialisationCallback} init
* Callback for Prism initialisation
* @property {String} defaultLanguageForUnknown
* The language to use for code blocks that specify a language that Prism does not know
* @property {String} defaultLanguageForUnspecified
* The language to use for code block that do not specify a language
* @property {String} defaultLanguage
* Shorthand to set both {@code defaultLanguageForUnknown} and {@code defaultLanguageForUnspecified} to the same value
*/
const DEFAULTS = {
plugins: [],
init: () => {},
defaultLanguageForUnknown: undefined,
defaultLanguageForUnspecified: undefined,
defaultLanguage: undefined
};
/**
* Loads the provided {@code lang} into prism.
*
* @param {String} lang
* Code of the language to load.
* @return {Object} The Prism language object for the provided {@code lang} code. {@code undefined} if the language is not known to Prism.
*/
function loadPrismLang(lang) {
if (!lang) return undefined;
let langObject = Prism.languages[lang];
if (langObject === undefined) {
try {
require('prismjs/components/index')([lang]);
return Prism.languages[lang];
} catch (e) {
// nothing to do
}
}
return langObject;
}
/**
* Loads the provided Prism plugin.a
* @param name
* Name of the plugin to load
* @throws {Error} If there is no plugin with the provided {@code name}
*/
function loadPrismPlugin(name) {
try {
require(`prismjs/plugins/${name}/prism-${name}`);
} catch (e) {
throw new Error(`Cannot load Prism plugin "${name}". Please check the spelling.`);
}
}
/**
* Select the language to use for highlighting, based on the provided options and the specified language.
*
* @param {Object} options
* The options that were used to initialise the plugin.
* @param {String} lang
* Code of the language to highlight the text in.
* @return {Array} An array where the first element is the name of the language to use, and the second element is the PRISM language object for that language.
*/
function selectLanguage(options, lang) {
let langToUse = lang;
if (langToUse === '' && options.defaultLanguageForUnspecified !== undefined) {
langToUse = options.defaultLanguageForUnspecified;
}
let prismLang = loadPrismLang(langToUse);
if (prismLang === undefined && options.defaultLanguageForUnknown !== undefined) {
langToUse = options.defaultLanguageForUnknown;
prismLang = loadPrismLang(langToUse);
}
return [langToUse, prismLang];
}
/**
* Highlights the provided text using Prism.
*
* @param {MarkdownIt} markdownit
* Instance of MarkdownIt Class. This argument is bound in markdownItPrism().
* @param {MarkdownItPrismOptions} options
* The options that have been used to initialise the plugin. This argument is bound in markdownItPrism().
* @param {String} text
* The text to highlight.
* @param {String} lang
* Code of the language to highlight the text in.
* @return {String} {@code text} wrapped in {@code <pre>} and {@code <code>}, both equipped with the appropriate class (markdown-it’s langPrefix + lang). If Prism knows {@code lang}, {@code text} will be highlighted by it.
*/
function highlight(markdownit, options, text, lang, attrs = []) {
attrs = attrs || [];
let [langToUse, prismLang] = selectLanguage(options, lang);
const code = prismLang ? Prism.highlight(text, prismLang) : markdownit.utils.escapeHtml(text);
let hasClass = false;
let preAttrs = [];
attrs = attrs
.map(([key, value]) => {
if (key === 'class') {
hasClass = true;
value += langToUse ? ` ${markdownit.options.langPrefix}${langToUse}` : '';
} else if (key === 'css-module') {
// 单独提取
preAttrs.push(['class', value]);
return '';
}
preAttrs.push([key, value]);
return `${key}="${value}"`;
})
.join(' ');
if (!hasClass && langToUse) {
attrs += `class="${markdownit.options.langPrefix}${langToUse}"`;
}
langToUse && preAttrs.push(['class', `${markdownit.options.langPrefix}${langToUse}`]);
let rs = {};
preAttrs.forEach(([key, value]) => {
rs[key] = rs[key] ? [rs[key], value].join(' ') : value;
});
preAttrs = Object.keys(rs)
.map(key => {
return `${key}="${rs[key]}"`;
})
.join(' ');
return `
${code}
`;
}
/**
* Checks whether an option represents a valid Prism language
*
* @param {MarkdownItPrismOptions} options
* The options that have been used to initialise the plugin.
* @param optionName
* The key of the option insides {@code options} that shall be checked.
* @throws {Error} If the option is not set to a valid Prism language.
*/
function checkLanguageOption(options, optionName) {
const language = options[optionName];
if (language !== undefined && loadPrismLang(language) === undefined) {
throw new Error(`Bad option ${optionName}: There is no Prism language '${language}'.`);
}
}
/**
* Initialisation function of the plugin. This function is not called directly by clients, but is rather provided
* to MarkdownIt’s {@link MarkdownIt.use} function.
*
* @param {MarkdownIt} markdownit
* The markdown it instance the plugin is being registered to.
* @param {MarkdownItPrismOptions} useroptions
* The options this plugin is being initialised with.
*/
module.exports = function markdownItPrism(markdownit, useroptions) {
const options = Object.assign({}, DEFAULTS, useroptions);
checkLanguageOption(options, 'defaultLanguage');
checkLanguageOption(options, 'defaultLanguageForUnknown');
checkLanguageOption(options, 'defaultLanguageForUnspecified');
options.defaultLanguageForUnknown = options.defaultLanguageForUnknown || options.defaultLanguage;
options.defaultLanguageForUnspecified = options.defaultLanguageForUnspecified || options.defaultLanguage;
options.plugins.forEach(loadPrismPlugin);
options.init(Prism);
replaceFence(markdownit, (...args) => highlight(markdownit, options, ...args));
};
function replaceFence(md, prismHighlight) {
const defaultFence = md.renderer.rules.fence;
md.renderer.rules.fence = function(tokens, idx, options, env, slf) {
let token = tokens[idx],
info = token.info ? String(token.info).trim() : '',
langName = '',
highlighted,
i,
tmpAttrs,
tmpToken;
if (info) {
langName = info.split(/\s+/g)[0];
}
highlighted = prismHighlight(token.content, langName, token.attrs);
if (highlighted.indexOf('/g, '>');
};
================================================
FILE: packages/nodeppt-parser/lib/markdown/span.js
================================================
const Utils = require('./attrs/utils');
const hasDelimiters = Utils.hasDelimiters('only', Utils.getOptions());
// 修改 https://github.com/pnewell/markdown-it-span/blob/master/index.js
module.exports = function ins_plugin(md) {
// Insert each marker as a separate text token, and add it to delimiter list
//
function tokenize(state, silent) {
var i,
scanned,
token,
len,
ch,
start = state.pos,
marker = state.src.charCodeAt(start);
if (silent) {
return false;
}
if (marker !== 0x3a /* : */) {
return false;
}
// 排查 :fa-:
if (state.src.charCodeAt(start + 1) === 0x66 /* f */ && state.src.charCodeAt(start + 2) === 0x61 /* a */) {
return false;
}
// 排查 ::fa-::
// 排查 :~fa-
if (
state.src.charCodeAt(start + 2) === 0x66 /* f */ &&
state.src.charCodeAt(start + 3) === 0x61 /* a */ &&
(state.src.charCodeAt(start + 1) === 0x3a /* : */ || state.src.charCodeAt(start + 1) === 0x7e) /* ~ */
) {
return false;
}
scanned = state.scanDelims(state.pos, true);
len = scanned.length;
ch = String.fromCharCode(marker);
if (len < 1) {
return false;
}
if (len % 1) {
token = state.push('text', '', 0);
token.content = ch;
len--;
}
for (i = 0; i < len; i += 1) {
token = state.push('text', '', 0);
token.content = ch + ch;
state.delimiters.push({
marker: marker,
jump: i,
token: state.tokens.length - 1,
level: state.level,
end: -1,
open: scanned.can_open,
close: scanned.can_close
});
}
state.pos += scanned.length;
return true;
}
// Walk through delimiter list and replace text tokens with tags
//
function postProcess(state) {
var i,
j,
startDelim,
endDelim,
token,
loneMarkers = [],
delimiters = state.delimiters,
max = state.delimiters.length;
for (i = 0; i < max; i++) {
startDelim = delimiters[i];
if (startDelim.marker !== 0x3a /* : */) {
continue;
}
if (startDelim.end === -1) {
continue;
}
endDelim = delimiters[startDelim.end];
token = state.tokens[startDelim.token];
token.type = 'span_open';
token.tag = 'span';
token.nesting = 1;
token.markup = ':';
token.content = '';
let jsx = state.tokens[endDelim.token + 1];
if (jsx && jsx.type === 'jsx_inline' && hasDelimiters(jsx.content)) {
// 说明是{.xxx}样式
let attrs = Utils.getAttrs(jsx.content, 0, Utils.getOptions());
Utils.addAttrs(attrs, token);
// 清理干净
jsx.type = 'text';
jsx.content = '';
}
token = state.tokens[endDelim.token];
token.type = 'span_close';
token.tag = 'span';
token.nesting = -1;
token.markup = ':';
token.content = '';
if (state.tokens[endDelim.token - 1].type === 'text' && state.tokens[endDelim.token - 1].content === ':') {
loneMarkers.push(endDelim.token - 1);
}
}
// If a marker sequence has an odd number of characters, it's splitted
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
// start of the sequence.
//
// So, we have to move all those markers after subsequent s_close tags.
//
while (loneMarkers.length) {
i = loneMarkers.pop();
j = i + 1;
while (j < state.tokens.length && state.tokens[j].type === 'span_close') {
j++;
}
j--;
if (i !== j) {
token = state.tokens[j];
state.tokens[j] = state.tokens[i];
state.tokens[i] = token;
}
}
}
md.inline.ruler.before('emphasis', 'span', tokenize);
md.inline.ruler2.before('emphasis', 'span', postProcess);
};
================================================
FILE: packages/nodeppt-parser/lib/tags/attrs.js
================================================
const {mergeAttrs} = require('../utils');
module.exports = tree => {
tree.walk(node => {
if (
node &&
node.tag &&
node.content &&
node.content.find &&
node.content.find(child => {
if (child && child.tag && child.attrs && child.attrs['css-module']) {
return true;
}
return false;
})
) {
// 说明是有子节点的父节点
// 查找三层数据,用于处理
const parentNode = node;
node.content.forEach(node => {
if (node.tag && node.attrs && node.attrs['css-module']) {
let ca = node.attrs['css-module'].split(/\s+/);
let rs = {};
let ex = /^([\w\-]+)=('|"){1}(.+)\2/;
parentNode.attrs = parentNode.attrs || {};
ca = ca.filter(attr => {
let m = ex.exec(attr);
if (m && m[1]) {
parentNode.attrs[m[1]] = m[3];
return false;
}
return true;
});
parentNode.attrs = mergeAttrs({class: ca.join(' ')}, parentNode.attrs);
delete node.attrs['css-module'];
}
});
}
return node;
});
};
================================================
FILE: packages/nodeppt-parser/lib/tags/header-footer.js
================================================
const utils = require('./utils');
module.exports = tree => {
let {slideNode, wrapNode} = utils(tree);
if (wrapNode.content && wrapNode.content.length) {
// console.log(wrapNode.content)
wrapNode.content = wrapNode.content.map(n => {
if (n.tag === 'footer' || n.tag === 'header') {
n.content = [
{
tag: 'div',
attrs: {
class: 'wrap'
},
content: n.content
}
];
if (n.tag === 'header') {
slideNode.content.unshift(n);
} else {
slideNode.content.push(n);
}
return false;
}
return n;
});
}
};
================================================
FILE: packages/nodeppt-parser/lib/tags/note.js
================================================
const utils = require('./utils');
module.exports = tree => {
let {slideNode, wrapNode} = utils(tree);
if (wrapNode.content && wrapNode.content.length) {
// console.log(wrapNode.content)
wrapNode.content = wrapNode.content.map(n => {
if (n.tag === 'note') {
n.tag = 'div';
let cls = '';
if (n.attrs) {
cls = n.attrs.class || '';
}
n.attrs = n.attrs || {};
cls = cls.split(/\s+/);
cls.push('speaker-note');
n.attrs.class = cls.join(' ');
n.content = [
{
tag: 'div',
attrs: {
class: 'wrap'
},
content: n.content
}
];
slideNode.content.push(n);
return false;
}
return n;
});
}
};
================================================
FILE: packages/nodeppt-parser/lib/tags/slide.js
================================================
const {mergeAttrs} = require('../utils');
const {getAttrs, getAttrsString} = require('../markdown/attrs/utils');
/**
*
*/
module.exports = tree => {
tree.match({tag: 'slide'}, node => {
node.tag = 'section';
node.attrs = mergeAttrs(
{
slide: true,
class: 'slide'
},
node.attrs
);
const attrs = node.attrs;
const wrapAttrs = {};
for (let i in attrs) {
if (i.startsWith(':')) {
// 这是 wrap 的样式
wrapAttrs[i.slice(1)] = attrs[i];
}
}
if (Object.keys(wrapAttrs).length > 0) {
node.content.forEach(node => {
if (node && node.tag === 'div' && node.attrs && node.attrs.wrap) {
node.attrs = mergeAttrs(node.attrs, wrapAttrs);
}
});
}
if (attrs.image) {
let [image, ...imgAttrs] = attrs.image.split(/\s+/);
imgAttrs = getAttrs(`{${imgAttrs.join(' ')}}`, 0, {
leftDelimiter: '{',
rightDelimiter: '}'
});
const rs = {};
let cls = [];
let noBackgroundClass = false;
if (imgAttrs.length) {
imgAttrs.forEach(([key, value]) => {
if (key === 'class') {
cls = value.split('.').map(c => {
if (!['dark', 'light', 'anim'].includes(c)) {
if (
[
'top',
'bottom',
'right',
'right-top',
'right-bottom',
'center',
'center-top',
'center-bottom',
'left',
'left-top',
'left-bottom'
].includes(c)
) {
noBackgroundClass = true;
}
return `background-${c}`;
}
return c;
});
} else {
rs[key] = value;
}
});
}
node.content.unshift({
tag: 'span',
attrs: {
...rs,
class: [noBackgroundClass ? '' : 'background', ...cls].join(' '),
style: `background-image:url('${image}')`
}
});
} else if (attrs.youtube) {
const ybAttrs = getAttrs(`{${attrs.youtube}}`, 0, {
leftDelimiter: '{',
rightDelimiter: '}'
});
const rs = {
'data-youtube': '',
};
let cls = [];
if (ybAttrs.length) {
ybAttrs.forEach(([key, value]) => {
if (key === 'class') {
cls = value.split('.').map(c => {
return c;
});
}else if(key==='id'){
key = 'youtube-id'
}
rs[`data-${key}`] = value.replace(/^[\'\"]|[\'\"]$/g, '');
});
}
if (cls.length === 0) {
cls.push('dark');
}
cls.push('embed');
node.content.unshift({
tag: 'div',
attrs: {class: cls.join(' ')},
content: [
{
tag: 'div',
attrs: rs
}
]
});
} else if (attrs.video) {
let [src, ...videoAttrs] = attrs.video.split(/\s+/);
videoAttrs = getAttrs(`{${videoAttrs.join(' ')}}`, 0, {
leftDelimiter: '{',
rightDelimiter: '}'
});
let rs = {};
let cls = [];
if (videoAttrs.length) {
videoAttrs.forEach(([key, value]) => {
if (key === 'class') {
cls = value.split('.').map(c => {
if (!['dark', 'light'].includes(c)) {
return `background-video-${c}`;
}
return c;
});
} else {
// console.log(value);
rs[key] = value.replace(/^\'|\'$/g, '');
}
});
}
rs = Object.assign(rs, {loop: true, muted: true});
const videoAttr = {
...rs,
autoplay: 'autoplay',
preload: 'true',
class: ['background-video', ...cls].join(' ')
};
// 支持多个
src = src.split(',').map(s => {
return {tag: 'source', attrs: {src: s}};
});
node.content.unshift({
tag: 'video',
attrs: videoAttr,
content: src
});
/**
*
*/
}
return node;
});
};
================================================
FILE: packages/nodeppt-parser/lib/tags/utils.js
================================================
module.exports = tree => {
let slide;
tree.match({tag: 'section'}, node => {
if (node.attrs && node.attrs.slide) {
slide = node;
}
return node;
});
let wrap;
tree.match({tag: 'div'}, node => {
if (node.attrs && node.attrs.wrap) {
wrap = node;
}
return node;
});
return {
slideNode: slide,
wrapNode: wrap
};
};
================================================
FILE: packages/nodeppt-parser/lib/utils.js
================================================
exports.mergeAttrs = (attrs1, attrs2 = {}) => {
for (let i in attrs2) {
switch (i) {
case 'class':
attrs1[i] = attrs1[i] ? [attrs1[i], attrs2[i]].join(' ') : attrs2[i];
break;
case 'style':
attrs1[i] = attrs1[i] ? [attrs1[i], attrs2[i]].join(';') : attrs2[i];
break;
default:
attrs1[i] = attrs2[i];
}
}
return attrs1;
};
================================================
FILE: packages/nodeppt-parser/lib/yaml-parser.js
================================================
const yaml = require('js-yaml');
function getSettings(str) {
const settings = yaml.load(str);
const pluginSettings = {};
if (settings.plugins && Array.isArray(settings.plugins)) {
settings.plugins = settings.plugins.map(p => {
if (typeof p === 'string') {
return p;
} else if (typeof p === 'object') {
const key = Object.keys(p)[0];
pluginSettings[key] = p[key];
return key;
}
});
}
settings.pluginsOptions = pluginSettings;
return settings;
}
module.exports = getSettings;
================================================
FILE: packages/nodeppt-parser/package.json
================================================
{
"name": "nodeppt-parser",
"version": "2.2.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"presentation",
"powerpoint",
"slideshow",
"keynote",
"ppt",
"slide",
"revealjs",
"impressjs",
"markdown-it",
"posthtml",
"webpack",
"nodeppt",
"markdown"
],
"author": {
"name": "Theo Wang",
"email": "ksky521@gmail.com"
},
"repository": {
"type": "git",
"url": "git://github.com/ksky521/nodeppt"
},
"license": "MIT",
"dependencies": {
"ejs": "^3.0.1",
"fs-extra": "^8.1.0",
"js-yaml": "^3.12.1",
"loader-utils": "^1.2.3",
"lodash.defaultsdeep": "^4.6.0",
"markdown-it": "^10.0.0",
"markdown-it-br": "^1.0.0",
"markdown-it-jsx": "1.1.0",
"@ksky521/markdown-it-katex":"^2.1.3",
"markdown-it-span": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"posthtml": "^0.12.0",
"posthtml-parser": "^0.4.1",
"posthtml-render": "^1.1.4",
"prismjs": "^1.15.0"
},
"gitHead": "9fba34ba1a8cc3ab149c2447c313e25b1e25093e"
}
================================================
FILE: packages/nodeppt-parser/template/index.ejs
================================================
<%= title %> - By <%= speaker %>
<% if (hasOwnProperty('prismTheme') && ['dark', 'coy', 'funky', 'okaidia', 'tomorrow', 'solarizedlight', 'twilight'].includes(prismTheme)) { %>
<% } %>
<% if (hasOwnProperty('css') && css.length) { %>
<% for (var i = 0;i
<% } %>
<% } %>
<% if (hasOwnProperty('plugins') && plugins && plugins.indexOf && ~plugins.indexOf('katex')) { %>
<% } %>
<%- content %>
<% if (hasOwnProperty('plugins') && plugins && plugins.indexOf && ~plugins.indexOf('echarts')) { %>
<% } %>
<% if (hasOwnProperty('plugins') && plugins && plugins.indexOf && ~plugins.indexOf('mermaid')) { %>
<% } %>
<% if (hasOwnProperty('js') && js.length) { %>
<% for (var i = 0;i
<% } %>
<% } %>
================================================
FILE: packages/nodeppt-serve/.npmignore
================================================
__tests__
__mocks__
package-lock.json
yarn.lock
================================================
FILE: packages/nodeppt-serve/PluginAPI.js
================================================
/**
* @file pluginAPI from vue cli
*/
const path = require('path');
class PluginAPI {
constructor(id, service) {
this.id = id;
this.service = service;
}
getEntry() {
return this.service.entry;
}
getEntryName() {
return path.parse(this.service.entry).name;
}
getNodepptOptions() {
return this.service.nodepptOptions;
}
/**
* 获取当前工作目录
* @return {string} 返回工作目录
*/
getCwd() {
return this.service.context;
}
/**
* 获取配置,不传入则返回全部
* @param {string} name
*/
getProjectOptions(name) {
const opts = this.service.projectOptions;
if (name) {
return opts[name];
}
return opts;
}
/**
* Resolve path for a project.
*
* @param {string} p - Relative path from project root
* @return {string} The resolved absolute path.
*/
resolve(p) {
return path.resolve(this.service.context, p);
}
registerCommand(name, opts, fn) {
if (typeof opts === 'function') {
fn = opts;
opts = null;
}
this.service.commands[name] = {fn, opts: opts || {}};
}
chainWebpack(fn) {
this.service.webpackChainFns.push(fn);
}
configureWebpack(fn) {
this.service.webpackRawConfigFns.push(fn);
}
configureDevServer(fn) {
this.service.devServerConfigFns.push(fn);
}
resolveWebpackConfig(chainableConfig) {
return this.service.resolveWebpackConfig(chainableConfig);
}
/**
* Resolve an intermediate chainable webpack config instance, which can be
* further tweaked before generating the final raw webpack config.
* You can call this multiple times to generate different branches of the
* base webpack config.
* See https://github.com/mozilla-neutrino/webpack-chain
*
* @return {ChainableWebpackConfig}
*/
resolveChainableWebpackConfig() {
return this.service.resolveChainableWebpackConfig();
}
}
module.exports = PluginAPI;
================================================
FILE: packages/nodeppt-serve/Service.js
================================================
/**
* @file 简版Service
*/
const path = require('path');
const fs = require('fs');
const Config = require('webpack-chain');
const merge = require('webpack-merge');
const PluginAPI = require('./PluginAPI');
const {error, isPlugin, chalk, warn, getDebugLogger} = require('nodeppt-shared-utils');
const defaultsDeep = require('lodash.defaultsdeep');
const defaults = require('./options');
const debug = getDebugLogger('Service', require('./package.json').name);
const configFileName = 'nodeppt.config.js';
module.exports = class Service {
constructor(context, entry, {plugins, pkg, useBuiltIn, nodepptOptions} = {}) {
this.initialized = false;
this.entry = path.resolve(context, entry);
// 这是nodeppt 传过来的配置,主要用version。。。
this.nodepptOptions = nodepptOptions;
this.pkg = pkg || {};
this.pkgContext = context;
this.commands = {};
this.context = context;
this.webpackChainFns = [];
this.webpackRawConfigFns = [];
this.devServerConfigFns = [];
this.plugins = this.resolvePlugins(plugins, useBuiltIn);
this.modes = this.plugins.reduce((modes, {apply: {defaultModes}}) => {
return Object.assign(modes, defaultModes);
}, {});
}
init(mode) {
if (this.initialized) {
return;
}
this.initialized = true;
this.mode = mode;
const userOptions = this.loadUserOptions();
const projectOptions = (this.projectOptions = defaultsDeep(userOptions, defaults()));
debug(projectOptions);
// apply plugins.
this.plugins.forEach(({id, apply}) => {
if (id.indexOf('markdown') === 0 || id.indexOf('posthtml') === 0) {
// 如果是以markdown 和posthtml开头的,则不是service插件,而是markdown-it和posthtml插件
return;
}
apply(new PluginAPI(id, this), projectOptions);
});
if (projectOptions.chainWebpack) {
this.webpackChainFns.push(projectOptions.chainWebpack);
}
if (projectOptions.configureWebpack) {
this.webpackRawConfigFns.push(projectOptions.configureWebpack);
}
}
loadUserOptions() {
// hulk.config.js
let fileConfig;
let resolved = {};
const configPath = path.resolve(this.context, configFileName);
if (fs.existsSync(configPath)) {
try {
fileConfig = require(configPath);
if (!fileConfig || typeof fileConfig !== 'object') {
error(`Error loading ${chalk.bold(configFileName)}: should export an object.`);
fileConfig = null;
}
} catch (e) {
error(`Error loading ${chalk.bold(configFileName)}:`);
throw e;
}
}
if (fileConfig) {
resolved = fileConfig;
}
// normalize some options
ensureSlash(resolved, 'baseUrl');
if (typeof resolved.baseUrl === 'string') {
resolved.baseUrl = resolved.baseUrl.replace(/^\.\//, '');
}
removeSlash(resolved, 'outputDir');
return resolved;
}
async run(name, args = {}, rawArgv = []) {
// debugger;
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name]);
process.env.NODE_ENV = mode || 'development';
// load env variables, load user config, apply plugins
this.init(mode);
args._ = args._ || [];
let command = this.commands[name];
if (!command && name) {
error(`command "${name}" does not exist.`);
process.exit(1);
}
if (!command || args.help) {
command = this.commands.help;
} else {
args._.shift(); // remove command itself
rawArgv.shift();
}
const {fn} = command;
return fn(args, rawArgv);
}
resolvePlugins(inlinePlugins, useBuiltIn) {
const idToPlugin = (id) => ({
id: id.replace(/^.\//, 'built-in:'),
apply: require(id),
});
let plugins;
const builtInPlugins = [
'./commands/serve',
'./commands/build',
// config plugins are order sensitive
'./config/base',
'./config/css',
'./config/dev',
'./config/prod',
'./config/app',
].map(idToPlugin);
if (inlinePlugins) {
plugins = useBuiltIn !== false ? builtInPlugins.concat(inlinePlugins) : inlinePlugins;
} else {
const projectPlugins = Object.keys(this.pkg.devDependencies || {})
.concat(Object.keys(this.pkg.dependencies || {}))
.filter(isPlugin)
.map(idToPlugin);
plugins = builtInPlugins.concat(projectPlugins);
}
return plugins;
}
resolveChainableWebpackConfig() {
const chainableConfig = new Config();
// apply chains
this.webpackChainFns.forEach((fn) => fn(chainableConfig));
return chainableConfig;
}
resolveWebpackConfig(chainableConfig = this.resolveChainableWebpackConfig()) {
if (!this.initialized) {
throw new Error('Service must call init() before calling resolveWebpackConfig().');
}
// get raw config
let config = chainableConfig.toConfig();
const original = config;
// apply raw config fns
this.webpackRawConfigFns.forEach((fn) => {
if (typeof fn === 'function') {
// function with optional return value
const res = fn(config);
if (res) {
config = merge(config, res);
}
} else if (fn) {
// merge literal values
config = merge(config, fn);
}
});
// #2206 If config is merged by merge-webpack, it discards the __ruleNames
// information injected by webpack-chain. Restore the info so that
// vue inspect works properly.
if (config !== original) {
cloneRuleNames(config.module && config.module.rules, original.module && original.module.rules);
}
return config;
}
};
function cloneRuleNames(to, from) {
if (!to || !from) {
return;
}
from.forEach((r, i) => {
if (to[i]) {
Object.defineProperty(to[i], '__ruleNames', {
value: r.__ruleNames,
});
cloneRuleNames(to[i].oneOf, r.oneOf);
}
});
}
function ensureSlash(config, key) {
let val = config[key];
if (typeof val === 'string') {
if (!/^https?:/.test(val)) {
val = val.replace(/^([^/.])/, '/$1');
}
config[key] = val.replace(/([^/])$/, '$1/');
}
}
function removeSlash(config, key) {
if (typeof config[key] === 'string') {
config[key] = config[key].replace(/\/$/g, '');
}
}
================================================
FILE: packages/nodeppt-serve/commands/build.js
================================================
/**
* @file build 主要内容
*/
const {info} = require('nodeppt-shared-utils');
const path = require('path');
module.exports = (api, options) => {
api.registerCommand('build', {}, async function serve(args) {
info('Build ...');
const context = process.cwd();
const {dest} = args;
process.env.NODE_ENV = 'production';
const webpack = require('webpack');
// resolve webpack config
const webpackConfig = api.resolveWebpackConfig();
webpackConfig.mode = 'production';
if (!args.map) {
delete webpackConfig.devtool; // = null;
}
webpackConfig.output.publicPath = './';
if (dest) {
const targetDir = path.resolve(context, dest || options.outputDir);
webpackConfig.output.path = targetDir;
}
return new Promise((resolve, reject) => {
webpack(webpackConfig, err => {
if (err) {
return reject(err);
}
resolve();
});
});
});
};
module.exports.defaultModes = {
build: 'production'
};
================================================
FILE: packages/nodeppt-serve/commands/serve.js
================================================
/**
* 部分代码来自 vue cli
* @file serve 主要内容
*/
const {info, prepareUrls, getLatestVersion, newVersionLog} = require('nodeppt-shared-utils');
const semver = require('semver');
let newVersion = 0;
const defaults = {
host: '0.0.0.0',
port: 8080,
https: false
};
module.exports = (api, options) => {
api.registerCommand(
'serve',
{
description: 'nodeppt dev server',
usage: 'nodeppt serve [options] [entry]',
options: {
'--host': `specify host (default: ${defaults.host})`,
'--port': `specify port (default: ${defaults.port})`,
'--https': `use https (default: ${defaults.https})`,
'--public': 'specify the public network URL for the HMR client'
}
},
async function serve(args) {
info('Starting development server...');
const currentVersion = args.version;
// 获取版本更新
getLatestVersion()
.then(latest => {
if (semver.lt(currentVersion, latest)) {
newVersion = latest;
}
})
.catch(e => {});
const url = require('url');
const path = require('path');
const chalk = require('chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const portfinder = require('portfinder');
// resolve webpack config
const webpackConfig = api.resolveWebpackConfig();
// in webpck config
const projectDevServerOptions = Object.assign(webpackConfig.devServer || {}, options.devServer);
// entry arg
const entry = args._[0];
if (entry) {
webpackConfig.entry = {
app: api.resolve(entry)
};
}
// resolve server options
const useHttps = args.https || projectDevServerOptions.https || defaults.https;
const protocol = useHttps ? 'https' : 'http';
const host = args.host || process.env.HOST || projectDevServerOptions.host || defaults.host;
portfinder.basePort = args.port || process.env.PORT || projectDevServerOptions.port || defaults.port;
const port = await portfinder.getPortPromise();
const rawPublicUrl = args.public || projectDevServerOptions.public;
const publicUrl = rawPublicUrl
? /^[a-zA-Z]+:\/\//.test(rawPublicUrl)
? rawPublicUrl
: `${protocol}://${rawPublicUrl}`
: null;
const urls = prepareUrls(protocol, host, port, options.baseUrl);
// inject dev & hot-reload middleware entries
const sockjsUrl = publicUrl
? `?${publicUrl}/sockjs-node`
: `?${url.format({
protocol,
port,
hostname: urls.lanUrlForConfig || 'localhost',
pathname: '/sockjs-node'
})}`;
const devClients = [
// dev server client
require.resolve('../template/reload.js'),
require.resolve('webpack-dev-server/client') + sockjsUrl
// hmr client
];
// inject dev/hot client
addDevClientToEntry(webpackConfig, devClients);
// create compiler
const compiler = webpack(webpackConfig);
// create server
const server = new WebpackDevServer(
compiler,
Object.assign(
{
clientLogLevel: 'none',
historyApiFallback: {
disableDotRule: true,
rewrites: [{from: /./, to: path.posix.join(options.baseUrl, 'index.html')}]
},
contentBase: api.resolve('public'),
watchContentBase: true,
hot: true,
quiet: true,
compress: false,
publicPath: options.baseUrl,
overlay: false
},
projectDevServerOptions,
{
https: useHttps,
before(app, server) {
// allow other plugins to register middlewares, e.g. PWA
api.service.devServerConfigFns.forEach(fn => fn(app, server));
// apply in project middlewares
projectDevServerOptions.before && projectDevServerOptions.before(app, server);
}
}
)
);
['SIGINT', 'SIGTERM'].forEach(signal => {
process.on(signal, () => {
server.close(() => {
if (newVersion) {
newVersionLog(currentVersion, newVersion);
}
process.exit(0);
});
});
});
return new Promise((resolve, reject) => {
// log instructions & open browser on first compilation complete
let isFirstCompile = true;
compiler.hooks.done.tap('nodeppt-serve', stats => {
if (stats.hasErrors()) {
return;
}
console.log();
console.log(' NodePPT running at:');
const networkUrl = publicUrl ? publicUrl.replace(/([^/])$/, '$1/') : urls.lanUrlForTerminal;
console.log(` - Url: ${chalk.cyan(networkUrl)}`);
console.log(` - Speaker Mode: ${chalk.cyan(networkUrl + '?mode=speaker')}`);
console.log();
if (isFirstCompile) {
isFirstCompile = false;
// resolve returned Promise
// so other commands can do api.service.run('serve').then(...)
resolve({
server,
url: urls.localUrlForBrowser
});
} else {
}
});
server.listen(port, host, err => {
if (err) {
reject(err);
}
});
});
}
);
};
function addDevClientToEntry(config, devClient) {
const {entry} = config;
if (typeof entry === 'object' && !Array.isArray(entry)) {
Object.keys(entry).forEach(key => {
entry[key] = devClient.concat(entry[key]);
});
} else if (typeof entry === 'function') {
config.entry = entry(devClient);
} else {
config.entry = devClient.concat(entry);
}
}
module.exports.defaultModes = {
serve: 'development'
};
================================================
FILE: packages/nodeppt-serve/config/app.js
================================================
/**
* @file app
*/
const fs = require('fs');
const path = require('path');
// ensure the filename passed to html-webpack-plugin is a relative path
// because it cannot correctly handle absolute paths
function ensureRelative(outputDir, p) {
if (path.isAbsolute(p)) {
return path.relative(outputDir, p);
} else {
return p;
}
}
module.exports = (api, options) => {
api.chainWebpack(webpackConfig => {
const isProd = process.env.NODE_ENV === 'production';
const outputDir = api.resolve(options.outputDir);
// code splitting
if (isProd) {
webpackConfig.optimization.splitChunks({
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
});
}
// HTML plugin
// #1669 html-webpack-plugin's default sort uses toposort which cannot
// handle cyclic deps in certain cases. Monkey patch it to handle the case
// before we can upgrade to its 4.0 version (incompatible with preload atm)
const chunkSorters = require('html-webpack-plugin/lib/chunksorter');
const depSort = chunkSorters.dependency;
chunkSorters.auto = chunkSorters.dependency = (chunks, ...args) => {
try {
return depSort(chunks, ...args);
} catch (e) {
// fallback to a manual sort if that happens...
return chunks.sort((a, b) => {
// make sure user entry is loaded last so user CSS can override
// vendor CSS
if (a.id === 'app') {
return 1;
} else if (b.id === 'app') {
return -1;
} else if (a.entry !== b.entry) {
return b.entry ? -1 : 1;
}
return 0;
});
}
};
const htmlOptions = {
templateParameters: (compilation, assets, pluginOptions) => {
// enhance html-webpack-plugin's built in template params
let stats;
return Object.assign({
// make stats lazy as it is expensive
get webpack() {
return stats || (stats = compilation.getStats().toJson());
},
compilation: compilation,
webpackConfig: compilation.options,
htmlWebpackPlugin: {
files: assets,
options: pluginOptions
}
});
}
};
if (isProd) {
Object.assign(htmlOptions, {
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
collapseBooleanAttributes: true,
removeScriptTypeAttributes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
}
});
// keep chunk ids stable so async chunks have consistent hash (#1916)
webpackConfig.plugin('named-chunks').use(require('webpack/lib/NamedChunksPlugin'), [
chunk => {
if (chunk.name) {
return chunk.name;
}
return (
'chunk-' +
Array.from(chunk.modulesIterable, m => {
return m.id;
}).join('_')
);
}
]);
}
// resolve HTML file(s)
const HTMLPlugin = require('html-webpack-plugin');
const multiPageConfig = options.pages;
const htmlPath = api.getEntry();
const publicCopyIgnore = ['index.html', '.DS_Store'];
if (!multiPageConfig) {
// default, single page setup.
htmlOptions.template = htmlPath;
if (isProd) {
htmlOptions.filename = api.getEntryName() + '.html';
}
webpackConfig.plugin('html').use(HTMLPlugin, [htmlOptions]);
} else {
// multi-page setup
webpackConfig.entryPoints.clear();
const pages = Object.keys(multiPageConfig);
const normalizePageConfig = c => (typeof c === 'string' ? {entry: c} : c);
pages.forEach(name => {
const {
title,
entry,
template = `public/${name}.html`,
filename = `${name}.html`,
chunks
} = normalizePageConfig(multiPageConfig[name]);
// inject entry
webpackConfig.entry(name).add(api.resolve(entry));
// resolve page index template
const hasDedicatedTemplate = fs.existsSync(api.resolve(template));
if (hasDedicatedTemplate) {
publicCopyIgnore.push(template);
}
const templatePath = hasDedicatedTemplate ? template : htmlPath;
// inject html plugin for the page
const pageHtmlOptions = Object.assign({}, htmlOptions, {
chunks: chunks || ['chunk-vendors', 'chunk-common', name],
template: templatePath,
filename: ensureRelative(outputDir, filename),
title
});
webpackConfig.plugin(`html-${name}`).use(HTMLPlugin, [pageHtmlOptions]);
});
}
// copy static assets in public/
const publicDir = api.resolve('./public');
if (fs.existsSync(publicDir)) {
webpackConfig.plugin('copy').use(require('copy-webpack-plugin'), [
[
{
from: publicDir,
to: outputDir,
ignore: publicCopyIgnore
}
]
]);
}
});
};
================================================
FILE: packages/nodeppt-serve/config/base.js
================================================
/**
* @file base
*/
// const path = require('path');
const webpack = require('webpack');
const {transformer, formatter} = require('nodeppt-shared-utils');
module.exports = (api, options) => {
const {version} = api.getNodepptOptions();
api.chainWebpack((webpackConfig) => {
const {getAssetPath, resolveLocal} = require('../lib/utils');
const inlineLimit = 4096;
const genAssetSubPath = (dir) => {
return getAssetPath(options, `${dir}/[name]${options.filenameHashing ? '.[hash:8]' : ''}.[ext]`);
};
const genUrlLoaderOptions = (dir) => {
return {
limit: inlineLimit,
// use explicit fallback to avoid regression in url-loader>=1.1.0
fallback: {
loader: 'file-loader',
options: {
name: genAssetSubPath(dir),
esModule: false,
},
},
};
};
let entryName = 'app';
if (api.service.entry) {
entryName = api.getEntryName();
}
webpackConfig
.mode('development')
.context(api.context)
.entry(entryName)
.add('./main.js')
.end()
.output.path(api.resolve(options.outputDir))
.filename(`[name]${options.filenameHashing ? '.[hash:8]' : ''}.js`)
.publicPath(options.baseUrl);
webpackConfig.resolve
.set('symlinks', false)
.extensions.merge(['.js', '.less'])
.end()
.modules.add('node_modules')
.add(api.resolve('node_modules'))
.add(resolveLocal('node_modules'))
.end();
webpackConfig.resolveLoader.modules
.add('node_modules')
.add(api.resolve('node_modules'))
.add(resolveLocal('node_modules'));
webpackConfig.module
.rule('markdown')
.test(/\.(md|markdown)$/)
.use('html-loader')
.loader(require.resolve('html-loader'))
.end()
.use('nodeppt-parser')
.loader(require.resolve('nodeppt-parser'))
.options({plugins: options.plugins, template: options.baseTemplate})
.end();
webpackConfig.module
.rule('js')
.test(/\.js$/)
.use('babel-loader')
.loader(require.resolve('babel-loader'))
.options({cacheDirectory: true})
.end();
// static assets -----------------------------------------------------------
webpackConfig.module
.rule('images')
.test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
.use('url-loader')
.loader(require.resolve('url-loader'))
.options(genUrlLoaderOptions('img'));
// do not base64-inline SVGs.
// https://github.com/facebookincubator/create-react-app/pull/1180
webpackConfig.module
.rule('svg')
.test(/\.(svg)(\?.*)?$/)
.use('file-loader')
.loader(require.resolve('file-loader'))
.options({
name: genAssetSubPath('img'),
});
webpackConfig.module
.rule('media')
.test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
.use('url-loader')
.loader(require.resolve('url-loader'))
.options(genUrlLoaderOptions('media'));
webpackConfig.module
.rule('fonts')
.test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
.use('url-loader')
.loader(require.resolve('url-loader'))
.options(genUrlLoaderOptions('fonts'));
webpackConfig.plugin('banner').use(
new webpack.BannerPlugin({
banner: `Created by nodeppt ${version} \n - Install: npm install -g nodeppt\n - Github: https://github.com/ksky521/nodeppt`,
})
);
webpackConfig.plugin('case-sensitive-paths').use(require('case-sensitive-paths-webpack-plugin'));
// friendly error plugin displays very confusing errors when webpack
// fails to resolve a loader, so we provide custom handlers to improve it
webpackConfig.plugin('friendly-errors').use(require('friendly-errors-webpack-plugin'), [
{
additionalTransformers: [transformer],
additionalFormatters: [formatter],
},
]);
webpackConfig.plugin('progress').use(require('webpack/lib/ProgressPlugin'));
});
};
================================================
FILE: packages/nodeppt-serve/config/css.js
================================================
/**
* @file css webpack
*/
// const {findExisting} = require('nodeppt-shared-utils');
const getAssetPath = require('../lib/utils').getAssetPath;
module.exports = (api, options) => {
api.chainWebpack(webpackConfig => {
const isProd = process.env.NODE_ENV === 'production';
const {modules = false, extract = isProd, sourceMap = false, loaderOptions = {}} = options.css || {};
const shouldExtract = extract !== false;
const filename = getAssetPath(
options,
`css/[name]${options.filenameHashing || isProd ? '.[contenthash:8]' : ''}.css`
);
const extractOptions = Object.assign(
{
filename,
chunkFilename: filename
},
extract && typeof extract === 'object' ? extract : {}
);
const cssPublicPath = '../'.repeat(
extractOptions.filename.replace(/^\.[\/\\]/, '').split(/[\/\\]/g).length - 1
);
// if building for production but not extracting CSS, we need to minimize
// the embbeded inline CSS as they will not be going through the optimizing
// plugin.
const needInlineMinification = isProd && !shouldExtract;
function createCSSRule(lang, test, loader, options) {
const baseRule = webpackConfig.module.rule(lang).test(test);
// baseRule.exclude.add(/nodeppt/)
applyLoaders(baseRule, modules);
function applyLoaders(rule, modules) {
if (shouldExtract) {
rule.use('extract-css-loader')
.loader(require('mini-css-extract-plugin').loader)
.options({
publicPath: cssPublicPath
});
} else {
rule.use('style-loader').loader(require.resolve('style-loader'));
}
const cssLoaderOptions = Object.assign(
{
sourceMap,
importLoaders: 1 + (needInlineMinification ? 1 : 0) // stylePostLoader injected by vue-loader
},
loaderOptions.css
);
if (modules) {
const {localIdentName = '[name]_[local]_[hash:base64:5]'} = loaderOptions.css || {};
Object.assign(cssLoaderOptions, {
modules,
localIdentName
});
}
rule.use('css-loader')
.loader(require.resolve('css-loader'))
.options(cssLoaderOptions);
if (loader) {
rule.use(loader)
.loader(require.resolve(loader))
.options(Object.assign({sourceMap}, options));
}
}
}
createCSSRule('css', /\.css$/);
createCSSRule('less', /\.less$/, 'less-loader', loaderOptions.less);
// inject CSS extraction plugin
if (shouldExtract) {
webpackConfig.plugin('extract-css').use(require('mini-css-extract-plugin'), [extractOptions]);
}
});
};
================================================
FILE: packages/nodeppt-serve/config/dev.js
================================================
/**
* @file dev webpack
*/
module.exports = (api, options) => {
api.chainWebpack(webpackConfig => {
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
webpackConfig.devtool('cheap-module-eval-source-map').output.publicPath(options.baseUrl);
webpackConfig.plugin('hmr').use(require('webpack/lib/HotModuleReplacementPlugin'));
// https://github.com/webpack/webpack/issues/6642
webpackConfig.output.globalObject('this');
webpackConfig.plugin('no-emit-on-errors').use(require('webpack/lib/NoEmitOnErrorsPlugin'));
if (options.devServer.progress !== false) {
webpackConfig.plugin('progress').use(require('webpack/lib/ProgressPlugin'));
}
}
});
};
================================================
FILE: packages/nodeppt-serve/config/prod.js
================================================
/**
* @file prod webpack
*/
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = (api, options) => {
api.chainWebpack(webpackConfig => {
if (process.env.NODE_ENV === 'production') {
const getAssetPath = require('../lib/utils').getAssetPath;
const filename = getAssetPath(options, `js/[name]${options.filenameHashing ? '.[contenthash:8]' : ''}.js`);
webpackConfig
.mode('production')
.devtool(options.productionSourceMap ? 'source-map' : false)
.output.filename(filename)
.chunkFilename(filename);
// 压缩
webpackConfig.optimization
.minimizer('css')
.use(OptimizeCSSAssetsPlugin, [{cssProcessorOptions: {safe: true}}]);
webpackConfig.optimization.minimizer('js').use(new TerserPlugin());
// keep module.id stable when vendor modules does not change
webpackConfig.plugin('hash-module-ids').use(require('webpack/lib/HashedModuleIdsPlugin'), [
{
hashDigest: 'hex'
}
]);
}
});
};
================================================
FILE: packages/nodeppt-serve/index.js
================================================
/**
* @file hulk -serve
*/
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const globalConfigPlugin = require('./lib/globalConfigPlugin');
const {findExisting} = require('nodeppt-shared-utils');
const Service = require('./Service');
function resolveEntry(entry) {
const context = process.cwd();
entry = entry || findExisting(context, ['main.js', 'index.js']);
if (!entry) {
console.log(chalk.red(`Failed to locate entry file in ${chalk.yellow(context)}.`));
console.log(chalk.red('Valid entry file should be one of: main.js, index.js.'));
process.exit(1);
}
if (!fs.existsSync(path.resolve(context, entry))) {
console.log(chalk.red(`Entry file ${chalk.yellow(entry)} does not exist.`));
process.exit(1);
}
return {
context,
entry
};
}
exports.serve = (e, args) => {
const {context, entry} = resolveEntry(e);
createService(context, entry, {version: args.version || '2.0'}).run('serve', args);
};
exports.build = (e, args) => {
const {context, entry} = resolveEntry(e);
const asLib = args.target && args.target !== 'app';
if (asLib) {
args.entry = entry;
}
createService(context, entry, {version: args.version || '2.0'}, asLib).run('build', args);
};
function createService(context, entry, nodepptOptions, asLib, plugins = []) {
// console.log(plugins);
return new Service(context, entry, {
nodepptOptions,
plugins: [...plugins, globalConfigPlugin(context, entry, asLib)]
});
}
================================================
FILE: packages/nodeppt-serve/lib/globalConfigPlugin.js
================================================
/**
* @file global config plugin
*/
const path = require('path');
const {findExisting} = require('nodeppt-shared-utils');
module.exports = function createConfigPlugin(context, entry, asLib) {
return {
id: 'nodeppt-service-global-config',
apply: (api, options) => {
api.chainWebpack(config => {
// entry arg
const entry = require.resolve('../template/main.js');
config.resolve.alias.set('~entry', path.resolve(context, entry));
let entryName = 'app';
if (api.service.entry) {
entryName = api.getEntryName();
}
// set entry
config
.entry(entryName)
.clear()
.add(entry);
// disable copy plugin if no public dir
if (asLib || !findExisting(context, ['public'])) {
config.plugins.delete('copy');
}
});
}
};
};
================================================
FILE: packages/nodeppt-serve/lib/utils.js
================================================
const fs = require('fs');
const path = require('path');
const {findExisting, chalk} = require('nodeppt-shared-utils');
exports.resolveEntry = entry => {
const context = process.cwd();
entry = entry || findExisting(context, ['README.md', 'readme.md', 'index.md', 'default.md']);
if (!entry) {
console.log(chalk.red(`Failed to locate entry file in ${chalk.yellow(context)}.`));
console.log(chalk.red('Valid entry file should be one of: readme.md, index.md, README.md or default.md.'));
process.exit(1);
}
if (typeof entry === 'string' && !fs.existsSync(path.resolve(context, entry))) {
console.log(chalk.red(`Entry file ${chalk.yellow(entry)} does not exist.`));
process.exit(1);
}
return {
context,
entry
};
};
exports.getAssetPath = (options, filePath) =>
options.assetsDir ? path.posix.join(options.assetsDir, filePath) : filePath;
exports.resolveLocal = function resolveLocal(...args) {
return path.join(__dirname, '../../', ...args);
};
================================================
FILE: packages/nodeppt-serve/options.js
================================================
/**
* @file 默认配置
*/
module.exports = () => ({
// project deployment base
baseUrl: '/',
// where to output built files
outputDir: 'dist',
// where to put static assets (js/css/img/font/...)
assetsDir: '',
// filename for index.html (relative to outputDir)
indexPath: 'index.html',
// 插件,包括 markdown 和 posthtml
plugins: [],
// chainWebpack: [],
// whether filename will contain hash part
filenameHashing: false,
// boolean, use full build?
runtimeCompiler: false,
// deps to transpile
transpileDependencies: [
/* string or regex */
],
// sourceMap for production build?
productionSourceMap: true,
// use thread-loader for babel & TS in production build
// enabled by default if the machine has more than 1 cores
parallel: () => {
try {
return require('os').cpus().length > 1;
} catch (e) {
return false;
}
},
// multi-page config
pages: undefined,
//