[PoC]Plugin architecture using webpack (HTML \class extension)#1074
[PoC]Plugin architecture using webpack (HTML \class extension)#1074ylemkimon wants to merge 2 commits intoKaTeX:masterfrom ylemkimon:plugin-html
Conversation
|
CLA signature looks good 👍 |
| <title>KaTeX Test</title> | ||
| <script src="katex.js" type="text/javascript"></script> | ||
| <link href="katex.css" rel="stylesheet" type="text/css"> | ||
| <script src="contrib/html.js" type="text/javascript"></script> |
There was a problem hiding this comment.
The user can use the plugin by loading after the KaTeX.
There was a problem hiding this comment.
The file looks like:
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["contrib/html"] = factory();
else
root["contrib/html"] = factory();
})(this, function() {
return webpackJsonp_name_([0],{
/***/ 59:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray__ = __webpack_require__(30);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__src_defineFunction__ = __webpack_require__(2);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__src_buildCommon__ = __webpack_require__(0);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__src_mathMLTree__ = __webpack_require__(1);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__src_domTree__ = __webpack_require__(12);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__src_buildHTML__ = __webpack_require__(3);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__src_buildMathML__ = __webpack_require__(4);
Object(__WEBPACK_IMPORTED_MODULE_1__src_defineFunction__["b" /* default */])({
type: "class",
names: ["\\class"],
props: {
numArgs: 2,
allowedInText: true,
greediness: 3,
argTypes: ["string", "original"]
},
handler: function handler(context, args) {
var classes = args[0];
var body = args[1];
return {
type: "class",
classes: classes.value,
value: Object(__WEBPACK_IMPORTED_MODULE_1__src_defineFunction__["c" /* ordargument */])(body)
};
},
htmlBuilder: function htmlBuilder(group, options) {
var elements = Object(__WEBPACK_IMPORTED_MODULE_5__src_buildHTML__["a" /* buildExpression */])(group.value.value, options, false);
var classes = group.value.classes.trim().split(/\s+/);
var fragment = new __WEBPACK_IMPORTED_MODULE_2__src_buildCommon__["a" /* default */].makeFragment(elements);
fragment.children.forEach(function (children) {
if (!(children instanceof __WEBPACK_IMPORTED_MODULE_4__src_domTree__["a" /* default */].svgNode)) {
var _children$classes;
(_children$classes = children.classes).push.apply(_children$classes, __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray___default()(classes));
}
});
return fragment;
},
mathmlBuilder: function mathmlBuilder(group, options) {
var inner = Object(__WEBPACK_IMPORTED_MODULE_6__src_buildMathML__["a" /* buildExpression */])(group.value.value, options);
var node = new __WEBPACK_IMPORTED_MODULE_3__src_mathMLTree__["a" /* default */].MathNode("mstyle", inner);
node.setAttribute("class", group.value.classes);
return node;
}
});
/***/ })
},[59])["default"];
});
|
This is really exciting to see. I'd like to take a deeper look at this soon. Would applying the non-standard |
| @@ -1,5 +1,5 @@ | |||
| .PHONY: build dist lint setup copy serve clean metrics test coverage zip contrib flow | |||
| build: test build/katex.min.js build/katex.min.css contrib zip compress | |||
| .PHONY: build dist lint setup webpack serve clean metrics test coverage zip flow | |||
There was a problem hiding this comment.
Assuming most of the infrastructure related changes go away once the webpack PR is merged?
What changes are specific to this PR?
There was a problem hiding this comment.
@rrandallcainc Yes. Currently only the last commit(5824510) is specific to this PR.
There was a problem hiding this comment.
Doh, should have read your full comment :)
|
@rrandallcainc As an element ID should be unique, I think it should be applied to the enclosing span. |
|
Just want to make sure I understand this plugin system: Since there are two separate entry points (contrib/html, and katex) which both share common KaTeX code, you use the CommonsChunkPlugin to extract the KaTeX specific code into it's own chunk, which these two entry points can share. This ensures that there's no code duplication, and Might need clarification from @kevinbarabash but was the intention to inject plugin functionality at run-time, not build-time? In other words, I think this change is dependent on the webpack config. If an outside user wants to plugin their own custom function, would they need to create a webpack config to allow them to do so? |
| import {buildExpression as htmlBuildExpression} from "../../src/buildHTML"; | ||
| import {buildExpression as mmlBuildExpression} from "../../src/buildMathML"; | ||
|
|
||
| defineFunction({ |
There was a problem hiding this comment.
It feels a little odd that some of this logic lies outside of the source (like this), but other logic needs to lie within the source (like Parser/types). That being said, I'm not sure there's a better way to implement it at this time without a major change.
|
@rrandallcainc
The common codes(KaTeX specific codes) are extracted to the
Yes.
Yes, the developer has to have KaTeX sources and add an entry to the webpack config to build a plugin. It will be like a SDK and maybe we can provide a boilerplate.
I'm thinking of adding I think with |
| type GroupParser = (optional: boolean) => ?ParsedFuncOrArgOrDollar; | ||
| const GROUP_PARSER_EXTENSIONS: { [type: string]: GroupParser } = {}; | ||
| export function defineGroupParser(type: string, parser: GroupParser) { | ||
| GROUP_PARSER_EXTENSIONS[type] = parser; |
There was a problem hiding this comment.
Interesting idea. This could also be used for defining a custom parser for \ce from the mhchem package.
|
My original thinking of a plugin system is that it would be more of a runtime system thing. I hadn't considered leveraging the fact that we're moving to webpack to build KaTeX. It feels a little weird imposing plugin makers use a particular build system. I need to think about it a bit more. A runtime plugin system would require us to expose more methods for generating HTML and MathML. I think if/when we moved to HAST for our internal representation of HTML and MathML then we wouldn't have to expose as much. If/when we move to an intermediate representation we'd still want to expose methods for building that IR. |
|
@ylemkimon can you rebase when you have a chance? I'm curious to see what this diff looks like now that the webpack changes have been merged. |
|
@kevinbarabash Done. |
k4b7
left a comment
There was a problem hiding this comment.
I think this is probably okay for building extensions in the contrib folder. We still have a long way to go in cleaning up and stabilizing internal data structures before we'll have something that can be used for runtime plugins. I like that \class is being defined in the contrib folder as it's not standard LaTeX.
| return newArgument(new ParseNode("string", res.text, "text"), res); | ||
| }); | ||
|
|
||
| // $FlowFixMe |
There was a problem hiding this comment.
@kevinbarabash props.argTypes expects an array of ArgType, so it doesn't recognize custom string group type. I couldn't find a way to extend ArgType from defineGroupParser.
There was a problem hiding this comment.
I think it's fine to just change it in types.js. Eventually though, we may say that
type ArgType = string;
and add other programmatic checks as necessary.
| type GroupParser = (optional: boolean) => ?ParsedFuncOrArgOrDollar; | ||
| const GROUP_PARSER_EXTENSIONS: { [type: string]: GroupParser } = {}; | ||
| export function defineGroupParser(type: string, parser: GroupParser) { | ||
| GROUP_PARSER_EXTENSIONS[type] = parser; |
There was a problem hiding this comment.
Interesting idea. This could also be used for defining a custom parser for \ce from the mhchem package.
| @@ -758,6 +764,12 @@ export default class Parser { | |||
| return this.parseUrlGroup(optional); | |||
| } | |||
There was a problem hiding this comment.
Maybe defineGroupParser can be used for color, size, url, and even original.
| if (!(children instanceof domTree.svgNode)) { | ||
| children.classes.push(...classes); | ||
| } | ||
| }); |
There was a problem hiding this comment.
I would've expected all of the elements to be wrapped in a single span with the given class applied. This applies the class to each element. Is that what MathJax does?
There was a problem hiding this comment.
@kevinbarabash No, MathJax does what you said:
all of the elements to be wrapped in a single
spanwith the given class applied.
Though, I'm not sure which is more suitable. For wrapping in a span, I think we can do in a similar way to \href:
https://github.com/Khan/KaTeX/blob/2a2742453b86fba6fd5184efb3c3255400796fb7/src/functions/href.js#L25-L66
There was a problem hiding this comment.
#1070 has been merged, but I think we should probably do what MathJax does since that's probably the only other TeX renderer which supports \class.
| const inner = mmlBuildExpression(group.value.value, options); | ||
| const node = new mathMLTree.MathNode("mstyle", inner); | ||
| node.setAttribute("class", group.value.classes); | ||
| return node; |
There was a problem hiding this comment.
The behavior of the MathML builder is quite different from the HTML builder. They should probably have similar semantics, e.g. apply the class to each individual element or wrapping all of the in a parent element.
| type Target = {| | ||
| name: string, // the name of output JS/CSS | ||
| entry: string, // the path to the entry point | ||
| entry: string | Object, // the path to the entry point |
There was a problem hiding this comment.
It's too bad there aren't any existing flow type definitions for webpack.
There was a problem hiding this comment.
@kevinbarabash There seems to be an open issue in flow-typed/flow-typed#165. Also webpack has JSON Schema(schemas/webpackOptionsSchema.json) and maybe we can convert it using json-schema-to-flow-type. I wonder there is any way to import JSON schema to flow directly.
There was a problem hiding this comment.
I wouldn't worry about it for this PR.
| target.name === 'katex' && new webpack.optimize.CommonsChunkPlugin({ | ||
| name: 'katex', | ||
| minChunks: Infinity, | ||
| }), |
There was a problem hiding this comment.
So if any modules from the katex chunk also appear in any other chunk (e.g. the contrib/html) remove them from the other chunk.
|
I think if we go with this way for now, it would be good to add some documentation around how users can leverage this plugin system. |
|
@rrandallcainc I think we can provide a boilerplate plugin. |
Looks like you have at least the parser half done.
We need some plugins first. :P
I suppose that someone could make one use the technique demonstrated in this PR. The problem though is that they'd need to require a number of files directly from the source. Pretty much every JS library I've seen that supports plugins allows to create a plugin either without an dependencies or by requiring some utilities to help work with the data structures the library uses. I think the code in this PR is useful. It expands the number of extension points we have by introducing This could use some documentation. Could you add an
We'd probably want some sort of versioning system. Even before that though there are some key changes that I'd like to make first:
I think the last one may take a while so maybe that's a v2 thing and maybe we can move KaTeX to v1 after we finish the other two. |
k4b7
left a comment
There was a problem hiding this comment.
Requesting two changes:
- wrap contents of
\classin a single span - update types.js and remove $FlowFixMe
|
Agreed with |
|
@rrandallcainc Yes, if we use single span, we can just set the id of it. Blocked on #1103. |
|
@ylemkimon I wonder if you had any thoughts about how we might allow plugins/extensions to add fonts and font metrics. I looking into generating Cyrillic fonts. If we add support for more scripts with fonts and metrics it would be nice to allow people to load only those which they think they might need. |
|
I believe this is no longer blocked by other PRs. |
|
Custom classes (and ids) are being added in #1437. |
Depends on #1068
Fixes #762
Fixes #90, fixes #507
See 5824510.
This is a proof of concept of implementing a plugin architecture using webpack and implements non-standard
\class{name}{math}extension. UsingCommonsChunkPlugin, it is possible to build codes using KaTeX internal functions, without exposing them to the public API(although it exposeswebpackJsonp__name_to the global) or duplicating their codes.Things to consider: