Repository: ksky521/nodeppt Branch: master Commit: 44f4babb6586 Files: 159 Total size: 434.6 KB Directory structure: gitextract_k4sj6t2_/ ├── .editorconfig ├── .github/ │ └── ISSUE_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── MIT-LICENSE.txt ├── README.md ├── gh-page.sh ├── lerna.json ├── package.json ├── packages/ │ ├── nodeppt/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── __tests__/ │ │ │ └── demo.md │ │ ├── bin/ │ │ │ └── nodeppt │ │ ├── commands/ │ │ │ └── new.js │ │ ├── lib/ │ │ │ ├── ask.js │ │ │ └── generate.js │ │ └── package.json │ ├── nodeppt-js/ │ │ ├── .npmignore │ │ ├── assets/ │ │ │ └── less/ │ │ │ ├── _base.less │ │ │ ├── _color.less │ │ │ ├── _typography.less │ │ │ ├── _vars.less │ │ │ ├── full.less │ │ │ ├── index.less │ │ │ ├── modules/ │ │ │ │ ├── _animation.less │ │ │ │ ├── _avatars.less │ │ │ │ ├── _badges.less │ │ │ │ ├── _browser.less │ │ │ │ ├── _build.less │ │ │ │ ├── _button.less │ │ │ │ ├── _cards.less │ │ │ │ ├── _flexblock-activity.less │ │ │ │ ├── _flexblock-clients.less │ │ │ │ ├── _flexblock-features.less │ │ │ │ ├── _flexblock-gallery.less │ │ │ │ ├── _flexblock-metrics.less │ │ │ │ ├── _flexblock-plans.less │ │ │ │ ├── _flexblock-reasons.less │ │ │ │ ├── _flexblock-specs.less │ │ │ │ ├── _flexblock-steps.less │ │ │ │ ├── _flexblock.less │ │ │ │ ├── _form.less │ │ │ │ ├── _grid.less │ │ │ │ ├── _header-footer.less │ │ │ │ ├── _logo.less │ │ │ │ ├── _longform.less │ │ │ │ ├── _media.less │ │ │ │ ├── _navigation.less │ │ │ │ ├── _print.less │ │ │ │ ├── _promos.less │ │ │ │ ├── _quotes.less │ │ │ │ ├── _slides-bg.less │ │ │ │ ├── _slides-navigation.less │ │ │ │ ├── _slides.less │ │ │ │ ├── _speaker-note.less │ │ │ │ ├── _tables.less │ │ │ │ ├── _toc.less │ │ │ │ ├── _with-note.less │ │ │ │ ├── _work.less │ │ │ │ └── _zoom.less │ │ │ └── utils/ │ │ │ ├── _animations.less │ │ │ ├── _bugs.less │ │ │ ├── _clear.less │ │ │ └── _reset.less │ │ ├── index.js │ │ ├── package.json │ │ └── plugins/ │ │ ├── echarts.js │ │ ├── keyboard.js │ │ ├── mermaid.js │ │ ├── speaker-mode.js │ │ └── speaker-note.js │ ├── nodeppt-parser/ │ │ ├── .npmignore │ │ ├── __tests__/ │ │ │ ├── classes.md │ │ │ ├── demo.md │ │ │ └── public/ │ │ │ └── demo.js │ │ ├── defaults.js │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── get-markdown-parser.js │ │ │ ├── get-parser.js │ │ │ ├── markdown/ │ │ │ │ ├── attrs/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── patterns.js │ │ │ │ │ └── utils.js │ │ │ │ ├── cite.js │ │ │ │ ├── container.js │ │ │ │ ├── containers/ │ │ │ │ │ ├── blink.js │ │ │ │ │ ├── card.js │ │ │ │ │ ├── column.js │ │ │ │ │ ├── cta.js │ │ │ │ │ ├── div.js │ │ │ │ │ ├── features.js │ │ │ │ │ ├── flexblock.js │ │ │ │ │ ├── flexbox.js │ │ │ │ │ ├── gallery.js │ │ │ │ │ ├── shadow.js │ │ │ │ │ ├── specs.js │ │ │ │ │ └── steps.js │ │ │ │ ├── echarts.js │ │ │ │ ├── fa.js │ │ │ │ ├── img.js │ │ │ │ ├── jsx.js │ │ │ │ ├── link.js │ │ │ │ ├── mermaid.js │ │ │ │ ├── plus-list.js │ │ │ │ ├── prism.js │ │ │ │ ├── regexp/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── utils.js │ │ │ │ └── span.js │ │ │ ├── tags/ │ │ │ │ ├── attrs.js │ │ │ │ ├── header-footer.js │ │ │ │ ├── note.js │ │ │ │ ├── slide.js │ │ │ │ └── utils.js │ │ │ ├── utils.js │ │ │ └── yaml-parser.js │ │ ├── package.json │ │ └── template/ │ │ └── index.ejs │ ├── nodeppt-serve/ │ │ ├── .npmignore │ │ ├── PluginAPI.js │ │ ├── Service.js │ │ ├── commands/ │ │ │ ├── build.js │ │ │ └── serve.js │ │ ├── config/ │ │ │ ├── app.js │ │ │ ├── base.js │ │ │ ├── css.js │ │ │ ├── dev.js │ │ │ └── prod.js │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── globalConfigPlugin.js │ │ │ └── utils.js │ │ ├── options.js │ │ ├── package.json │ │ └── template/ │ │ ├── main.js │ │ └── reload.js │ └── nodeppt-shared-utils/ │ ├── .npmignore │ ├── index.js │ ├── lib/ │ │ ├── download-repo.js │ │ ├── eval.js │ │ ├── find-existing.js │ │ ├── get-debug.js │ │ ├── get-latest-version.js │ │ ├── git-user.js │ │ ├── logger.js │ │ ├── new-version-log.js │ │ ├── path.js │ │ ├── plugin.js │ │ ├── prepare-urls.js │ │ ├── spinner.js │ │ └── webpack-error.js │ └── package.json └── site/ ├── animation.md ├── background.md ├── classes.md ├── component.md ├── echarts.md ├── index.md ├── layout.md ├── media.md ├── mermaid.md └── public/ └── background.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [{.travis.yml,ci.yml}] indent_style = space indent_size = 2 [*.md] trim_trailing_whitespace = false ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ | Executable | Version | | ---: | :--- | | `node --version` | VERSION | | `npm --version` | VERSION | | `nodeppt -v` | VERSION | | OS | Version | | --- | --- | | NAME | VERSION | ================================================ FILE: .gitignore ================================================ yarn.lock /node_modules /.sass-cache /src/index.html /test /3th /.idea /.vscode .DS_Store /npm-debug.log dist publish output yarn-lock.json package-lock.json .DS_Store ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env ### WebStorm ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: .idea/**/workspace.xml .idea/**/tasks.xml .idea/dictionaries # Sensitive or high-churn files: .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.xml .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml # Gradle: .idea/**/gradle.xml .idea/**/libraries # CMake cmake-build-debug/ # Mongo Explorer plugin: .idea/**/mongoSettings.xml ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ### WebStorm Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 # *.iml # modules.xml # .idea/misc.xml # *.ipr # Sonarlint plugin .idea/sonarlint # End of https://www.gitignore.io/api/node,webstorm ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "printWidth": 120, "tabWidth": 4, "bracketSpacing": false, "overrides": [ { "files": ".prettierrc", "options": {"parser": "json"} } ] } ================================================ FILE: MIT-LICENSE.txt ================================================ Copyright 2013 Theowang http://js8.in Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

nodeppt 2.0

> 累死累活干不过做 PPT 的!
> **查看效果:https://nodeppt.js.org** [![Markpress npm badge](https://nodei.co/npm/nodeppt.png)](https://www.npmjs.com/package/nodeppt) **nodeppt 2.0** 基于[webslides](https://github.com/webslides/WebSlides)、webpack、markdown-it、posthtml 重构,[新效果](https://nodeppt.js.org)

Install

```bash npm install -g nodeppt ``` ## TODO * bug fix * 增加多页编辑公共资源,说人话就是 splitChunks

Usage

简化了,就三个命令: - 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 ```

演讲者模式

nodeppt 有演讲者模式,在页面 url 后面增加`?mode=speaker` 既可以打开演讲者模式,双屏同步

Keyboard Shortcuts

- Page: ↑/↓/←/→ Space Home End - Fullscreen: F - Overview: -/+ - Speaker Note: N - Grid Background: Enter

公共资源:public 文件夹

如果项目文件夹下,存在`public`文件夹,可以直接通过 url 访问,参考`webpack dev server`的 `contentBase` 选项。 在`build`的时候,public 文件夹中的文件会完全 copy 到`dist`文件夹中

编写

最佳体验是 chrome 浏览器,本来就是给做演示用的,所以就别考虑非 Chrome 浏览器兼容问题了! 这里说下怎么编写。 ### 基本语法 整个 markdown 文件分为两部分,第一部分是写在最前面的**配置**,然后是使用``隔开的每页幻灯片内容。 ### 配置 nodeppt 的配置是直接写在 md 文件顶部的,采用 yaml 语法,例如下面配置: ```yaml title: nodeppt markdown 演示 speaker: 三水清 url: https://github.com/ksky521/nodeppt js: - https://www.echartsjs.com/asset/theme/shine.js prismTheme: solarizedlight plugins: - echarts - mermaid - katex ``` - title: 演讲主题 - speaker:演讲者 - url:地址 - js:js 文件数组,放到 body 之前 - css:css 文件数组,放到头部 - prismTheme:prism 配色,取值范围 `['dark', 'coy', 'funky', 'okaidia', 'tomorrow', 'solarizedlight', 'twilight']` - plugins:目前支持 [echarts](https://echarts.baidu.com/),[mermaid](https://mermaidjs.github.io/)和 [katex](https://katex.org) 三个插件 #### 插件 目前 nodeppt 支持 [图表 echarts](https://echarts.baidu.com/),[流程图 mermaid](https://mermaidjs.github.io/),[数学符号 KaTeX](https://katex.org) 三个插件。 #### echarts echarts 主题配色可以直接在`yaml`配置的 js 中引入。echarts 采用`fence`语法,如下: ```echarts { xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, yAxis: { type: 'value' }, series: [{ data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line' }] } ``` 详见[site/echarts.md](./site/echarts.md) #### mermaid mermaid 主题配色可以直接在`yaml`配置的 js 中引入。mermaid 采用`fence`语法,如下: ```mermaid sequenceDiagram Alice ->> Bob: Hello Bob, how are you? Bob-->>John: How about you John? Bob--x Alice: I am good thanks! Bob-x John: I am good thanks! Note right of John: Bob thinks a long
long time, so long
that the text does
not fit on a row. Bob-->Alice: Checking with John... Alice->John: Yes... John, how are you? ``` 详见[site/mermaid.md](./site/mermaid.md) #### ketex 参考:[markdown-it-katex](https://www.npmjs.com/package/markdown-it-katex)语法 ### `` 语法 nodeppt 会根据``对整个 markdown 文件进行拆分,拆成单页的幻灯片内容。`` 标签支持下面标签: - class/style 等:正常的 class 类,可以通过这个控制居中(aligncenter),内容位置,背景色等 - image:背景图片,基本语法 `image="img_url"` - video:背景视频,基本语法 `video="video_src1,video_src2"` - :class:wrap 的 class,下面详解 每个 slide 会解析成下面的 html 结构: ```html
// 具体 markdown 渲染的内容
``` 其中`` 的`class`等会被解析到 `
`标签上面,而`:class`则被解析到`div.wrap`上面,例如: ```html ``` output 为: ```html
// 具体 markdown 渲染的内容
``` #### 背景:图片 ``的`image` 会被解析成背景大图,常见的支持方式有: ```md # 这是一个普通的背景图 # 这张背景图会在图片上面蒙一层偏黑色的透明层 # 这张背景图会在图片上面蒙一层偏白色的透明层 # 这张背景图会缓慢动 ``` 详见[site/background.md](./site/background.md)和[在线演示](https://js8.in/nodeppt/background.html) ### 样式 样式太多,具体详见[site/classes.md](./site/classes.md)和[在线演示](https://js8.in/nodeppt/classes.html) ### 布局 nodeppt 这次使用`webslides`的布局,支持丰富的布局,实在太多了,直接看文档[site/layout.md](./site/layout.md)和[在线演示](https://js8.in/nodeppt/layout.html) ### attribute 参考[markdown-it-attrs](https://www.npmjs.com/package/markdown-it-attrs),支持了`attribute`,修改增加多 class 支持等功能。 其中:`..class`会往上一级节点添加 class,支持`{.class1.class2}`这种多 class 的语法。用法举例: ```markdown # header {.style-me.class2} paragraph {data-toggle=modal} ``` Output: ```html

header

paragraph

``` ```markdown Use the css-module green on this paragraph. {.text-intro} ``` Output: ```html

Use the css-module green on this paragraph.

``` ```markdown - list item **bold** {.red} ``` Output: ```html
  • list item bold
``` ```markdown - list item **bold** {.red} ``` Output: ```html
  • list item bold
``` ### image 增强 对于 image ,支持外面包裹一层的写法,具体语法 `!![](图片地址 属性)`,例如: ```markdown !![](https://webslides.tv/static/images/iphone.png .size-50.alignleft) ``` Output: ```html ``` ```markdown !![figure](https://webslides.tv/static/images/setup.png .aligncenter) ``` Output: ```html
``` ### button nodeppt 的 button 是类似`link`语法的,支持蓝色、圆角、空心和 icon 版本的 button: ```markdown [普通按钮](){.button} [圆角普通按钮](){.button.radius} [空心](){.button.ghost} [:fa-github: 前面带 icon](){.button} ``` ### Icon:FontAwesome nodeppt 的 icon 支持 [FontAwesome](https://fontawesome.com/) 语法: - `:fa-xxx:` → `` - `:~fa-xxx:~` → `` - `::fa-xxx::` → 块级``,即不会被`p`包裹 ### span 代码修改自[markdown-it-span](https://github.com/pnewell/markdown-it-span/),支持 `attr`语法,基本用法: ```md :span: :span: {.text-span} ``` ### 动效 nodeppt 一如既往的支持动效,2.0 版本支持动效主要是页面内的动效。 支持动效包括: - fadeIn - zoomIn - rollIn - moveIn - fadeInUp - slow 在需要支持的动效父节点添加`.build`或者在具体的某个元素上添加`.tobuild+动效 class`即可。 按照惯例,nodeppt 还支持`animate.css`的动效哦~ 详细查看文件:[site/animation.md](./site/animation.md)和[在线演示](https://js8.in/nodeppt/animation.html) ### 使用强大的`:::`完成复杂布局 `:::`语法是扩展了 [markdown-it-container](https://www.npmjs.com/package/markdown-it-container) 语法,默认是任意 tag,例如 ```markdown :::div {.content-left} ## title ::: ``` Output: ```html

title

``` 还支持,`tag` 嵌套,除此之外,支持的组件包括: - card:卡片,一边是图片,一边是内容 - column:column 多栏布局 - shadowbox:带阴影的盒子 - steps:步骤组件 - cta: - gallery:图片 - flexblock:flex block 布局,支持多个子类型 - note: 演讲注释 基本语法是: ```markdown :::TYPE {.attrs} ## 第一部分 使用 hr 标签隔开 --- ## 第二部分 这里的内容也是哦 ::: ``` 详细可以看 [component](./site/component.md) 部分的 markdown 文件和[在线演示](https://js8.in/nodeppt/component.html)

打印?导出 pdf?

chrome 浏览器,直接在第一页 `command+P/ctrl+P` 即可

高级玩法

如果上面 ### `nodeppt.config.js` 在 nodeppt 执行路径下创建`nodeppt.config.js`文件,可以配置跟`webpack`相关的选项,另外可以支持自研 nodeppt 插件。 默认内置的`config.js`内容如下: ```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: true, // 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, // <% } %> ` }; ================================================ FILE: packages/nodeppt-parser/index.js ================================================ const path = require('path'); const fs = require('fs-extra'); const defaultDeep = require('lodash.defaultsdeep'); const ejs = require('ejs'); const loaderUtils = require('loader-utils'); const getParser = require('./lib/get-parser'); // parsers const yamlParser = require('./lib/yaml-parser'); const defaults = require('./defaults'); // 模板 const defaultTemplate = fs.readFileSync(path.join(__dirname, './template/index.ejs')).toString(); // 这里不要用箭头函数,this 指向问题! /* eslint-disable space-before-function-paren */ module.exports = function (content) { /* eslint-enable space-before-function-paren */ const {plugins = []} = loaderUtils.getOptions(this); const resourcePath = this.resourcePath; const parser = getParser(plugins); const settings = content.split(//i)[0]; const yamlSettings = yamlParser(settings); // 支持baseTemplate,传入ejs模板 let template = defaultTemplate; if (yamlSettings.baseTemplate && typeof yamlSettings.baseTemplate === 'string') { const baseTemplate = path.resolve(path.dirname(resourcePath), yamlSettings.baseTemplate); if (fs.existsSync(baseTemplate)) { template = fs.readFileSync(baseTemplate).toString(); } } // 首部 yaml 设置部分 const globalSettings = defaultDeep(yamlSettings, defaults); content = parser(content); return ejs.render(template, {...globalSettings, content}); }; ================================================ FILE: packages/nodeppt-parser/lib/get-markdown-parser.js ================================================ const mdIt = require('markdown-it')(); // const prism = require('markdown-it-prism'); const prism = require('./markdown/prism'); mdIt.use(prism, { defaultLanguageForUnknown: 'textile' }); mdIt.use(require('markdown-it-sup')); mdIt.use(require('markdown-it-br')); mdIt.use(require('@ksky521/markdown-it-katex')); mdIt.use(require('./markdown/jsx')); mdIt.use(require('./markdown/echarts')); // mermaid 流程图 mdIt.use(require('./markdown/mermaid')); mdIt.use(require('./markdown/fa')); // mdIt.use(require('./markdown/plus-list')); mdIt.use(require('./markdown/link')); // 注意attrs顺序 mdIt.use(require('./markdown/attrs')); mdIt.use(require('./markdown/img')); mdIt.use(require('./markdown/cite')); mdIt.use(require('./markdown/span')); mdIt.use(require('./markdown/container'), 'column', require('./markdown/containers/column')); mdIt.use(require('./markdown/container'), 'shadowbox', require('./markdown/containers/shadow')); mdIt.use(require('./markdown/container'), 'steps', require('./markdown/containers/steps')); mdIt.use(require('./markdown/container'), 'card', require('./markdown/containers/card')); // 不用了 // mdIt.use(require('./markdown/container'), 'flexbox', require('./markdown/containers/flexbox')('flexbox')); mdIt.use(require('./markdown/container'), 'flexblock', require('./markdown/containers/flexblock')); // 不用了 // mdIt.use(require('./markdown/container'), 'blink', require('./markdown/containers/blink')); // mdIt.use(require('./markdown/container'), 'features', require('./markdown/containers/features')); // 不用了 // mdIt.use(require('./markdown/container'), 'specs', require('./markdown/containers/specs')); mdIt.use(require('./markdown/container'), 'cta', require('./markdown/containers/cta')); mdIt.use(require('./markdown/container'), 'gallery', require('./markdown/containers/gallery')); mdIt.use(require('./markdown/container'), 'div', require('./markdown/containers/div')); function getMdParser(plugins) { plugins.forEach(plugin => { mdIt.use(plugin); }); return mdIt.render.bind(mdIt); } module.exports = getMdParser; ================================================ FILE: packages/nodeppt-parser/lib/get-parser.js ================================================ const posthtml = require('posthtml'); const getMdParser = require('./get-markdown-parser'); // 内置 posthtml 插件 const buildInPlugins = [ './tags/slide.js', './tags/note.js', './tags/header-footer.js', // attrs放到最后 './tags/attrs.js', ]; const buildInPosthtmlPlugins = buildInPlugins.map((file) => { return require(file); }); module.exports = (plugins) => { const markdownPlugins = []; const posthtmlPlugins = []; plugins.forEach((p) => { if (p && typeof p.apply === 'function') { if (p.id.indexOf('markdown') === 0) { markdownPlugins.push(p.apply); } else if (p.id.indexOf('posthtml') === 0) { posthtmlPlugins.push(p.apply); } } }); const mdRender = getMdParser(markdownPlugins); return (str) => { const slideTag = str.match(/\n/gim) || []; const contents = str.split(/\n/im); contents.shift(); return contents .map((c, i) => { // 生成 attr const html = ` ${slideTag[i]}
${mdRender(c)}
`; // 生成 content ast return posthtml(buildInPosthtmlPlugins.concat(posthtmlPlugins)).process(html, {sync: true}).html; }) .join('\n'); }; }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/attrs/index.js ================================================ 'use strict'; // from https://github.com/arve0/markdown-it-attrs // 增加多个 class 支持 const patternsConfig = require('./patterns.js'); const utils = require('./utils'); const defaultOptions = { leftDelimiter: '{', rightDelimiter: '}' }; module.exports = function attributes(md, options) { if (!options) { options = defaultOptions; } utils.setOptions(options); const patterns = patternsConfig(options); function curlyAttrs(state) { let tokens = state.tokens; for (let i = 0; i < tokens.length; i++) { for (let p = 0; p < patterns.length; p++) { let pattern = patterns[p]; let j = null; // position of child with offset 0 let match = pattern.tests.every(t => { let res = test(tokens, i, t); if (res.j !== null) { j = res.j; } return res.match; }); if (match) { pattern.transform(tokens, i, j); if (pattern.name === 'inline attributes' || pattern.name === 'inline nesting 0') { // retry, may be several inline attributes p--; } } } } } md.core.ruler.before('linkify', 'curly_attributes', curlyAttrs); }; /** * Test if t matches token stream. * * @param {array} tokens * @param {number} i * @param {object} t Test to match. * @return {object} { match: true|false, j: null|number } */ function test(tokens, i, t) { let res = { match: false, j: null // position of child }; let ii = t.shift !== undefined ? i + t.shift : t.position; let token = get(tokens, ii); // supports negative ii if (token === undefined) { return res; } for (let key in t) { if (key === 'shift' || key === 'position') { continue; } if (token[key] === undefined) { return res; } if (key === 'children' && isArrayOfObjects(t.children)) { if (token.children.length === 0) { return res; } let match; let childTests = t.children; let children = token.children; if (childTests.every(tt => tt.position !== undefined)) { // positions instead of shifts, do not loop all children match = childTests.every(tt => test(children, tt.position, tt).match); if (match) { // we may need position of child in transform let j = last(childTests).position; res.j = j >= 0 ? j : children.length + j; } } else { for (let j = 0; j < children.length; j++) { match = childTests.every(tt => test(children, j, tt).match); if (match) { res.j = j; // all tests true, continue with next key of pattern t break; } } } if (match === false) { return res; } continue; } switch (typeof t[key]) { case 'boolean': case 'number': case 'string': if (token[key] !== t[key]) { return res; } break; case 'function': if (!t[key](token[key])) { return res; } break; case 'object': if (isArrayOfFunctions(t[key])) { let r = t[key].every(tt => tt(token[key])); if (r === false) { return res; } break; } // fall through for objects !== arrays of functions default: throw new Error( `Unknown type of pattern test (key: ${key}). Test should be of type boolean, number, string, function or array of functions.` ); } } // no tests returned false -> all tests returns true res.match = true; return res; } function isArrayOfObjects(arr) { return Array.isArray(arr) && arr.length && arr.every(i => typeof i === 'object'); } function isArrayOfFunctions(arr) { return Array.isArray(arr) && arr.length && arr.every(i => typeof i === 'function'); } /** * Get n item of array. Supports negative n, where -1 is last * element in array. * @param {array} arr * @param {number} n */ function get(arr, n) { return n >= 0 ? arr[n] : arr[arr.length + n]; } // get last element of array, safe - returns {} if not found function last(arr) { return arr.slice(-1)[0] || {}; } ================================================ FILE: packages/nodeppt-parser/lib/markdown/attrs/patterns.js ================================================ 'use strict'; /** * If a pattern matches the token stream, * then run transform. */ const utils = require('./utils.js'); module.exports = options => { const __hr = new RegExp( '^ {0,3}[-*_]{3,} ?' + utils.escapeRegExp(options.leftDelimiter) + '[^' + utils.escapeRegExp(options.rightDelimiter) + ']' ); return [ { /** * ```python {.cls} * for i in range(10): * print(i) * ``` */ name: 'fenced code blocks', tests: [ { shift: 0, block: true, info: utils.hasDelimiters('end', options) } ], transform: (tokens, i) => { let token = tokens[i]; let start = token.info.lastIndexOf(options.leftDelimiter); let attrs = utils.getAttrs(token.info, start, options); utils.addAttrs(attrs, token); token.info = utils.removeDelimiter(token.info, options); } }, { /** * bla [:fa-file: abc](http://){.d} * * differs from 'inline attributes' as it does * not have a closing tag (nesting: -1) */ name: 'fa_inline nesting', tests: [ { shift: 0, type: 'inline', children: [ { shift: -1, type: 'link_open' }, { shift: 0, type: 'fa_inline' }, { shift: 1, type: 'text' }, { shift: 2, type: 'link_close' }, { shift: 3, type: str => str === 'text' || str === 'jsx_inline', content: utils.hasDelimiters('only', options) } ] } ], transform: (tokens, i, j) => { const children = tokens[i].children; for (let m = 1; m < children.length; m++) { let child = children[m]; if ( child && children[m - 1] && children[m - 1].type === 'link_open' && child.type === 'fa_inline' && children[m + 1] && children[m + 1].type === 'text' && children[m + 2] && children[m + 2].type === 'link_close' && children[m + 3] ) { let jsx = children[m + 3]; if (utils.hasDelimiters('start', options)(jsx.content)) { // 说明是有效的 jsx let token = children.splice(m + 3, 1)[0]; let attrToken = tokens[i].children[m - 1]; let attrs = utils.getAttrs(token.content, 0, options); utils.addAttrs(attrs, attrToken); m--; } } } } }, { /** * bla [xxxx](http://){.d} * * differs from 'inline attributes' as it does * not have a closing tag (nesting: -1) */ name: 'inline nesting link', tests: [ { shift: 0, type: 'inline', children: [ { shift: 0, type: 'link_open' }, { shift: 1, type: 'text' }, { shift: 2, type: 'link_close' }, { shift: 3, type: str => str === 'text' || str === 'jsx_inline', content: utils.hasDelimiters('only', options) } ] } ], transform: (tokens, i, j) => { const children = tokens[i].children; // console.log(children[j]); for (let m = 1; m < children.length; m++) { let child = children[m]; if ( child && children[m - 1] && children[m - 1].type === 'link_open' && child.type === 'text' && children[m + 1] && children[m + 1].type === 'link_close' && children[m + 2] ) { let jsx = children[m + 2]; if (utils.hasDelimiters('only', options)(jsx.content)) { // 说明是有效的 jsx let token = children.splice(m + 2, 1)[0]; let attrToken = tokens[i].children[m - 1]; let attrs = utils.getAttrs(token.content, 0, options); utils.addAttrs(attrs, attrToken); m--; } } } } }, { /** * bla `click()`{.c} ![](img.png){.d} * * differs from 'inline attributes' as it does * not have a closing tag (nesting: -1) */ name: 'inline nesting 0', tests: [ { shift: 0, type: 'inline', children: [ { shift: -1, type: str => str === 'image' || str === 'code_inline' }, { shift: 0, type: str => str === 'text' || str === 'jsx_inline', content: utils.hasDelimiters('start', options) } ] } ], transform: (tokens, i, j) => { let token = tokens[i].children[j]; let endChar = token.content.indexOf(options.rightDelimiter); let attrToken = tokens[i].children[j - 1]; let attrs = utils.getAttrs(token.content, 0, options); utils.addAttrs(attrs, attrToken); if (token.content.length === endChar + options.rightDelimiter.length) { tokens[i].children.splice(j, 1); } else { token.content = token.content.slice(endChar + options.rightDelimiter.length); } } }, { /** * | h1 | * | -- | * | c1 | * {.c} */ name: 'tables', tests: [ { // let this token be i, such that for-loop continues at // next token after tokens.splice shift: 0, type: 'table_close' }, { shift: 1, type: 'paragraph_open' }, { shift: 2, type: 'inline', content: utils.hasDelimiters('only', options) } ], transform: (tokens, i) => { let token = tokens[i + 2]; let tableOpen = utils.getMatchingOpeningToken(tokens, i); let attrs = utils.getAttrs(token.content, 0, options); // add attributes utils.addAttrs(attrs, tableOpen); // remove

{.c}

tokens.splice(i + 1, 3); } }, { /** * *emphasis*{.with attrs=1} */ name: 'inline attributes', tests: [ { shift: 0, type: 'inline', children: [ { shift: -1, nesting: -1 // closing inline tag, {.a} }, { shift: 0, type: 'text', content: utils.hasDelimiters('start', options) } ] } ], transform: (tokens, i, j) => { let token = tokens[i].children[j]; let content = token.content; let attrs = utils.getAttrs(content, 0, options); let openingToken = utils.getMatchingOpeningToken(tokens[i].children, j - 1); utils.addAttrs(attrs, openingToken); token.content = content.slice(content.indexOf(options.rightDelimiter) + options.rightDelimiter.length); } }, { /** * - item * {.a} */ name: 'list softbreak', tests: [ { shift: -2, type: 'list_item_open' }, { shift: 0, type: 'inline', children: [ { position: -2, type: 'softbreak' }, { position: -1, content: utils.hasDelimiters('only', options) } ] } ], transform: (tokens, i, j) => { let token = tokens[i].children[j]; let content = token.content; let attrs = utils.getAttrs(content, 0, options); let ii = i - 2; while ( tokens[ii - 1] && tokens[ii - 1].type !== 'ordered_list_open' && tokens[ii - 1].type !== 'bullet_list_open' ) { ii--; } utils.addAttrs(attrs, tokens[ii - 1]); tokens[i].children = tokens[i].children.slice(0, -2); } }, { /** * - nested list * - with double \n * {.a} <-- apply to nested ul * * {.b} <-- apply to root
    */ name: 'list double softbreak', tests: [ { // let this token be i = 0 so that we can erase // the

    {.a}

    tokens below shift: 0, type: str => str === 'bullet_list_close' || str === 'ordered_list_close' }, { shift: 1, type: 'paragraph_open' }, { shift: 2, type: 'inline', content: utils.hasDelimiters('only', options), children: arr => arr.length === 1 }, { shift: 3, type: 'paragraph_close' } ], transform: (tokens, i) => { let token = tokens[i + 2]; let content = token.content; let attrs = utils.getAttrs(content, 0, options); let openingToken = utils.getMatchingOpeningToken(tokens, i); utils.addAttrs(attrs, openingToken); tokens.splice(i + 1, 3); } }, { /** * - end of {.list-item} */ name: 'list item end', tests: [ { shift: -2, type: 'list_item_open' }, { shift: 0, type: 'inline', children: [ { position: -1, content: utils.hasDelimiters('end', options) } ] } ], transform: (tokens, i, j) => { let token = tokens[i].children[j]; let content = token.content; let attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), options); utils.addAttrs(attrs, tokens[i - 2]); let trimmed = content.slice(0, content.lastIndexOf(options.leftDelimiter)); token.content = last(trimmed) !== ' ' ? trimmed : trimmed.slice(0, -1); } }, { /** * something with softbreak * {.cls} */ name: '\n{.a} softbreak then curly in start', tests: [ { shift: 0, type: 'inline', children: [ { position: -2, type: 'softbreak' }, { position: -1, type: str => str === 'text' || str === 'jsx_inline', content: utils.hasDelimiters('only', options) } ] } ], transform: (tokens, i, j) => { let token = tokens[i].children[j]; let attrs = utils.getAttrs(token.content, 0, options); // find last closing tag let ii = i + 1; while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) { ii++; } let openingToken = utils.getMatchingOpeningToken(tokens, ii); utils.addAttrs(attrs, openingToken); tokens[i].children = tokens[i].children.slice(0, -2); } }, { /** * horizontal rule --- {#id} */ name: 'horizontal rule', tests: [ { shift: 0, type: 'paragraph_open' }, { shift: 1, type: 'inline', children: arr => arr.length === 1, content: str => str.match(__hr) !== null }, { shift: 2, type: 'paragraph_close' } ], transform: (tokens, i) => { let token = tokens[i]; token.type = 'hr'; token.tag = 'hr'; token.nesting = 0; let content = tokens[i + 1].content; let start = content.lastIndexOf(options.leftDelimiter); token.attrs = utils.getAttrs(content, start, options); token.markup = content; tokens.splice(i + 1, 2); } }, { /** * end of {.block} */ name: 'end of block', tests: [ { shift: 0, type: 'inline', children: [ { position: -1, content: utils.hasDelimiters('end', options), type: t => t !== 'code_inline' } ] } ], transform: (tokens, i, j) => { let token = tokens[i].children[j]; let content = token.content; let attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), options); let ii = i + 1; while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) { ii++; } let openingToken = utils.getMatchingOpeningToken(tokens, ii); utils.addAttrs(attrs, openingToken); let trimmed = content.slice(0, content.lastIndexOf(options.leftDelimiter)); token.content = last(trimmed) !== ' ' ? trimmed : trimmed.slice(0, -1); } } ]; }; // get last element of array or string function last(arr) { return arr.slice(-1)[0]; } ================================================ FILE: packages/nodeppt-parser/lib/markdown/attrs/utils.js ================================================ 'use strict'; exports.findAttrs = (attrArray, name, getValueOnly = true) => { let rs = (attrArray || []).find(([key, value]) => { if (key === name) { return true; } return false; }); if (getValueOnly) { return rs[1]; } return rs; }; /** * parse {.class #id key=val} strings * @param {string} str: string to parse * @param {int} start: where to start parsing (including {) * @returns {2d array}: [['key', 'val'], ['class', 'red']] */ exports.getAttrs = function(str, start, options) { // not tab, line feed, form feed, space, solidus, greater than sign, quotation mark, apostrophe and equals sign const allowedKeyChars = /[^\t\n\f />"'=]/; const pairSeparator = ' '; const keySeparator = '='; const classChar = '.'; const idChar = '#'; const attrs = []; let key = ''; let value = ''; let parsingKey = true; let valueInsideQuotes = false; // read inside {} // start + left delimiter length to avoid beginning { // breaks when } is found or end of string for (let i = start + options.leftDelimiter.length; i < str.length; i++) { if (str.slice(i, i + options.rightDelimiter.length) === options.rightDelimiter) { if (key !== '') { attrs.push([key, value]); } break; } let char_ = str.charAt(i); // switch to reading value if equal sign if (char_ === keySeparator && parsingKey) { parsingKey = false; continue; } // {.class} {..css-module} if (char_ === classChar && key === '') { // console.log(str) if (str.charAt(i + 1) === classChar) { key = 'css-module'; i += 1; } else { key = 'class'; } parsingKey = false; continue; } // {#id} if (char_ === idChar && key === '') { key = 'id'; parsingKey = false; continue; } // {value="inside quotes"} if (char_ === '"' && value === '') { valueInsideQuotes = true; continue; } if (char_ === '"' && valueInsideQuotes) { valueInsideQuotes = false; continue; } // read next key/value pair if (char_ === pairSeparator && !valueInsideQuotes) { if (key === '') { // beginning or ending space: { .red } vs {.red} continue; } attrs.push([key, value]); key = ''; value = ''; parsingKey = true; continue; } // continue if character not allowed if (parsingKey && char_.search(allowedKeyChars) === -1) { continue; } // no other conditions met; append to key/value if (parsingKey) { key += char_; continue; } value += char_; } return attrs; }; /** * add attributes from [['key', 'val']] list * @param {array} attrs: [['key', 'val']] * @param {token} token: which token to add attributes * @returns token */ exports.addAttrs = function(attrs, token) { for (let j = 0, l = attrs.length; j < l; ++j) { let key = attrs[j][0]; // 多个. 支持 if (key === 'class') { token.attrJoin('class', attrs[j][1].split('.').join(' ')); } else if (key === 'css-module') { token.attrJoin('css-module', attrs[j][1].split('.').join(' ')); } else { token.attrPush(attrs[j]); } } return token; }; // 增加获取 attr string 方法 exports.getAttrsString = function(attrs, fn) { const defFN = (key, value) => `${key}="${value}"`; const cb = typeof fn === 'function' ? (key, value) => { const r = fn(key, value); if (r) { return r; } else { return defFN(key, value); } } : (key, value) => `${key}="${value}"`; const str = []; for (let j = 0, l = attrs.length; j < l; ++j) { let key = attrs[j][0]; // 多个. 支持 if (key === 'class' || key === 'css-module') { str.push(cb(key, attrs[j][1].split('.').join(' '))); } else { str.push(cb(key, attrs[j][1])); } } return str.join(' '); }; /** * Does string have properly formatted curly? * * start: '{.a} asdf' * middle: 'a{.b}c' * end: 'asdf {.a}' * only: '{.a}' * * @param {string} where to expect {} curly. start, middle, end or only. * @return {function(string)} Function which testes if string has curly. */ exports.hasDelimiters = function(where, options) { if (!where) { throw new Error('Parameter `where` not passed. Should be "start", "middle", "end" or "only".'); } /** * @param {string} str * @return {boolean} */ return function(str) { // we need minimum three chars, for example {b} let minCurlyLength = options.leftDelimiter.length + 1 + options.rightDelimiter.length; if (!str || typeof str !== 'string' || str.length < minCurlyLength) { return false; } function validCurlyLength(curly) { let isClass = curly.charAt(options.leftDelimiter.length) === '.'; let isId = curly.charAt(options.leftDelimiter.length) === '#'; return isClass || isId ? curly.length >= minCurlyLength + 1 : curly.length >= minCurlyLength; } let start, end, slice, nextChar; let rightDelimiterMinimumShift = minCurlyLength - options.rightDelimiter.length; switch (where) { case 'start': // first char should be {, } found in char 2 or more slice = str.slice(0, options.leftDelimiter.length); start = slice === options.leftDelimiter ? 0 : -1; end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, rightDelimiterMinimumShift); // check if next character is not one of the delimiters nextChar = str.charAt(end + options.rightDelimiter.length); if (nextChar && options.rightDelimiter.indexOf(nextChar) !== -1) { end = -1; } break; case 'end': // last char should be } start = str.lastIndexOf(options.leftDelimiter); end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + rightDelimiterMinimumShift); end = end === str.length - options.rightDelimiter.length ? end : -1; break; case 'only': // '{.a}' slice = str.slice(0, options.leftDelimiter.length); start = slice === options.leftDelimiter ? 0 : -1; slice = str.slice(str.length - options.rightDelimiter.length); end = slice === options.rightDelimiter ? str.length - options.rightDelimiter.length : -1; break; } return ( start !== -1 && end !== -1 && validCurlyLength(str.substring(start, end + options.rightDelimiter.length)) ); }; }; let latestOptions = {}; exports.setOptions = opts => { latestOptions = opts; }; exports.getOptions = () => { return latestOptions; }; /** * Removes last curly from string. */ exports.removeDelimiter = function(str, options) { const start = escapeRegExp(options.leftDelimiter); const end = escapeRegExp(options.rightDelimiter); let curly = new RegExp('[ \\n]?' + start + '[^' + start + end + ']+' + end + '$'); let pos = str.search(curly); return pos !== -1 ? str.slice(0, pos) : str; }; /** * Escapes special characters in string s such that the string * can be used in `new RegExp`. For example "[" becomes "\\[". * * @param {string} s Regex string. * @return {string} Escaped string. */ function escapeRegExp(s) { return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); } exports.escapeRegExp = escapeRegExp; /** * find corresponding opening block */ exports.getMatchingOpeningToken = function(tokens, i) { if (tokens[i].type === 'softbreak') { return false; } // non closing blocks, example img if (tokens[i].nesting === 0) { return tokens[i]; } // inline tokens changes level on same token // that have .nesting +- 1 let level = tokens[i].block ? tokens[i].level : tokens[i].level + 1; // adjust for inline tokens let type = tokens[i].type.replace('_close', '_open'); for (; i >= 0; --i) { if (tokens[i].type === type && tokens[i].level === level) { return tokens[i]; } } }; /** * from https://github.com/markdown-it/markdown-it/blob/master/lib/common/utils.js */ let HTML_ESCAPE_TEST_RE = /[&<>"]/; let HTML_ESCAPE_REPLACE_RE = /[&<>"]/g; let HTML_REPLACEMENTS = { '&': '&', '<': '<', '>': '>', '"': '"' }; function replaceUnsafeChar(ch) { return HTML_REPLACEMENTS[ch]; } exports.escapeHtml = function(str) { if (HTML_ESCAPE_TEST_RE.test(str)) { return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar); } return str; }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/cite.js ================================================ module.exports = function ins_plugin(md) { // https://github.com/markdown-it/markdown-it-mark/blob/master/index.js function tokenize(state, silent) { var i, scanned, token, len, ch, start = state.pos, marker = state.src.charCodeAt(start); if (silent) { return false; } if (marker !== 0x3d /* = */) { return false; } scanned = state.scanDelims(state.pos, true); len = scanned.length; ch = String.fromCharCode(marker); if (len < 2) { return false; } if (len % 2) { token = state.push('text', '', 0); token.content = ch; len--; } for (i = 0; i < len; i += 2) { 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 !== 0x3d /* = */) { continue; } if (startDelim.end === -1) { continue; } endDelim = delimiters[startDelim.end]; token = state.tokens[startDelim.token]; token.type = 'cite_open'; token.tag = 'cite'; token.nesting = 1; token.markup = '=='; token.content = ''; token = state.tokens[endDelim.token]; token.type = 'cite_close'; token.tag = 'cite'; 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 === 'cite_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', 'cite', tokenize); md.inline.ruler2.before('emphasis', 'cite', postProcess); }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/container.js ================================================ // 参考 markdown-it-container module.exports = function container_plugin(md, name, options) { function validateDefault(params) { return params.trim().split(' ', 2)[0] === name; } function renderDefault(tokens, idx, _options, env, slf) { // add a class to the opening tag if (tokens[idx].nesting === 1) { tokens[idx].attrJoin('class', name); } return slf.renderToken(tokens, idx, _options, env, slf); } function emptyFn(t) { return t; } options = options || {}; var min_markers = 3, marker_str = options.marker || ':', marker_char = marker_str.charCodeAt(0), marker_len = marker_str.length, validate = options.validate || validateDefault, handler = options.handler || emptyFn, render = options.render || renderDefault; function container(state, startLine, endLine, silent) { var pos, nextLine, marker_count, markup, params, token, old_parent, old_line_max, auto_closed = false, start = state.bMarks[startLine] + state.tShift[startLine], max = state.eMarks[startLine]; // Check out the first character quickly, // this should filter out most of non-containers // if (marker_char !== state.src.charCodeAt(start)) { return false; } // Check out the rest of the marker string // for (pos = start + 1; pos <= max; pos++) { if (marker_str[(pos - start) % marker_len] !== state.src[pos]) { break; } } marker_count = Math.floor((pos - start) / marker_len); if (marker_count < min_markers) { return false; } pos -= (pos - start) % marker_len; markup = state.src.slice(start, pos); params = state.src.slice(pos, max); if (!validate(params)) { return false; } // Since start is found, we can report success here in validation mode // if (silent) { return true; } // Search for the end of the block // nextLine = startLine; for (;;) { nextLine++; if (nextLine >= endLine) { // unclosed block should be autoclosed by end of document. // also block seems to be autoclosed by end of parent break; } start = state.bMarks[nextLine] + state.tShift[nextLine]; max = state.eMarks[nextLine]; if (start < max && state.sCount[nextLine] < state.blkIndent) { // non-empty line with negative indent should stop the list: // - ``` // test break; } if (marker_char !== state.src.charCodeAt(start)) { continue; } if (state.sCount[nextLine] - state.blkIndent >= 4) { // closing fence should be indented less than 4 spaces continue; } for (pos = start + 1; pos <= max; pos++) { if (marker_str[(pos - start) % marker_len] !== state.src[pos]) { break; } } // closing code fence must be at least as long as the opening one if (Math.floor((pos - start) / marker_len) < marker_count) { continue; } // make sure tail has spaces only pos -= (pos - start) % marker_len; pos = state.skipSpaces(pos); if (pos < max) { continue; } // found! auto_closed = true; break; } old_parent = state.parentType; old_line_max = state.lineMax; state.parentType = 'container'; // this will prevent lazy continuations from ever going past our end marker state.lineMax = nextLine; token = state.push('container_' + name + '_open', 'div', 1); token.markup = markup; token.block = true; token.info = params; token.map = [startLine, nextLine]; // 修正 let len = state.tokens.length - startLine; state.md.block.tokenize(state, startLine + 1, nextLine); token = state.push('container_' + name + '_close', 'div', -1); token.markup = state.src.slice(start, pos); token.block = true; token.info = params; // 这里测试下 handler(state, params, startLine + len - 1, len); state.parentType = old_parent; state.lineMax = old_line_max; state.line = nextLine + (auto_closed ? 1 : 0); return true; } md.block.ruler.before('fence', 'container_' + name, container, { alt: ['paragraph', 'reference', 'blockquote', 'list'] }); md.renderer.rules['container_' + name + '_open'] = render; md.renderer.rules['container_' + name + '_close'] = render; }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/blink.js ================================================ const name = 'blink'; module.exports = { validate(params) { return params.trim().match(new RegExp('^' + name + '\\s*(.*)$')); }, handler(state, opts, start) { function getOpenToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_open', tag, 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_close', tag, -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; let open = false; let done = 0; for (let i = start; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 tokens.splice(i + 1, 0, getOpenToken('li', token.level), getOpenToken('a', token.level + 1)); open = true; i = i + 2; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 tokens.splice(i, 0, getCloseToken('a', token.level + 1), getCloseToken('li', token.level)); open = false; i = i + 2; } else if (open && 'hr' === token.type && done === 0) { // 第一层的 Hr 需要替换 tokens.splice( i, 1, getCloseToken('a', token.level), getCloseToken('li', token.level - 1), getOpenToken('li', token.level - 1), getOpenToken('a', token.level) ); i = i + 3; } else if (open) { if (token.type === 'paragraph_close' || token.type === 'paragraph_open') { tokens.splice(i, 1); i--; continue; } // 加深一层,因为外面多套了一层div token.level = token.level + 2; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } } } return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` flexblock blink border ${attrs[cmIndex][1]}` : ` flexblock blink border`; } else { attrs.push([ 'class', cmIndex >= 0 ? `flexblock blink border ${attrs[cmIndex][1]}` : `flexblock blink border` ]); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
      \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/card.js ================================================ const utils = require('../attrs/utils'); const attrOptions = utils.getOptions(); const name = 'card'; module.exports = { validate(params) { return params.trim().match(new RegExp('^' + name + '(-\\d+)?\\s*(.*)$')); }, handler(state, opts, start) { function getOpenToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_open', tag, 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_close', tag, -1); token.block = true; token.level = 1 + level; return token; } function hasImg(tokens) { if ( tokens.length === 3 && tokens[0].type === 'paragraph_open' && tokens[2].type === 'paragraph_close' && /^\!\[/.test(tokens[1].content) ) { return [ getOpenToken('figure', tokens[0].level - 1), tokens[1], getCloseToken('figure', tokens[2].level - 1) ]; } return false; } opts = opts.split(/card(?:-\d+)?\s+/).splice(1)[0]; let attrs = []; let cardClass = ''; if (utils.hasDelimiters('only', attrOptions)(opts, attrOptions)) { attrs = utils.getAttrs(opts, 0, attrOptions); cardClass = utils.findAttrs(attrs, 'class'); } // tokens const tokens = state.tokens; let hrIdx = 0; for (let i = start + 1; i < tokens.length - 1; i++) { // 第一步,查找hr let token = tokens[i]; if (token.type === 'hr') { hrIdx = i; break; } } // 第二步:拆分 let part1 = tokens.slice(start + 1, hrIdx); let part2 = tokens.slice(hrIdx + 1, tokens.length - 1); // console.log(part1, part2); // 第三步:查找哪part有img let imgs = hasImg(part1); if (imgs) { // 第一部分有图片 tokens.splice(start + 1, 3, ...imgs); if (cardClass.indexOf('quote') === -1) { let level = tokens[start].level; const divToken = getOpenToken('div', level - 1); divToken.attrPush(['class', 'flex-content']); tokens.splice(hrIdx, 1, divToken); tokens.splice(tokens.length - 1, 0, getCloseToken('div', level - 1)); } else { tokens.splice(hrIdx, 1); } // console.log(tokens); } else { imgs = hasImg(part2); if (imgs) { // 第二部分有图片 if (cardClass.indexOf('quote') === -1) { let level = tokens[start].level; const divToken = getOpenToken('div', level - 1); divToken.attrPush(['class', 'flex-content']); tokens.splice(start + 1, 0, divToken); tokens.splice(hrIdx + 1, 4, getCloseToken('div', level - 1), ...imgs); } else { tokens.splice(hrIdx, 3, ...imgs); } } } // console.log(tokens.slice(start - 1)); }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { let info = token.info.split(' ').shift(); const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (info === 'card') { info = 'card-50'; } if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` ${info} ${attrs[cmIndex][1]}` : ` ${info}`; } else { attrs.push(['class', cmIndex >= 0 ? ` ${info} ${attrs[cmIndex][1]}` : ` ${info}`]); } attrs = attrs.map(([key, value]) => { if (key === 'class') { // 处理掉 quote 存在 bg-wihte情况,和多个 bg-*情况 if (!~value.indexOf('bg') && value.indexOf('quote') === -1) { value += ' bg-white'; } } return `${key}="${value}"`; }); // opening tag return `
    \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/column.js ================================================ const name = 'column'; module.exports = { validate(params) { return params.trim().match(/^column\s*(.*)$/); }, handler(state, opts) { function getOpenToken(level) { const token = new state.Token('container_' + name + '_item_open', 'div', 1); token.block = true; token.level = 1 + level; token.attrs = [['class', name]]; return token; } function getCloseToken(level) { const token = new state.Token('container_' + name + '_item_close', 'div', -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; let open = false; let done = 0; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 tokens.splice(i + 1, 0, getOpenToken(token.level)); open = true; i++; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 tokens.splice(i, 0, getCloseToken(token.level)); open = false; i++; } else if (open && 'hr' === token.type && done === 0) { // 第一层的 Hr 需要替换 tokens.splice(i, 1, getCloseToken(token.level - 1), getOpenToken(token.level - 1)); i++; } else if (open) { // 加深一层,因为外面多套了一层div token.level = token.level + 1; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } } } return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` grid ${attrs[cmIndex][1]}` : ' grid'; } else { attrs.push(['class', cmIndex >= 0 ? `grid ${attrs[cmIndex][1]}` : 'grid']); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
    \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/cta.js ================================================ const name = 'cta'; module.exports = { validate(params) { return params.trim().match(new RegExp('^' + name + '\\s*(.*)$')); }, handler(state, opts) { function getOpenToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_open', tag, 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_close', tag, -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; let open = false; let done = 0; let count = 1; let level = 1; let imgStart = 0, imgEnd = 0; let ctxStart = 0, ctxEnd = 0; let img = [], context = []; for (let i = 0; i < tokens.length; i++) { let token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 open = true; level = token.level + 1; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 open = false; } else if (open && 'hr' === token.type && done === 0) { tokens.splice(i, 1); i--; count++; } else if (open) { // 加深一层,因为外面多套了一层div // token.level = token.level + 2; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } if (count === 1) { if (!imgStart) { imgStart = i; } else { imgEnd = i; } img.push(token); } else { if (!ctxStart) { ctxStart = i; } else { ctxEnd = i; } token.level = token.level + 1; context.push(token); } } } // 分组完成 const divNumber = getOpenToken('div', level - 1); divNumber.attrPush(['class', 'number']); tokens.splice(imgStart, imgEnd - imgStart + 1, divNumber, ...img, getCloseToken('div', level - 1)); const divToken = getOpenToken('div', level - 1); divToken.attrPush(['class', 'benefit']); tokens.splice(ctxStart + 2, ctxEnd - ctxStart + 3, divToken, ...context, getCloseToken('div', level - 1)); return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` cta ${attrs[cmIndex][1]}` : ` cta`; } else { attrs.push(['class', cmIndex >= 0 ? ` cta ${attrs[cmIndex][1]}` : ` cta`]); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
    \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/div.js ================================================ const {getAttrs, getAttrsString} = require('../attrs/utils'); module.exports = { validate(params) { return true; }, handler(state, opts) { return state; }, render(tokens, idx) { const token = tokens[idx]; let tag = 'div'; if (['footer', 'note', 'header'].includes(token.info)) { tag = token.info; } if (token.nesting === 1) { let attrs = token.attrs || []; attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `<${tag} ${attrs.join(' ')}>\n`; } else { // closing tag return `\n`; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/features.js ================================================ const name = 'features'; module.exports = { validate(params) { return params.trim().match(new RegExp('^' + name + '\\s*(.*)$')); }, handler(state, opts, start) { function getOpenToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_open', tag, 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_close', tag, -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; let open = false; let done = 0; for (let i = start; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 tokens.splice(i + 1, 0, getOpenToken('li', token.level), getOpenToken('div', token.level + 1)); open = true; i = i + 2; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 tokens.splice(i, 0, getCloseToken('div', token.level + 1), getCloseToken('li', token.level)); open = false; i = i + 2; } else if (open && 'hr' === token.type && done === 0) { // 第一层的 Hr 需要替换 tokens.splice( i, 1, getCloseToken('div', token.level), getCloseToken('li', token.level - 1), getOpenToken('li', token.level - 1), getOpenToken('div', token.level) ); i = i + 3; } else if (open) { // 加深一层,因为外面多套了一层div token.level = token.level + 2; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } } } return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` flexblock features ${attrs[cmIndex][1]}` : ` flexblock features`; } else { attrs.push([ 'class', cmIndex >= 0 ? `flexblock features ${attrs[cmIndex][1]}` : `flexblock features` ]); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
      \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/flexblock.js ================================================ const utils = require('../attrs/utils'); const attrOptions = utils.getOptions(); module.exports = { validate(params) { return params.trim().match(/^flexblock\s*(.*)$/); }, handler(state, opts, start, len) { function getOpenToken(tag, level, block = true, attrs = []) { const token = new state.Token('container_flexblock_item_' + tag + '_open', tag, 1); token.block = block; token.attrs = attrs; token.level = 1 + level; return token; } function getCloseToken(tag, level, block = true) { const token = new state.Token('container_flexblock_item_' + tag + '_close', tag, -1); token.block = block; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; opts = opts.split(/flexblock\s+/).splice(1)[0]; let attrs = []; let flexblockClass = ''; if (utils.hasDelimiters('only', attrOptions)(opts, attrOptions)) { attrs = utils.getAttrs(opts, 0, attrOptions); flexblockClass = utils.findAttrs(attrs, 'class'); } let open = false; // 支持多个,[[start,level, [item_tokens],end],[],[]...] const items = []; let itemsIdx = 0; for (let i = start; i < tokens.length; i++) { const token = tokens[i]; // 分组 if (token.type === 'container_flexblock_open') { open = true; items[itemsIdx] = [i, token.level]; } else if (token.type === 'container_flexblock_close') { open = false; // end items[itemsIdx][3] = i - 1; } else if (open && 'hr' === token.type) { items[itemsIdx++][3] = i; items[itemsIdx] = [i, token.level]; } else if (open) { items[itemsIdx][2] = items[itemsIdx][2] || []; // 全部先加深一层 token.level += 1; items[itemsIdx][2].push(token); } } // console.log(tokens[start], tokens[tokens.length - 1]); for (let i = items.length - 1; i >= 0; i--) { // 从后往前处理,可以避免i变化 const [start, level, itemArray, end] = items[i]; // console.log(items[i]) let openTag = getOpenToken('li', level); let closeTag = getCloseToken('li', level); let addCount = 2; // 增加额外几个标签 // 去除只有p包裹的外层p元素 if (itemArray[0].type === 'paragraph_open' && itemArray[itemArray.length - 1].type === 'paragraph_close') { let find = false; for (let i = 1; i < itemArray.length - 1; i++) { const token = itemArray[i]; if (token.type === 'paragraph_open') { find = true; break; } } if (!find) { // 说明是唯一哦~,那么就要扒皮了 addCount -= 2; itemArray.shift(); itemArray.pop(); } } if (flexblockClass.indexOf('blink') >= 0 && flexblockClass.indexOf('features') === -1) { // 增加a标签 let linkTag = getOpenToken('a', level); itemArray.unshift(linkTag); itemArray.unshift(openTag); let linkCloseTag = getCloseToken('a', level); itemArray.push(linkCloseTag); itemArray.push(closeTag); addCount += 2; } else if (flexblockClass.indexOf('specs') >= 0) { // 增加div标签 let linkTag = getOpenToken('div', level); itemArray.unshift(linkTag); itemArray.unshift(openTag); let linkCloseTag = getCloseToken('div', level); itemArray.push(linkCloseTag); itemArray.push(closeTag); addCount += 2; } else if (flexblockClass.indexOf('clients') >= 0) { // 偷懒的方法,直接取前三个 itemArray.splice(0, 1, getOpenToken('figure', level)); itemArray[2].type = 'text'; itemArray[2].content = ''; itemArray.splice(2, 0, getOpenToken('figcaption', level)); // 增加a标签 let linkTag = getOpenToken('a', level); itemArray.unshift(linkTag); itemArray.unshift(openTag); itemArray.push(getCloseToken('figcaption', level)); itemArray.push(getCloseToken('figure', level)); let linkCloseTag = getCloseToken('a', level); itemArray.push(linkCloseTag); itemArray.push(closeTag); addCount += 5; } else if (flexblockClass.indexOf('features') >= 0) { // 增加div标签 let linkTag = getOpenToken('div', level); itemArray.unshift(linkTag); if (flexblockClass.indexOf('blink') >= 0) { itemArray.unshift(getOpenToken('a', level)); addCount++; } itemArray.unshift(openTag); let linkCloseTag = getCloseToken('div', level); itemArray.push(linkCloseTag); itemArray.push(closeTag); if (flexblockClass.indexOf('blink') >= 0) { itemArray.push(getCloseToken('a', level)); addCount++; } addCount += 2; } else { // 默认的 itemArray.unshift(openTag); itemArray.push(closeTag); } tokens.splice(start + 1, end - start, ...itemArray); } // console.log(tokens[start], tokens[tokens.length - 1]); // console.log(tokens.slice(start)); }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` flexblock ${attrs[cmIndex][1]}` : ` flexblock`; } else { attrs.push(['class', cmIndex >= 0 ? `flexblock ${attrs[cmIndex][1]}` : `flexblock`]); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
      \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/flexbox.js ================================================ module.exports = (name, clss) => { if (!clss) { // 默认是 name clss = name; } return { validate(params) { return params.trim().match(new RegExp('^' + name + '\\s*(.*)$')); }, handler(state, opts, start, end) { function getOpenToken(level) { const token = new state.Token('container_' + name + '_item_open', 'li', 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(level) { const token = new state.Token('container_' + name + '_item_close', 'li', -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; let open = false; let done = 0; // console.log(5555,opts, tokens) for (let i = start; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 tokens.splice(i + 1, 0, getOpenToken(token.level)); i++; // console.log(666666); open = true; continue; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 tokens.splice(i, 0, getCloseToken(token.level)); i++; open = false; continue; } else if (open && 'hr' === token.type && done === 0) { // 第一层的 Hr 需要替换 // console.log(77777); tokens.splice(i, 1, getCloseToken(token.level - 1), getOpenToken(token.level - 1)); i++; } else if (open) { if (token.type === 'paragraph_close' || token.type === 'paragraph_open') { tokens.splice(i, 1); i--; continue; } // 加深一层,因为外面多套了一层div token.level = token.level + 1; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } } } // console.log(tokens); return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` flexblock ${clss} ${attrs[cmIndex][1]}` : ` flexblock ${clss}`; } else { attrs.push([ 'class', cmIndex >= 0 ? `flexblock ${clss} ${attrs[cmIndex][1]}` : `flexblock ${clss}` ]); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
      \n`; } else { // closing tag return '
    \n'; } } }; }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/gallery.js ================================================ const name = 'gallery'; module.exports = { validate(params) { return params.trim().match(/^gallery(|-overlay)\s*(.*)$/); }, handler(state, opts) { const m = opts.match(/^gallery(|-overlay)\s*(.*)$/); if (m && m[1]) { opts = m[1].trim(); } function getOpenToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_open', tag, 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_close', tag, -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; // console.log(opts); let open = false; let done = 0; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 tokens.splice(i + 1, 0, getOpenToken('li', token.level), getOpenToken('a', token.level + 1)); open = true; i = i + 2; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 tokens.splice(i, 0, getCloseToken('a', token.level + 1), getCloseToken('li', token.level)); open = false; i = i + 2; } else if (open && 'hr' === token.type && done === 0) { // 第一层的 Hr 需要替换 tokens.splice( i, 1, getCloseToken('a', token.level), getCloseToken('li', token.level - 1), getOpenToken('li', token.level - 1), getOpenToken('a', token.level) ); i = i + 3; } else if (open) { // 加深一层,因为外面多套了一层div token.level = token.level + 2; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } // 确定下一个是 img const nextToken = tokens[i + 1]; if ( token.type === 'paragraph_open' && nextToken.type === 'inline' && /^\!\[]\(.+\)$/.test(nextToken.content) ) { let level = token.level - 1; // 替换掉 paragraph_open tokens.splice(i, 1, getOpenToken('figure', level)); // 替换掉 paragraph_close tokens.splice(i + 2, 1, getCloseToken('figure', level - 2)); const tt = []; for (let j = i + 3; j < tokens.length; j++) { const temp = tokens[j]; if (temp.type === 'hr' || temp.type === 'container_gallery_close') { tokens.splice(i + 3, tt.length); let openTag, closeTag; if (opts === 'gallery-overlay') { openTag = getOpenToken('div', token.level - 2); closeTag = getCloseToken('div', token.level - 2); openTag.attrPush(['class', 'overlay']); } else { openTag = getOpenToken('figcaption', token.level - 2); closeTag = getCloseToken('figcaption', token.level - 2); } // 到头了 tokens.splice(i + 2, 0, openTag, ...tt, closeTag); // i = i + 4 + tt.length; break; } // 加深2层 temp.level = temp.level + 2; tt.push(temp); } } } } // console.log(tokens); return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` flexblock gallery ${attrs[cmIndex][1]}` : ` flexblock gallery`; } else { attrs.push(['class', cmIndex >= 0 ? `flexblock gallery ${attrs[cmIndex][1]}` : `flexblock gallery`]); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
      \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/shadow.js ================================================ const name = 'shadowbox'; module.exports = { validate(params) { return params.trim().match(/^shadowbox\s*(.*)$/); }, handler(state, opts) { function getOpenToken(level) { const token = new state.Token('container_' + name + '_item_open', 'li', 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(level) { const token = new state.Token('container_' + name + '_item_close', 'li', -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; let open = false; let done = 0; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 tokens.splice(i + 1, 0, getOpenToken(token.level)); open = true; i++; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 tokens.splice(i, 0, getCloseToken(token.level)); open = false; i++; } else if (open && 'hr' === token.type && done === 0) { // 第一层的 Hr 需要替换 tokens.splice(i, 1, getCloseToken(token.level - 1), getOpenToken(token.level - 1)); i++; } else if (open) { // 加深一层,因为外面多套了一层div token.level = token.level + 1; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } } } return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` bg-white shadow ${attrs[cmIndex][1]}` : ' bg-white shadow'; } else { attrs.push(['class', cmIndex >= 0 ? `bg-white shadow ${attrs[cmIndex][1]}` : 'bg-white shadow']); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
      \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/specs.js ================================================ const name = 'specs'; module.exports = { validate(params) { return params.trim().match(new RegExp('^' + name + '\\s*(.*)$')); }, handler(state, opts, start) { function getOpenToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_open', tag, 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_close', tag, -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; let open = false; let done = 0; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 tokens.splice(i + 1, 0, getOpenToken('li', token.level), getOpenToken('div', token.level + 1)); open = true; i = i + 2; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 tokens.splice(i, 0, getCloseToken('div', token.level + 1), getCloseToken('li', token.level)); open = false; i = i + 2; } else if (open && 'hr' === token.type && done === 0) { // 第一层的 Hr 需要替换 tokens.splice( i, 1, getCloseToken('div', token.level), getCloseToken('li', token.level - 1), getOpenToken('li', token.level - 1), getOpenToken('div', token.level) ); i = i + 3; } else if (open) { // 加深一层,因为外面多套了一层div token.level = token.level + 2; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } } } return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` flexblock specs ${attrs[cmIndex][1]}` : ` flexblock specs`; } else { attrs.push([ 'class', cmIndex >= 0 ? `flexblock specs ${attrs[cmIndex][1]}` : `flexblock specs` ]); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
      \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/containers/steps.js ================================================ const name = 'steps'; module.exports = { validate(params) { return params.trim().match(new RegExp('^' + name + '\\s*(.*)$')); }, handler(state, opts, start) { function getOpenToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_open', tag, 1); token.block = true; token.level = 1 + level; return token; } function getCloseToken(tag, level) { const token = new state.Token('container_' + name + '_' + tag + '_close', tag, -1); token.block = true; token.level = 1 + level; return token; } // tokens const tokens = state.tokens; let open = false; let done = 0; let step = 1; for (let i = start; i < tokens.length; i++) { const token = tokens[i]; if (token.type === 'container_' + name + '_open') { // 在 open 后面插入 tokens.splice(i + 1, 0, getOpenToken('li', token.level)); open = true; i++; } else if (token.type === 'container_' + name + '_close') { // 在 close 之前插入 tokens.splice(i, 0, getCloseToken('li', token.level)); open = false; i++; } else if (open && 'hr' === token.type && done === 0) { // 第一层的 Hr 需要替换 tokens.splice(i, 1, getCloseToken('li', token.level - 1), getOpenToken('li', token.level - 1)); i++; step++; if (step >= 2) { let t1 = getOpenToken('div', token.level); t1.attrPush(['class', `process step-${step}`]); tokens.splice(i + 1, 0, t1, getCloseToken('div', token.level)); i = i + 2; } } else if (open) { // 加深一层,因为外面多套了一层div token.level = token.level + 2; // 保证hr 是最贴近 container 的一层 if (/_open$/.test(token.type)) { done++; } else if (/_close$/.test(token.type)) { done--; } } } return state; }, render(tokens, idx) { const token = tokens[idx]; if (token.nesting === 1) { const cmIndex = token.attrIndex('css-module'); let clsIndex = token.attrIndex('class'); let attrs = token.attrs || []; if (clsIndex >= 0) { attrs[clsIndex][1] += cmIndex >= 0 ? ` flexblock steps ${attrs[cmIndex][1]}` : ` flexblock steps`; } else { attrs.push(['class', cmIndex >= 0 ? `flexblock steps ${attrs[cmIndex][1]}` : `flexblock steps`]); } attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }); // opening tag return `
      \n`; } else { // closing tag return '
    \n'; } } }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/echarts.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.trim(); if (token.info === 'echarts') { try { // 这里特殊处理下! const json = (1, eval)(`(function(){return ${code};})()`); let attrs = token.attrs || []; attrs = attrs.map(([key, value]) => { return `${key}="${value}"`; }).join(' ') return `
    `; } catch (err) { return `
    ${err}
    `; } } return temp(tokens, idx, options, env, slf); }; }; ================================================ FILE: packages/nodeppt-parser/lib/markdown/fa.js ================================================ const mditr = require('./regexp'); module.exports = md => { // FA4 style. md.use( mditr( /\:fa-(.+?)\:/, (match, utils) => { return ''; }, 'fa_inline' ) ); md.use( mditr( /\:\~fa-(.+?)~\:/, (match, utils) => { return ''; }, 'fa_span' ) ); md.use( mditr( /\:\:fa-(.+?)\:\:/, (match, utils) => { return ''; }, 'fa_block' ) ); md.core.ruler.push('fa_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; } // 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}>`; } 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, //