commit 8c1af7cdbba2750cb2424c03318fef5251443568
Author: Jean Schurger <jean@schurger.org>
Date: Tue, 17 Dec 2024 12:54:00 -0500
Imported.
Diffstat:
24 files changed, 3665 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+.coverage
+.DS_Store
diff --git a/Makefile b/Makefile
@@ -0,0 +1,15 @@
+top: README.html routes.html
+
+%.html: %.org
+ cat _org/header >$@
+ cat _org/style.css >>$@
+ cat _org/body >>$@
+ cat $< | ./_org/org2html.js >> $@
+ cat _org/footer >>$@
+
+
+routes.org: cadre_test_controllers/*.py
+ cadre-test-app-dump-routes config.ini >routes.org
+
+tests:
+ nosetests --nocapture --with-coverage --cover-inclusive --cover-package=cadre,cadre_flask,cadre_test_controllers,cadre_test_app
diff --git a/README.org b/README.org
@@ -0,0 +1,334 @@
+#+TITLE: This is the 'cadre' framework !
+
+This is the cadre framework engine.
+
+* Introduction
+
+It is a simple web framework, without hard dependencies to base web framework.
+Right now, the only implementation of the underlying framework available is 'flask'.
+
+* Things you can do with it
+
+** Install it !
+
+#+begin_example shell
+python setup.py develop
+#+end_example
+
+** Create an App
+
+Your app *MUST* be packaged ! (also your controllers).
+
+*** setup.py
+
+#+begin_example python
+from setuptools import setup
+
+console_scripts = """
+cadre-test-app-serve = cadre_test_app:serve
+cadre-test-app-dump-routes = cadre_test_app:dump_routes
+
+"""
+
+setup(name='cadre_test_app',
+ version='0.1.0',
+ description="Cadre_Test_App framework",
+ author="Ubity Awesome Dev Team",
+ author_email='dev@ubity.com',
+ packages=['cadre_test_app'],
+ install_requires=["Cadre"],
+ entry_points={'console_scripts': console_scripts,
+ 'cadre_test_app.controllers': [
+ 'hello = cadre_test_app_hello']},
+ license='GPLv3')
+#+end_example python
+
+*** main (cadre_test_app/__init__.py)
+
+#+begin_example python
+import sys
+from cadre import Cadre
+
+class Cadre_Test_App(Cadre):
+ pass
+
+def serve():
+ if len(sys.argv) != 2:
+ print("Usage: %s config.ini")
+ sys.exit(1)
+ Cadre_Test_App(sys.argv[1]).serve()
+
+# tool to dump routes
+def dump_routes():
+ if len(sys.argv) != 2:
+ print("Usage: %s config.ini")
+ sys.exit(1)
+ Cadre_Test_App(sys.argv[1]).dump_routes()
+#+end_example
+
+*** config.ini
+
+Should contains at least a section telling that we are using /flask/ (as this is the only sub framework implemented.
+The section name is you app name (ie: *cadre_test_app*).
+
+#+begin_example
+[cadre_test_app]
+framework = flask
+#+end_example
+
+Optionnaly a *[flask]* section to give to 'flask'.
+
+#+begin_example
+[flask]
+PORT = 5001
+DEBUG = true
+HOST = 0.0.0.0
+#+end_example
+
+Optionnaly the *loggers* section.
+#+begin_example
+[loggers]
+keys = root, cadre, cadre_flask
+
+[logger_root]
+handlers =
+
+[logger_cadre]
+level = DEBUG
+handlers = console
+qualname = cadre
+
+[logger_cadre_flask]
+level = DEBUG
+handlers = console
+qualname = cadre_flask
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[formatter_generic]
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+#+end_example
+
+*** a controller (cadre_test_app_hello/__init__.py)
+
+#+begin_example python
+from cadre import Controller
+
+
+class Hello(Controller):
+ """ A very polite controller """
+
+ def index__(self):
+ """ Says hello ! """
+ return "Hello world 0"
+
+
+class Hello_2(Controller):
+ """ A very polite controller (version 2) """
+
+ def index__(self):
+ """ Says hello ! """
+ return "Hello world"
+
+ def to(self, who):
+ """ Says hello to whom ask for """
+ return "Hello %s !" % who
+
+ def foo(self):
+ return "Foo"
+
+ def foo_(self):
+ return "Foo with an '_' at the end !"
+
+ def bar_GET(self):
+ return "Bar"
+
+ def baz_POST(self):
+ return "Baz"
+
+ def qux_GET_POST(self):
+ return "Qux"
+#+end_example
+
+** Run you app
+
+#+begin_example
+python setup.py develop
+cadre_test_app-serve config.ini
+#+end_example
+
+** Query it
+
+*** Simple query
+
+#+begin_example
+curl http://localhost:5001/hello/
+#+end_example
+
+*** Specific version of a controller
+
+**** Using headers
+#+begin_example
+curl http://localhost:5001/hello/ -H X-cadre_test_app-version:0
+#+end_example
+
+**** Using URL prefix
+
+You can set:
+
+#+begin_example
+versioning = prefix
+#+end_example
+
+In you're application section in the configuration file.
+Doing so will prepend the version as prefix to each routes.
+
+**** First version
+
+By default, without specifying any version number, the first version of a controller is *0*.
+To set a different version number, use the *first_version* keyword in your application's section configuration file.
+
+#+begin_example
+first_version = 1
+#+end_example
+
+*** Parameters
+
+Routes are constructed using the controllers, method names and method args.
+
+In the following example,
+
+#+begin_example
+class Foo(Controller):
+
+ def bar(self, baz):
+ pass
+#+end_example
+
+The matching route will be: */foo/bar/<baz>*
+
+For example querying */foo/bar/123* would call the *bar()* method with the value *123* for the *baz* parameter.
+
+*** Args
+
+Args are the url-encoded key=value style args after the ? in the URL.
+They are available in requests.args.
+
+#+begin_example
+class Foo(Controller):
+
+ def bar(self):
+ from cadre import request
+ request.args.get('qwe')
+#+end_example
+
+
+*** Headers
+
+#+begin_example
+class Foo(Controller):
+
+ def bar(self):
+ from cadre import request
+ request.headers.get('X-Magic-Header')
+#+end_example
+
+*** Cookies
+
+#+begin_example
+class Foo(Controller):
+
+ def bar_needs_cookie(self):
+ from cadre import request
+ request.cookies.get('my_great_cookie')
+
+ def baz_set_cookie(self):
+ from cadre import request
+ request.cookies.set('my_great_cookie', 'my_great_value')
+#+end_example
+
+
+*** JSON
+
+#+begin_example
+class Foo(Controller):
+
+ def bar(self):
+ from cadre import request
+ request.json # containt an object,
+ # mostlikely a dict of the received JSON
+#+end_example
+
+** Dump routes
+
+#+begin_example
+python setup.py develop # if not done already
+cadre_test_app-dump-routes
+#+end_example
+
+** Run with UWSGI
+
+You can use UWSGI to run your app.
+For that, you will need some UWSGI configuration and loader.
+
+*** configuration
+
+Can be added in /config.ini/
+
+#+begin_example
+[uwsgi]
+http = localhost:5001
+file = cadre_test_app.wsgi
+#+end_example
+
+*** loader cadre_test_app.wsgi
+#+begin_example python
+from cadre_test_app import Cadre_Test_App
+application = Cadre_Test_App("config.ini").get_wsgi_app()
+#+end_example
+
+*** start it
+
+#+begin_example
+uwsgi config.ini
+#+end_example
+
+
+* Running 'cadre' tests
+
+#+begin_example shell
+pip install nose coverage
+make tests
+#+end_example
+
+* Nice features (not yet documented)
+
+- Custom error handling
+- *before* and *after* hooks
+- Automatic detection of missing tests
+- Validation of input JSON using decorator
+
+* Things to do as soon as possible
+
+- [X] Send a file
+- [X] Handle query args
+- [X] Return HTTP code on error
+- [X] Get cookie
+- [X] Set cookie
+- [X] Add controllers test and runtime
+- [X] walk sub __subclasses
+- [X] Honor version in the flask routes
+- [X] Raise a +meaningfull+ 405 if method is missing (route existing from another version)
+- [X] Honor the verb
+- [X] Make uwsgi friendly (returning an app)
+- [X] Add a uwsgi example
+- [X] Add __before__ __after__
+- [X] Add json
+- [X] Get and Set cookies
diff --git a/_org/body b/_org/body
@@ -0,0 +1,3 @@
+ </style>
+ </head>
+ <body>
+\ No newline at end of file
diff --git a/_org/footer b/_org/footer
@@ -0,0 +1,2 @@
+ </body>
+</html>
diff --git a/_org/header b/_org/header
@@ -0,0 +1,6 @@
+<!doctype html>
+<html lang="fr">
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
+ <style>
+\ No newline at end of file
diff --git a/_org/org.js b/_org/org.js
@@ -0,0 +1,1851 @@
+// Generated by export.rb at Fri Apr 26 15:29:06 UTC 2019
+/*
+ Copyright (c) 2014 Masafumi Oyamada
+
+ 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.
+*/
+var Org = (function () {
+ var exports = {};
+
+ // ------------------------------------------------------------
+ // Syntax
+ // ------------------------------------------------------------
+
+ var Syntax = {
+ rules: {},
+
+ define: function (name, syntax) {
+ this.rules[name] = syntax;
+ var methodName = "is" + name.substring(0, 1).toUpperCase() + name.substring(1);
+ this[methodName] = function (line) {
+ return this.rules[name].exec(line);
+ };
+ }
+ };
+
+ Syntax.define("header", /^(\*+)\s+(.*)$/); // m[1] => level, m[2] => content
+ Syntax.define("preformatted", /^(\s*):(?: (.*)$|$)/); // m[1] => indentation, m[2] => content
+ Syntax.define("unorderedListElement", /^(\s*)(?:-|\+|\s+\*)\s+(.*)$/); // m[1] => indentation, m[2] => content
+ Syntax.define("orderedListElement", /^(\s*)(\d+)(?:\.|\))\s+(.*)$/); // m[1] => indentation, m[2] => number, m[3] => content
+ Syntax.define("tableSeparator", /^(\s*)\|((?:\+|-)*?)\|?$/); // m[1] => indentation, m[2] => content
+ Syntax.define("tableRow", /^(\s*)\|(.*?)\|?$/); // m[1] => indentation, m[2] => content
+ Syntax.define("blank", /^$/);
+ Syntax.define("horizontalRule", /^(\s*)-{5,}$/); //
+ Syntax.define("directive", /^(\s*)#\+(?:(begin|end)_)?(.*)$/i); // m[1] => indentation, m[2] => type, m[3] => content
+ Syntax.define("comment", /^(\s*)#(.*)$/);
+ Syntax.define("line", /^(\s*)(.*)$/);
+ Syntax.define("property", /^([A-Z]+):\s(.*)$/); // m[1] => keyword, m[2] => value
+
+ // ------------------------------------------------------------
+ // Token
+ // ------------------------------------------------------------
+
+ function Token() {
+ }
+
+ Token.prototype = {
+ isListElement: function () {
+ return this.type === Lexer.tokens.orderedListElement ||
+ this.type === Lexer.tokens.unorderedListElement;
+ },
+
+ isTableElement: function () {
+ return this.type === Lexer.tokens.tableSeparator ||
+ this.type === Lexer.tokens.tableRow;
+ }
+ };
+
+ // ------------------------------------------------------------
+ // Lexer
+ // ------------------------------------------------------------
+
+ function Lexer(stream) {
+ this.stream = stream;
+ this.tokenStack = [];
+ }
+
+ Lexer.prototype = {
+ tokenize: function (line) {
+ var token = new Token();
+ token.fromLineNumber = this.stream.lineNumber;
+
+ if (Syntax.isHeader(line)) {
+ token.type = Lexer.tokens.header;
+ token.indentation = 0;
+ token.content = RegExp.$2;
+ // specific
+ token.level = RegExp.$1.length;
+ } else if(Syntax.isProperty(line)) {
+ token.type = Lexer.tokens.property;
+ token.indentation = 0;
+ token.content = RegExp.$2;
+ // specific
+ token.keyword = RegExp.$1;
+ } else if (Syntax.isPreformatted(line)) {
+ token.type = Lexer.tokens.preformatted;
+ token.indentation = RegExp.$1.length;
+ token.content = RegExp.$2;
+ } else if (Syntax.isUnorderedListElement(line)) {
+ token.type = Lexer.tokens.unorderedListElement;
+ token.indentation = RegExp.$1.length;
+ token.content = RegExp.$2;
+ } else if (Syntax.isOrderedListElement(line)) {
+ token.type = Lexer.tokens.orderedListElement;
+ token.indentation = RegExp.$1.length;
+ token.content = RegExp.$3;
+ // specific
+ token.number = RegExp.$2;
+ } else if (Syntax.isTableSeparator(line)) {
+ token.type = Lexer.tokens.tableSeparator;
+ token.indentation = RegExp.$1.length;
+ token.content = RegExp.$2;
+ } else if (Syntax.isTableRow(line)) {
+ token.type = Lexer.tokens.tableRow;
+ token.indentation = RegExp.$1.length;
+ token.content = RegExp.$2;
+ } else if (Syntax.isBlank(line)) {
+ token.type = Lexer.tokens.blank;
+ token.indentation = 0;
+ token.content = null;
+ } else if (Syntax.isHorizontalRule(line)) {
+ token.type = Lexer.tokens.horizontalRule;
+ token.indentation = RegExp.$1.length;
+ token.content = null;
+ } else if (Syntax.isDirective(line)) {
+ token.type = Lexer.tokens.directive;
+ token.indentation = RegExp.$1.length;
+ token.content = RegExp.$3;
+ // decide directive type (begin, end or oneshot)
+ var directiveTypeString = RegExp.$2;
+ if (/^begin/i.test(directiveTypeString))
+ token.beginDirective = true;
+ else if (/^end/i.test(directiveTypeString))
+ token.endDirective = true;
+ else
+ token.oneshotDirective = true;
+ } else if (Syntax.isComment(line)) {
+ token.type = Lexer.tokens.comment;
+ token.indentation = RegExp.$1.length;
+ token.content = RegExp.$2;
+ } else if (Syntax.isLine(line)) {
+ token.type = Lexer.tokens.line;
+ token.indentation = RegExp.$1.length;
+ token.content = RegExp.$2;
+ } else {
+ throw new Error("SyntaxError: Unknown line: " + line);
+ }
+
+ return token;
+ },
+
+ pushToken: function (token) {
+ this.tokenStack.push(token);
+ },
+
+ pushDummyTokenByType: function (type) {
+ var token = new Token();
+ token.type = type;
+ this.tokenStack.push(token);
+ },
+
+ peekStackedToken: function () {
+ return this.tokenStack.length > 0 ?
+ this.tokenStack[this.tokenStack.length - 1] : null;
+ },
+
+ getStackedToken: function () {
+ return this.tokenStack.length > 0 ?
+ this.tokenStack.pop() : null;
+ },
+
+ peekNextToken: function () {
+ return this.peekStackedToken() ||
+ this.tokenize(this.stream.peekNextLine());
+ },
+
+ getNextToken: function () {
+ return this.getStackedToken() ||
+ this.tokenize(this.stream.getNextLine());
+ },
+
+ hasNext: function () {
+ return this.stream.hasNext();
+ },
+
+ getLineNumber: function () {
+ return this.stream.lineNumber;
+ }
+ };
+
+ Lexer.tokens = {};
+ [
+ "header",
+ "orderedListElement",
+ "unorderedListElement",
+ "tableRow",
+ "tableSeparator",
+ "preformatted",
+ "line",
+ "horizontalRule",
+ "blank",
+ "directive",
+ "comment",
+ "property"
+ ].forEach(function (tokenName, i) {
+ Lexer.tokens[tokenName] = i;
+ });
+
+ // ------------------------------------------------------------
+ // Exports
+ // ------------------------------------------------------------
+
+ if (typeof exports !== "undefined")
+ exports.Lexer = Lexer;
+
+ function PrototypeNode(type, children) {
+ this.type = type;
+ this.children = [];
+
+ if (children) {
+ for (var i = 0, len = children.length; i < len; ++i) {
+ this.appendChild(children[i]);
+ }
+ }
+ }
+ PrototypeNode.prototype = {
+ previousSibling: null,
+ parent: null,
+ get firstChild() {
+ return this.children.length < 1 ?
+ null : this.children[0];
+ },
+ get lastChild() {
+ return this.children.length < 1 ?
+ null : this.children[this.children.length - 1];
+ },
+ appendChild: function (newChild) {
+ var previousSibling = this.children.length < 1 ?
+ null : this.lastChild;
+ this.children.push(newChild);
+ newChild.previousSibling = previousSibling;
+ newChild.parent = this;
+ },
+ toString: function () {
+ var string = "<" + this.type + ">";
+
+ if (typeof this.value !== "undefined") {
+ string += " " + this.value;
+ } else if (this.children) {
+ string += "\n" + this.children.map(function (child, idx) {
+ return "#" + idx + " " + child.toString();
+ }).join("\n").split("\n").map(function (line) {
+ return " " + line;
+ }).join("\n");
+ }
+
+ return string;
+ }
+ };
+
+ var Node = {
+ types: {},
+
+ define: function (name, postProcess) {
+ this.types[name] = name;
+
+ var methodName = "create" + name.substring(0, 1).toUpperCase() + name.substring(1);
+ var postProcessGiven = typeof postProcess === "function";
+
+ this[methodName] = function (children, options) {
+ var node = new PrototypeNode(name, children);
+
+ if (postProcessGiven)
+ postProcess(node, options || {});
+
+ return node;
+ };
+ }
+ };
+
+ Node.define("text", function (node, options) {
+ node.value = options.value;
+ });
+ Node.define("header", function (node, options) {
+ node.level = options.level;
+ });
+ Node.define("property");
+ Node.define("orderedList");
+ Node.define("unorderedList");
+ Node.define("definitionList");
+ Node.define("listElement");
+ Node.define("paragraph");
+ Node.define("preformatted");
+ Node.define("table");
+ Node.define("tableRow");
+ Node.define("tableCell");
+ Node.define("horizontalRule");
+ Node.define("directive");
+
+ // Inline
+ Node.define("inlineContainer");
+
+ Node.define("bold");
+ Node.define("italic");
+ Node.define("underline");
+ Node.define("code");
+ Node.define("verbatim");
+ Node.define("dashed");
+ Node.define("link", function (node, options) {
+ node.src = options.src;
+ });
+
+ if (typeof exports !== "undefined")
+ exports.Node = Node;
+
+ function Stream(sequence) {
+ this.sequences = sequence.split(/\r?\n/);
+ this.totalLines = this.sequences.length;
+ this.lineNumber = 0;
+ }
+
+ Stream.prototype.peekNextLine = function () {
+ return this.hasNext() ? this.sequences[this.lineNumber] : null;
+ };
+
+ Stream.prototype.getNextLine = function () {
+ return this.hasNext() ? this.sequences[this.lineNumber++] : null;
+ };
+
+ Stream.prototype.hasNext = function () {
+ return this.lineNumber < this.totalLines;
+ };
+
+ if (typeof exports !== "undefined") {
+ exports.Stream = Stream;
+ }
+
+ // var Stream = require("./stream.js").Stream;
+ // var Lexer = require("./lexer.js").Lexer;
+ // var Node = require("./node.js").Node;
+
+ function Parser() {
+ this.inlineParser = new InlineParser();
+ }
+
+ Parser.parseStream = function (stream, options) {
+ var parser = new Parser();
+ parser.initStatus(stream, options);
+ parser.parseNodes();
+ return parser.nodes;
+ };
+
+ Parser.prototype = {
+ initStatus: function (stream, options) {
+ if (typeof stream === "string")
+ stream = new Stream(stream);
+ this.lexer = new Lexer(stream);
+ this.nodes = [];
+ this.options = {
+ toc: true,
+ num: true,
+ "^": "{}",
+ multilineCell: false
+ };
+ // Override option values
+ if (options && typeof options === "object") {
+ for (var key in options) {
+ this.options[key] = options[key];
+ }
+ }
+ this.document = {
+ options: this.options,
+ directiveValues: {},
+ convert: function (ConverterClass, exportOptions) {
+ var converter = new ConverterClass(this, exportOptions);
+ return converter.result;
+ }
+ };
+ },
+
+ parse: function (stream, options) {
+ this.initStatus(stream, options);
+ this.parseDocument();
+ this.document.nodes = this.nodes;
+ return this.document;
+ },
+
+ createErrorReport: function (message) {
+ return new Error(message + " at line " + this.lexer.getLineNumber());
+ },
+
+ skipBlank: function () {
+ var blankToken = null;
+ while (this.lexer.peekNextToken().type === Lexer.tokens.blank)
+ blankToken = this.lexer.getNextToken();
+ return blankToken;
+ },
+
+ setNodeOriginFromToken: function (node, token) {
+ node.fromLineNumber = token.fromLineNumber;
+ return node;
+ },
+
+ appendNode: function (newNode) {
+ var previousSibling = this.nodes.length > 0 ? this.nodes[this.nodes.length - 1] : null;
+ this.nodes.push(newNode);
+ newNode.previousSibling = previousSibling;
+ },
+
+ // ------------------------------------------------------------
+ // <Document> ::= <Element>*
+ // ------------------------------------------------------------
+
+ parseDocument: function () {
+ this.parseTitle();
+ this.parseNodes();
+ },
+
+ parseNodes: function () {
+ while (this.lexer.hasNext()) {
+ var element = this.parseElement();
+ if (element) this.appendNode(element);
+ }
+ },
+
+ parseTitle: function () {
+ this.skipBlank();
+
+ if (this.lexer.hasNext() &&
+ this.lexer.peekNextToken().type === Lexer.tokens.line)
+ this.document.title = this.createTextNode(this.lexer.getNextToken().content);
+ else
+ this.document.title = null;
+
+ this.lexer.pushDummyTokenByType(Lexer.tokens.blank);
+ },
+
+ // ------------------------------------------------------------
+ // <Element> ::= (<Header> | <List>
+ // | <Preformatted> | <Paragraph>
+ // | <Table>)*
+ // ------------------------------------------------------------
+
+ parseElement: function () {
+ var element = null;
+
+ switch (this.lexer.peekNextToken().type) {
+ case Lexer.tokens.header:
+ element = this.parseHeader();
+ break;
+ case Lexer.tokens.property:
+ element = this.parseProperty();
+ break;
+ case Lexer.tokens.preformatted:
+ element = this.parsePreformatted();
+ break;
+ case Lexer.tokens.orderedListElement:
+ case Lexer.tokens.unorderedListElement:
+ element = this.parseList();
+ break;
+ case Lexer.tokens.line:
+ element = this.parseText();
+ break;
+ case Lexer.tokens.tableRow:
+ case Lexer.tokens.tableSeparator:
+ element = this.parseTable();
+ break;
+ case Lexer.tokens.blank:
+ this.skipBlank();
+ if (this.lexer.hasNext()) {
+ if (this.lexer.peekNextToken().type === Lexer.tokens.line)
+ element = this.parseParagraph();
+ else
+ element = this.parseElement();
+ }
+ break;
+ case Lexer.tokens.horizontalRule:
+ this.lexer.getNextToken();
+ element = Node.createHorizontalRule();
+ break;
+ case Lexer.tokens.directive:
+ element = this.parseDirective();
+ break;
+ case Lexer.tokens.comment:
+ // Skip
+ this.lexer.getNextToken();
+ break;
+ default:
+ throw this.createErrorReport("Unhandled token: " + this.lexer.peekNextToken().type);
+ }
+
+ return element;
+ },
+
+ parseElementBesidesDirectiveEnd: function () {
+ try {
+ // Temporary, override the definition of `parseElement`
+ this.parseElement = this.parseElementBesidesDirectiveEndBody;
+ return this.parseElement();
+ } finally {
+ this.parseElement = this.originalParseElement;
+ }
+ },
+
+ parseElementBesidesDirectiveEndBody: function () {
+ if (this.lexer.peekNextToken().type === Lexer.tokens.directive &&
+ this.lexer.peekNextToken().endDirective) {
+ return null;
+ }
+
+ return this.originalParseElement();
+ },
+
+ // ------------------------------------------------------------
+ // <Header>
+ //
+ // : preformatted
+ // : block
+ // ------------------------------------------------------------
+
+ parseHeader: function () {
+ var headerToken = this.lexer.getNextToken();
+ var header = Node.createHeader([
+ this.createTextNode(headerToken.content) // TODO: Parse inline markups
+ ], { level: headerToken.level });
+ this.setNodeOriginFromToken(header, headerToken);
+
+ while(this.lexer.hasNext() && this.lexer.peekNextToken().type === Lexer.tokens.property){
+ var property = this.parseProperty();
+ header.appendChild(property);
+ }
+
+ return header;
+ },
+
+ parseProperty: function(rootToken){
+ var propertyToken = this.lexer.getNextToken();
+ var property = Node.createProperty(null);
+ this.setNodeOriginFromToken(property, propertyToken);
+ property.keyword = propertyToken.keyword;
+ property.content = propertyToken.content;
+ return property;
+ },
+
+ // ------------------------------------------------------------
+ // <Preformatted>
+ //
+ // : preformatted
+ // : block
+ // ------------------------------------------------------------
+
+ parsePreformatted: function () {
+ var preformattedFirstToken = this.lexer.peekNextToken();
+ var preformatted = Node.createPreformatted([]);
+ this.setNodeOriginFromToken(preformatted, preformattedFirstToken);
+
+ var textContents = [];
+
+ while (this.lexer.hasNext()) {
+ var token = this.lexer.peekNextToken();
+ if (token.type !== Lexer.tokens.preformatted ||
+ token.indentation < preformattedFirstToken.indentation)
+ break;
+ this.lexer.getNextToken();
+ textContents.push(token.content);
+ }
+
+ preformatted.appendChild(this.createTextNode(textContents.join("\n"), true /* no emphasis */));
+
+ return preformatted;
+ },
+
+ // ------------------------------------------------------------
+ // <List>
+ //
+ // - foo
+ // 1. bar
+ // 2. baz
+ // ------------------------------------------------------------
+
+ // XXX: not consider codes (e.g., =Foo::Bar=)
+ definitionPattern: /^(.*?) :: *(.*)$/,
+
+ parseList: function () {
+ var rootToken = this.lexer.peekNextToken();
+ var list;
+ var isDefinitionList = false;
+
+ if (this.definitionPattern.test(rootToken.content)) {
+ list = Node.createDefinitionList([]);
+ isDefinitionList = true;
+ } else {
+ list = rootToken.type === Lexer.tokens.unorderedListElement ?
+ Node.createUnorderedList([]) : Node.createOrderedList([]);
+ }
+ this.setNodeOriginFromToken(list, rootToken);
+
+ while (this.lexer.hasNext()) {
+ var nextToken = this.lexer.peekNextToken();
+ if (!nextToken.isListElement() || nextToken.indentation !== rootToken.indentation)
+ break;
+ list.appendChild(this.parseListElement(rootToken.indentation, isDefinitionList));
+ }
+
+ return list;
+ },
+
+ unknownDefinitionTerm: "???",
+
+ parseListElement: function (rootIndentation, isDefinitionList) {
+ var listElementToken = this.lexer.getNextToken();
+ var listElement = Node.createListElement([]);
+ this.setNodeOriginFromToken(listElement, listElementToken);
+
+ listElement.isDefinitionList = isDefinitionList;
+
+ if (isDefinitionList) {
+ var match = this.definitionPattern.exec(listElementToken.content);
+ listElement.term = [
+ this.createTextNode(match && match[1] ? match[1] : this.unknownDefinitionTerm)
+ ];
+ listElement.appendChild(this.createTextNode(match ? match[2] : listElementToken.content));
+ } else {
+ listElement.appendChild(this.createTextNode(listElementToken.content));
+ }
+
+ while (this.lexer.hasNext()) {
+ var blankToken = this.skipBlank();
+ if (!this.lexer.hasNext())
+ break;
+
+ var notBlankNextToken = this.lexer.peekNextToken();
+ if (blankToken && !notBlankNextToken.isListElement())
+ this.lexer.pushToken(blankToken); // Recover blank token only when next line is not listElement.
+ if (notBlankNextToken.indentation <= rootIndentation)
+ break; // end of the list
+
+ var element = this.parseElement(); // recursive
+ if (element)
+ listElement.appendChild(element);
+ }
+
+ return listElement;
+ },
+
+ // ------------------------------------------------------------
+ // <Table> ::= <TableRow>+
+ // ------------------------------------------------------------
+
+ parseTable: function () {
+ var nextToken = this.lexer.peekNextToken();
+ var table = Node.createTable([]);
+ this.setNodeOriginFromToken(table, nextToken);
+ var sawSeparator = false;
+
+ var allowMultilineCell = nextToken.type === Lexer.tokens.tableSeparator && this.options.multilineCell;
+
+ while (this.lexer.hasNext() &&
+ (nextToken = this.lexer.peekNextToken()).isTableElement()) {
+ if (nextToken.type === Lexer.tokens.tableRow) {
+ var tableRow = this.parseTableRow(allowMultilineCell);
+ table.appendChild(tableRow);
+ } else {
+ // Lexer.tokens.tableSeparator
+ sawSeparator = true;
+ this.lexer.getNextToken();
+ }
+ }
+
+ if (sawSeparator && table.children.length) {
+ table.children[0].children.forEach(function (cell) {
+ cell.isHeader = true;
+ });
+ }
+
+ return table;
+ },
+
+ // ------------------------------------------------------------
+ // <TableRow> ::= <TableCell>+
+ // ------------------------------------------------------------
+
+ parseTableRow: function (allowMultilineCell) {
+ var tableRowTokens = [];
+
+ while (this.lexer.peekNextToken().type === Lexer.tokens.tableRow) {
+ tableRowTokens.push(this.lexer.getNextToken());
+ if (!allowMultilineCell) {
+ break;
+ }
+ }
+
+ if (!tableRowTokens.length) {
+ throw this.createErrorReport("Expected table row");
+ }
+
+ var firstTableRowToken = tableRowTokens.shift();
+ var tableCellTexts = firstTableRowToken.content.split("|");
+
+ tableRowTokens.forEach(function (rowToken) {
+ rowToken.content.split("|").forEach(function (cellText, cellIdx) {
+ tableCellTexts[cellIdx] = (tableCellTexts[cellIdx] || "") + "\n" + cellText;
+ });
+ });
+
+ // TODO: Prepare two pathes: (1)
+ var tableCells = tableCellTexts.map(
+ // TODO: consider '|' escape?
+ function (text) {
+ return Node.createTableCell(Parser.parseStream(text));
+ }, this);
+
+ return this.setNodeOriginFromToken(Node.createTableRow(tableCells), firstTableRowToken);
+ },
+
+ // ------------------------------------------------------------
+ // <Directive> ::= "#+.*"
+ // ------------------------------------------------------------
+
+ parseDirective: function () {
+ var directiveToken = this.lexer.getNextToken();
+ var directiveNode = this.createDirectiveNodeFromToken(directiveToken);
+
+ if (directiveToken.endDirective)
+ throw this.createErrorReport("Unmatched 'end' directive for " + directiveNode.directiveName);
+
+ if (directiveToken.oneshotDirective) {
+ this.interpretDirective(directiveNode);
+ return directiveNode;
+ }
+
+ if (!directiveToken.beginDirective)
+ throw this.createErrorReport("Invalid directive " + directiveNode.directiveName);
+
+ // Parse begin ~ end
+ directiveNode.children = [];
+ if (this.isVerbatimDirective(directiveNode))
+ return this.parseDirectiveBlockVerbatim(directiveNode);
+ else
+ return this.parseDirectiveBlock(directiveNode);
+ },
+
+ createDirectiveNodeFromToken: function (directiveToken) {
+ var matched = /^[ ]*([^ ]*)[ ]*(.*)[ ]*$/.exec(directiveToken.content);
+
+ var directiveNode = Node.createDirective(null);
+ this.setNodeOriginFromToken(directiveNode, directiveToken);
+ directiveNode.directiveName = matched[1].toLowerCase();
+ directiveNode.directiveArguments = this.parseDirectiveArguments(matched[2]);
+ directiveNode.directiveOptions = this.parseDirectiveOptions(matched[2]);
+ directiveNode.directiveRawValue = matched[2];
+
+ return directiveNode;
+ },
+
+ isVerbatimDirective: function (directiveNode) {
+ var directiveName = directiveNode.directiveName;
+ return directiveName === "src" || directiveName === "example" || directiveName === "html";
+ },
+
+ parseDirectiveBlock: function (directiveNode, verbatim) {
+ this.lexer.pushDummyTokenByType(Lexer.tokens.blank);
+
+ while (this.lexer.hasNext()) {
+ var nextToken = this.lexer.peekNextToken();
+ if (nextToken.type === Lexer.tokens.directive &&
+ nextToken.endDirective &&
+ this.createDirectiveNodeFromToken(nextToken).directiveName === directiveNode.directiveName) {
+ // Close directive
+ this.lexer.getNextToken();
+ return directiveNode;
+ }
+ var element = this.parseElementBesidesDirectiveEnd();
+ if (element)
+ directiveNode.appendChild(element);
+ }
+
+ throw this.createErrorReport("Unclosed directive " + directiveNode.directiveName);
+ },
+
+ parseDirectiveBlockVerbatim: function (directiveNode) {
+ var textContent = [];
+
+ while (this.lexer.hasNext()) {
+ var nextToken = this.lexer.peekNextToken();
+ if (nextToken.type === Lexer.tokens.directive &&
+ nextToken.endDirective &&
+ this.createDirectiveNodeFromToken(nextToken).directiveName === directiveNode.directiveName) {
+ this.lexer.getNextToken();
+ directiveNode.appendChild(this.createTextNode(textContent.join("\n"), true));
+ return directiveNode;
+ }
+ textContent.push(this.lexer.stream.getNextLine());
+ }
+
+ throw this.createErrorReport("Unclosed directive " + directiveNode.directiveName);
+ },
+
+ parseDirectiveArguments: function (parameters) {
+ return parameters.split(/[ ]+/).filter(function (param) {
+ return param.length && param[0] !== "-";
+ });
+ },
+
+ parseDirectiveOptions: function (parameters) {
+ return parameters.split(/[ ]+/).filter(function (param) {
+ return param.length && param[0] === "-";
+ });
+ },
+
+ interpretDirective: function (directiveNode) {
+ // http://orgmode.org/manual/Export-options.html
+ switch (directiveNode.directiveName) {
+ case "options:":
+ this.interpretOptionDirective(directiveNode);
+ break;
+ case "title:":
+ this.document.title = directiveNode.directiveRawValue;
+ break;
+ case "author:":
+ this.document.author = directiveNode.directiveRawValue;
+ break;
+ case "email:":
+ this.document.email = directiveNode.directiveRawValue;
+ break;
+ default:
+ this.document.directiveValues[directiveNode.directiveName] = directiveNode.directiveRawValue;
+ break;
+ }
+ },
+
+ interpretOptionDirective: function (optionDirectiveNode) {
+ optionDirectiveNode.directiveArguments.forEach(function (pairString) {
+ var pair = pairString.split(":");
+ this.options[pair[0]] = this.convertLispyValue(pair[1]);
+ }, this);
+ },
+
+ convertLispyValue: function (lispyValue) {
+ switch (lispyValue) {
+ case "t":
+ return true;
+ case "nil":
+ return false;
+ default:
+ if (/^[0-9]+$/.test(lispyValue))
+ return parseInt(lispyValue);
+ return lispyValue;
+ }
+ },
+
+ // ------------------------------------------------------------
+ // <Paragraph> ::= <Blank> <Line>*
+ // ------------------------------------------------------------
+
+ parseParagraph: function () {
+ var paragraphFisrtToken = this.lexer.peekNextToken();
+ var paragraph = Node.createParagraph([]);
+ this.setNodeOriginFromToken(paragraph, paragraphFisrtToken);
+
+ var textContents = [];
+
+ while (this.lexer.hasNext()) {
+ var nextToken = this.lexer.peekNextToken();
+ if (nextToken.type !== Lexer.tokens.line
+ || nextToken.indentation < paragraphFisrtToken.indentation)
+ break;
+ this.lexer.getNextToken();
+ textContents.push(nextToken.content);
+ }
+
+ paragraph.appendChild(this.createTextNode(textContents.join("\n")));
+
+ return paragraph;
+ },
+
+ parseText: function (noEmphasis) {
+ var lineToken = this.lexer.getNextToken();
+ return this.createTextNode(lineToken.content, noEmphasis);
+ },
+
+ // ------------------------------------------------------------
+ // <Text> (DOM Like)
+ // ------------------------------------------------------------
+
+ createTextNode: function (text, noEmphasis) {
+ return noEmphasis ? Node.createText(null, { value: text })
+ : this.inlineParser.parseEmphasis(text);
+ }
+ };
+ Parser.prototype.originalParseElement = Parser.prototype.parseElement;
+
+ // ------------------------------------------------------------
+ // Parser for Inline Elements
+ //
+ // @refs org-emphasis-regexp-components
+ // ------------------------------------------------------------
+
+ function InlineParser() {
+ this.preEmphasis = " \t\\('\"";
+ this.postEmphasis = "- \t.,:!?;'\"\\)";
+ this.borderForbidden = " \t\r\n,\"'";
+ this.bodyRegexp = "[\\s\\S]*?";
+ this.markers = "*/_=~+";
+
+ this.emphasisPattern = this.buildEmphasisPattern();
+ this.linkPattern = /\[\[([^\]]*)\](?:\[([^\]]*)\])?\]/g; // \1 => link, \2 => text
+ }
+
+ InlineParser.prototype = {
+ parseEmphasis: function (text) {
+ var emphasisPattern = this.emphasisPattern;
+ emphasisPattern.lastIndex = 0;
+
+ var result = [],
+ match,
+ previousLast = 0,
+ savedLastIndex;
+
+ while ((match = emphasisPattern.exec(text))) {
+ var whole = match[0];
+ var pre = match[1];
+ var marker = match[2];
+ var body = match[3];
+ var post = match[4];
+
+ {
+ // parse links
+ var matchBegin = emphasisPattern.lastIndex - whole.length;
+ var beforeContent = text.substring(previousLast, matchBegin + pre.length);
+ savedLastIndex = emphasisPattern.lastIndex;
+ result.push(this.parseLink(beforeContent));
+ emphasisPattern.lastIndex = savedLastIndex;
+ }
+
+ var bodyNode = [Node.createText(null, { value: body })];
+ var bodyContainer = this.emphasizeElementByMarker(bodyNode, marker);
+ result.push(bodyContainer);
+
+ previousLast = emphasisPattern.lastIndex - post.length;
+ }
+
+ if (emphasisPattern.lastIndex === 0 ||
+ emphasisPattern.lastIndex !== text.length - 1)
+ result.push(this.parseLink(text.substring(previousLast)));
+
+ if (result.length === 1) {
+ // Avoid duplicated inline container wrapping
+ return result[0];
+ } else {
+ return Node.createInlineContainer(result);
+ }
+ },
+
+ depth: 0,
+ parseLink: function (text) {
+ var linkPattern = this.linkPattern;
+ linkPattern.lastIndex = 0;
+
+ var match,
+ result = [],
+ previousLast = 0,
+ savedLastIndex;
+
+ while ((match = linkPattern.exec(text))) {
+ var whole = match[0];
+ var src = match[1];
+ var title = match[2];
+
+ // parse before content
+ var matchBegin = linkPattern.lastIndex - whole.length;
+ var beforeContent = text.substring(previousLast, matchBegin);
+ result.push(Node.createText(null, { value: beforeContent }));
+
+ // parse link
+ var link = Node.createLink([]);
+ link.src = src;
+ if (title) {
+ savedLastIndex = linkPattern.lastIndex;
+ link.appendChild(this.parseEmphasis(title));
+ linkPattern.lastIndex = savedLastIndex;
+ } else {
+ link.appendChild(Node.createText(null, { value: src }));
+ }
+ result.push(link);
+
+ previousLast = linkPattern.lastIndex;
+ }
+
+ if (linkPattern.lastIndex === 0 ||
+ linkPattern.lastIndex !== text.length - 1)
+ result.push(Node.createText(null, { value: text.substring(previousLast) }));
+
+ return Node.createInlineContainer(result);
+ },
+
+ emphasizeElementByMarker: function (element, marker) {
+ switch (marker) {
+ case "*":
+ return Node.createBold(element);
+ case "/":
+ return Node.createItalic(element);
+ case "_":
+ return Node.createUnderline(element);
+ case "=":
+ case "~":
+ return Node.createCode(element);
+ case "+":
+ return Node.createDashed(element);
+ }
+ },
+
+ buildEmphasisPattern: function () {
+ return new RegExp(
+ "([" + this.preEmphasis + "]|^|\r?\n)" + // \1 => pre
+ "([" + this.markers + "])" + // \2 => marker
+ "([^" + this.borderForbidden + "]|" + // \3 => body
+ "[^" + this.borderForbidden + "]" +
+ this.bodyRegexp +
+ "[^" + this.borderForbidden + "])" +
+ "\\2" +
+ "([" + this.postEmphasis +"]|$|\r?\n)", // \4 => post
+ // flags
+ "g"
+ );
+ }
+ };
+
+ if (typeof exports !== "undefined") {
+ exports.Parser = Parser;
+ exports.InlineParser = InlineParser;
+ }
+
+ // var Node = require("../node.js").Node;
+
+ function Converter() {
+ }
+
+ Converter.prototype = {
+ exportOptions: {
+ headerOffset: 1,
+ exportFromLineNumber: false,
+ suppressSubScriptHandling: false,
+ suppressAutoLink: false,
+ // HTML
+ translateSymbolArrow: false,
+ suppressCheckboxHandling: false,
+ // { "directive:": function (node, childText, auxData) {} }
+ customDirectiveHandler: null,
+ // e.g., "org-js-"
+ htmlClassPrefix: null,
+ htmlIdPrefix: null
+ },
+
+ untitled: "Untitled",
+ result: null,
+
+ // TODO: Manage TODO lists
+
+ initialize: function (orgDocument, exportOptions) {
+ this.orgDocument = orgDocument;
+ this.documentOptions = orgDocument.options || {};
+ this.exportOptions = exportOptions || {};
+
+ this.headers = [];
+ this.headerOffset =
+ typeof this.exportOptions.headerOffset === "number" ? this.exportOptions.headerOffset : 1;
+ this.sectionNumbers = [0];
+ },
+
+ createTocItem: function (headerNode, parentTocs) {
+ var childTocs = [];
+ childTocs.parent = parentTocs;
+ var tocItem = { headerNode: headerNode, childTocs: childTocs };
+ return tocItem;
+ },
+
+ computeToc: function (exportTocLevel) {
+ if (typeof exportTocLevel !== "number")
+ exportTocLevel = Infinity;
+
+ var toc = [];
+ toc.parent = null;
+
+ var previousLevel = 1;
+ var currentTocs = toc; // first
+
+ for (var i = 0; i < this.headers.length; ++i) {
+ var headerNode = this.headers[i];
+
+ if (headerNode.level > exportTocLevel)
+ continue;
+
+ var levelDiff = headerNode.level - previousLevel;
+ if (levelDiff > 0) {
+ for (var j = 0; j < levelDiff; ++j) {
+ if (currentTocs.length === 0) {
+ // Create a dummy tocItem
+ var dummyHeader = Node.createHeader([], {
+ level: previousLevel + j
+ });
+ dummyHeader.sectionNumberText = "";
+ currentTocs.push(this.createTocItem(dummyHeader, currentTocs));
+ }
+ currentTocs = currentTocs[currentTocs.length - 1].childTocs;
+ }
+ } else if (levelDiff < 0) {
+ levelDiff = -levelDiff;
+ for (var k = 0; k < levelDiff; ++k) {
+ currentTocs = currentTocs.parent;
+ }
+ }
+
+ currentTocs.push(this.createTocItem(headerNode, currentTocs));
+
+ previousLevel = headerNode.level;
+ }
+
+ return toc;
+ },
+
+ convertNode: function (node, recordHeader, insideCodeElement) {
+ if (!insideCodeElement) {
+ if (node.type === Node.types.directive) {
+ if (node.directiveName === "example" ||
+ node.directiveName === "src") {
+ insideCodeElement = true;
+ }
+ } else if (node.type === Node.types.preformatted) {
+ insideCodeElement = true;
+ }
+ }
+
+ if (typeof node === "string") {
+ node = Node.createText(null, { value: node });
+ }
+
+ var childText = node.children ? this.convertNodesInternal(node.children, recordHeader, insideCodeElement) : "";
+ var text;
+
+ var auxData = this.computeAuxDataForNode(node);
+
+ switch (node.type) {
+ case Node.types.header:
+ // Parse task status
+ var taskStatus = null;
+ var _match = childText.match(/^[A-Z]{3,}\s+/);
+ if(_match && _match[0]){
+ taskStatus = _match[0].trim().toLowerCase();
+ }
+
+ // Compute section number
+ var sectionNumberText = null;
+ if (recordHeader) {
+ var thisHeaderLevel = node.level;
+ var previousHeaderLevel = this.sectionNumbers.length;
+ if (thisHeaderLevel > previousHeaderLevel) {
+ // Fill missing section number
+ var levelDiff = thisHeaderLevel - previousHeaderLevel;
+ for (var j = 0; j < levelDiff; ++j) {
+ this.sectionNumbers[thisHeaderLevel - 1 - j] = 0; // Extend
+ }
+ } else if (thisHeaderLevel < previousHeaderLevel) {
+ this.sectionNumbers.length = thisHeaderLevel; // Collapse
+ }
+ this.sectionNumbers[thisHeaderLevel - 1]++;
+ if (this.documentOptions.num)
+ sectionNumberText = this.sectionNumbers.join(".");
+ node.sectionNumberText = sectionNumberText; // Can be used in ToC
+ }
+
+ text = this.convertHeader(node, childText, auxData,
+ taskStatus, sectionNumberText);
+
+ if (recordHeader)
+ this.headers.push(node);
+ break;
+ case Node.types.orderedList:
+ text = this.convertOrderedList(node, childText, auxData);
+ break;
+ case Node.types.unorderedList:
+ text = this.convertUnorderedList(node, childText, auxData);
+ break;
+ case Node.types.definitionList:
+ text = this.convertDefinitionList(node, childText, auxData);
+ break;
+ case Node.types.listElement:
+ if (node.isDefinitionList) {
+ var termText = this.convertNodesInternal(node.term, recordHeader, insideCodeElement);
+ text = this.convertDefinitionItem(node, childText, auxData,
+ termText, childText);
+ } else {
+ text = this.convertListItem(node, childText, auxData);
+ }
+ break;
+ case Node.types.paragraph:
+ text = this.convertParagraph(node, childText, auxData);
+ break;
+ case Node.types.preformatted:
+ text = this.convertPreformatted(node, childText, auxData);
+ break;
+ case Node.types.table:
+ text = this.convertTable(node, childText, auxData);
+ break;
+ case Node.types.tableRow:
+ text = this.convertTableRow(node, childText, auxData);
+ break;
+ case Node.types.tableCell:
+ if (node.isHeader) {
+ if (childText)
+ text = this.convertTableHeader(node, childText, auxData);
+ } else
+ text = this.convertTableCell(node, childText, auxData);
+ break;
+ case Node.types.horizontalRule:
+ text = this.convertHorizontalRule(node, childText, auxData);
+ break;
+ // ============================================================ //
+ // Inline
+ // ============================================================ //
+ case Node.types.inlineContainer:
+ text = this.convertInlineContainer(node, childText, auxData);
+ break;
+ case Node.types.bold:
+ text = this.convertBold(node, childText, auxData);
+ break;
+ case Node.types.italic:
+ text = this.convertItalic(node, childText, auxData);
+ break;
+ case Node.types.underline:
+ text = this.convertUnderline(node, childText, auxData);
+ break;
+ case Node.types.code:
+ text = this.convertCode(node, childText, auxData);
+ break;
+ case Node.types.dashed:
+ text = this.convertDashed(node, childText, auxData);
+ break;
+ case Node.types.link:
+ text = this.convertLink(node, childText, auxData);
+ break;
+ case Node.types.property:
+ text = this.convertProperty(node, childText, auxData);
+ break;
+ case Node.types.directive:
+ switch (node.directiveName) {
+ case "quote":
+ text = this.convertQuote(node, childText, auxData);
+ break;
+ case "example":
+ text = this.convertExample(node, childText, auxData);
+ break;
+ case "src":
+ text = this.convertSrc(node, childText, auxData);
+ break;
+ case "html":
+ case "html:":
+ text = this.convertHTML(node, childText, auxData);
+ break;
+ default:
+ if (this.exportOptions.customDirectiveHandler &&
+ this.exportOptions.customDirectiveHandler[node.directiveName]) {
+ text = this.exportOptions.customDirectiveHandler[node.directiveName](
+ node, childText, auxData
+ );
+ } else {
+ text = childText;
+ }
+ }
+ break;
+ case Node.types.text:
+ text = this.convertText(node.value, insideCodeElement);
+ break;
+ default:
+ throw Error("Unknown node type: " + node.type);
+ }
+
+ if (typeof this.postProcess === "function") {
+ text = this.postProcess(node, text, insideCodeElement);
+ }
+
+ return text;
+ },
+
+ convertText: function (text, insideCodeElement) {
+ var escapedText = this.escapeSpecialChars(text, insideCodeElement);
+
+ if (!this.exportOptions.suppressSubScriptHandling && !insideCodeElement) {
+ escapedText = this.makeSubscripts(escapedText, insideCodeElement);
+ }
+ if (!this.exportOptions.suppressAutoLink) {
+ escapedText = this.linkURL(escapedText);
+ }
+
+ return escapedText;
+ },
+
+ // By default, ignore html
+ convertHTML: function (node, childText, auxData) {
+ return childText;
+ },
+
+ convertNodesInternal: function (nodes, recordHeader, insideCodeElement) {
+ var nodesTexts = [];
+ for (var i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+ var nodeText = this.convertNode(node, recordHeader, insideCodeElement);
+ nodesTexts.push(nodeText);
+ }
+ return this.combineNodesTexts(nodesTexts);
+ },
+
+ convertHeaderBlock: function (headerBlock, recordHeader) {
+ throw Error("convertHeaderBlock is not implemented");
+ },
+
+ convertHeaderTree: function (headerTree, recordHeader) {
+ return this.convertHeaderBlock(headerTree, recordHeader);
+ },
+
+ convertNodesToHeaderTree: function (nodes, nextBlockBegin, blockHeader) {
+ var childBlocks = [];
+ var childNodes = [];
+
+ if (typeof nextBlockBegin === "undefined") {
+ nextBlockBegin = 0;
+ }
+ if (typeof blockHeader === "undefined") {
+ blockHeader = null;
+ }
+
+ for (var i = nextBlockBegin; i < nodes.length;) {
+ var node = nodes[i];
+
+ var isHeader = node.type === Node.types.header;
+
+ if (!isHeader) {
+ childNodes.push(node);
+ i = i + 1;
+ continue;
+ }
+
+ // Header
+ if (blockHeader && node.level <= blockHeader.level) {
+ // Finish Block
+ break;
+ } else {
+ // blockHeader.level < node.level
+ // Begin child block
+ var childBlock = this.convertNodesToHeaderTree(nodes, i + 1, node);
+ childBlocks.push(childBlock);
+ i = childBlock.nextIndex;
+ }
+ }
+
+ // Finish block
+ return {
+ header: blockHeader,
+ childNodes: childNodes,
+ nextIndex: i,
+ childBlocks: childBlocks
+ };
+ },
+
+ convertNodes: function (nodes, recordHeader, insideCodeElement) {
+ return this.convertNodesInternal(nodes, recordHeader, insideCodeElement);
+ },
+
+ combineNodesTexts: function (nodesTexts) {
+ return nodesTexts.join("");
+ },
+
+ getNodeTextContent: function (node) {
+ if (node.type === Node.types.text)
+ return this.escapeSpecialChars(node.value);
+ else
+ return node.children ? node.children.map(this.getNodeTextContent, this).join("") : "";
+ },
+
+ // @Override
+ escapeSpecialChars: function (text) {
+ throw Error("Implement escapeSpecialChars");
+ },
+
+ // http://daringfireball.net/2010/07/improved_regex_for_matching_urls
+ urlPattern: /\b(?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])/ig,
+
+ // @Override
+ linkURL: function (text) {
+ var self = this;
+ return text.replace(this.urlPattern, function (matched) {
+ if (matched.indexOf("://") < 0)
+ matched = "http://" + matched;
+ return self.makeLink(matched);
+ });
+ },
+
+ makeLink: function (url) {
+ throw Error("Implement makeLink");
+ },
+
+ makeSubscripts: function (text) {
+ if (this.documentOptions["^"] === "{}")
+ return text.replace(/\b([^_ \t]*)_{([^}]*)}/g,
+ this.makeSubscript);
+ else if (this.documentOptions["^"])
+ return text.replace(/\b([^_ \t]*)_([^_]*)\b/g,
+ this.makeSubscript);
+ else
+ return text;
+ },
+
+ makeSubscript: function (match, body, subscript) {
+ throw Error("Implement makeSubscript");
+ },
+
+ stripParametersFromURL: function (url) {
+ return url.replace(/\?.*$/, "");
+ },
+
+ imageExtensionPattern: new RegExp("(" + [
+ "bmp", "png", "jpeg", "jpg", "gif", "tiff",
+ "tif", "xbm", "xpm", "pbm", "pgm", "ppm", "svg"
+ ].join("|") + ")$", "i")
+ };
+
+ if (typeof exports !== "undefined")
+ exports.Converter = Converter;
+
+ // var Converter = require("./converter.js").Converter;
+ // var Node = require("../node.js").Node;
+
+ function ConverterHTML(orgDocument, exportOptions) {
+ this.initialize(orgDocument, exportOptions);
+ this.result = this.convert();
+ }
+
+ ConverterHTML.prototype = {
+ __proto__: Converter.prototype,
+
+ convert: function () {
+ var title = this.orgDocument.title ? this.convertNode(this.orgDocument.title) : this.untitled;
+ var titleHTML = this.tag("h" + Math.max(Number(this.headerOffset), 1), title);
+ var contentHTML = this.convertNodes(this.orgDocument.nodes, true /* record headers */);
+ var toc = '';
+ var tocHTML = '';
+ if (this.documentOptions.toc) {
+ toc = this.computeToc(this.documentOptions["toc"]);
+ tocHTML = this.tocToHTML(toc);
+ }
+ return {
+ title: title,
+ titleHTML: titleHTML,
+ contentHTML: contentHTML,
+ tocHTML: tocHTML,
+ toc: toc,
+ toString: function () {
+ return titleHTML + tocHTML + "\n" + contentHTML;
+ }
+ };
+ },
+
+ tocToHTML: function (toc) {
+ function tocToHTMLFunction(tocList) {
+ var html = "";
+ for (var i = 0; i < tocList.length; ++i) {
+ var tocItem = tocList[i];
+ var sectionNumberText = tocItem.headerNode.sectionNumberText;
+ var sectionNumber = this.documentOptions.num ?
+ this.inlineTag("span", sectionNumberText, {
+ "class": "section-number"
+ }) : "";
+ var header = this.getNodeTextContent(tocItem.headerNode);
+ var headerLink = this.inlineTag("a", sectionNumber + header, {
+ href: "#header-" + sectionNumberText.replace(/\./g, "-")
+ });
+ var subList = tocItem.childTocs.length ? tocToHTMLFunction.call(this, tocItem.childTocs) : "";
+ html += this.tag("li", headerLink + subList);
+ }
+ return this.tag("ul", html);
+ }
+
+ return tocToHTMLFunction.call(this, toc);
+ },
+
+ computeAuxDataForNode: function (node) {
+ while (node.parent &&
+ node.parent.type === Node.types.inlineContainer) {
+ node = node.parent;
+ }
+ var attributesNode = node.previousSibling;
+ var attributesText = "";
+ while (attributesNode &&
+ attributesNode.type === Node.types.directive &&
+ attributesNode.directiveName === "attr_html:") {
+ attributesText += attributesNode.directiveRawValue + " ";
+ attributesNode = attributesNode.previousSibling;
+ }
+ return attributesText;
+ },
+
+ // Method to construct org-js generated class
+ orgClassName: function (className) {
+ return this.exportOptions.htmlClassPrefix ?
+ this.exportOptions.htmlClassPrefix + className
+ : className;
+ },
+
+ // Method to construct org-js generated id
+ orgId: function (id) {
+ return this.exportOptions.htmlIdPrefix ?
+ this.exportOptions.htmlIdPrefix + id
+ : id;
+ },
+
+ // ----------------------------------------------------
+ // Node conversion
+ // ----------------------------------------------------
+
+ convertHeader: function (node, childText, auxData,
+ taskStatus, sectionNumberText) {
+ var headerAttributes = {};
+
+ if (taskStatus) {
+ childText = this.inlineTag("span", childText.substring(0, 4), {
+ "class": "task-status " + taskStatus
+ }) + childText.substring(5);
+ }
+
+ if (sectionNumberText) {
+ childText = this.inlineTag("span", sectionNumberText, {
+ "class": "section-number"
+ }) + childText;
+ headerAttributes["id"] = "header-" + sectionNumberText.replace(/\./g, "-");
+ }
+
+ if (taskStatus)
+ headerAttributes["class"] = "task-status " + taskStatus;
+
+ return this.tag("h" + (this.headerOffset + node.level),
+ childText, headerAttributes, auxData);
+ },
+
+ convertOrderedList: function (node, childText, auxData) {
+ return this.tag("ol", childText, null, auxData);
+ },
+
+ convertUnorderedList: function (node, childText, auxData) {
+ return this.tag("ul", childText, null, auxData);
+ },
+
+ convertDefinitionList: function (node, childText, auxData) {
+ return this.tag("dl", childText, null, auxData);
+ },
+
+ convertDefinitionItem: function (node, childText, auxData,
+ term, definition) {
+ return this.tag("dt", term) + this.tag("dd", definition);
+ },
+
+ convertListItem: function (node, childText, auxData) {
+ if (this.exportOptions.suppressCheckboxHandling) {
+ return this.tag("li", childText, null, auxData);
+ } else {
+ var listItemAttributes = {};
+ var listItemText = childText;
+ // Embed checkbox
+ if (/^\s*\[(X| |-)\]([\s\S]*)/.exec(listItemText)) {
+ listItemText = RegExp.$2 ;
+ var checkboxIndicator = RegExp.$1;
+
+ var checkboxAttributes = { type: "checkbox" };
+ switch (checkboxIndicator) {
+ case "X":
+ checkboxAttributes["checked"] = "true";
+ listItemAttributes["data-checkbox-status"] = "done";
+ break;
+ case "-":
+ listItemAttributes["data-checkbox-status"] = "intermediate";
+ break;
+ default:
+ listItemAttributes["data-checkbox-status"] = "undone";
+ break;
+ }
+
+ listItemText = this.inlineTag("input", null, checkboxAttributes) + listItemText;
+ }
+
+ return this.tag("li", listItemText, listItemAttributes, auxData);
+ }
+ },
+
+ convertParagraph: function (node, childText, auxData) {
+ return this.tag("p", childText, null, auxData);
+ },
+
+ convertPreformatted: function (node, childText, auxData) {
+ return this.tag("pre", childText, null, auxData);
+ },
+
+ convertTable: function (node, childText, auxData) {
+ return this.tag("table", this.tag("tbody", childText), null, auxData);
+ },
+
+ convertTableRow: function (node, childText, auxData) {
+ return this.tag("tr", childText);
+ },
+
+ convertTableHeader: function (node, childText, auxData) {
+ return this.tag("th", childText);
+ },
+
+ convertTableCell: function (node, childText, auxData) {
+ return this.tag("td", childText);
+ },
+
+ convertHorizontalRule: function (node, childText, auxData) {
+ return this.tag("hr", null, null, auxData);
+ },
+
+ convertInlineContainer: function (node, childText, auxData) {
+ return childText;
+ },
+
+ convertBold: function (node, childText, auxData) {
+ return this.inlineTag("b", childText);
+ },
+
+ convertItalic: function (node, childText, auxData) {
+ return this.inlineTag("i", childText);
+ },
+
+ convertUnderline: function (node, childText, auxData) {
+ return this.inlineTag("span", childText, {
+ style: "text-decoration:underline;"
+ });
+ },
+
+ convertCode: function (node, childText, auxData) {
+ return this.inlineTag("code", childText);
+ },
+
+ convertDashed: function (node, childText, auxData) {
+ return this.inlineTag("del", childText);
+ },
+
+ convertLink: function (node, childText, auxData) {
+ var srcParameterStripped = this.stripParametersFromURL(node.src);
+ if (this.imageExtensionPattern.exec(srcParameterStripped)) {
+ var imgText = this.getNodeTextContent(node);
+ return this.inlineTag("img", null, {
+ src: node.src,
+ alt: imgText,
+ title: imgText
+ }, auxData);
+ } else {
+ return this.inlineTag("a", childText, { href: node.src });
+ }
+ },
+
+ convertQuote: function (node, childText, auxData) {
+ return this.tag("blockquote", childText, null, auxData);
+ },
+
+ convertExample: function (node, childText, auxData) {
+ return this.tag("pre", childText, null, auxData);
+ },
+
+ convertSrc: function (node, childText, auxData) {
+ var codeLanguage = node.directiveArguments.length
+ ? node.directiveArguments[0]
+ : "unknown";
+ childText = this.tag("code", childText, {
+ "class": "language-" + codeLanguage
+ }, auxData);
+ return this.tag("pre", childText, {
+ "class": "prettyprint"
+ });
+ },
+
+ // @override
+ convertHTML: function (node, childText, auxData) {
+ if (node.directiveName === "html:") {
+ return node.directiveRawValue;
+ } else if (node.directiveName === "html") {
+ return node.children.map(function (textNode) {
+ return textNode.value;
+ }).join("\n");
+ } else {
+ return childText;
+ }
+ },
+
+ // @implement
+ convertHeaderBlock: function (headerBlock, level, index) {
+ level = level || 0;
+ index = index || 0;
+
+ var contents = [];
+
+ var headerNode = headerBlock.header;
+ if (headerNode) {
+ contents.push(this.convertNode(headerNode));
+ }
+
+ var blockContent = this.convertNodes(headerBlock.childNodes);
+ contents.push(blockContent);
+
+ var childBlockContent = headerBlock.childBlocks
+ .map(function (block, idx) {
+ return this.convertHeaderBlock(block, level + 1, idx);
+ }, this)
+ .join("\n");
+ contents.push(childBlockContent);
+
+ var contentsText = contents.join("\n");
+
+ if (headerNode) {
+ return this.tag("section", "\n" + contents.join("\n"), {
+ "class": "block block-level-" + level
+ });
+ } else {
+ return contentsText;
+ }
+ },
+
+ // ----------------------------------------------------
+ // Supplemental methods
+ // ----------------------------------------------------
+
+ replaceMap: {
+ // [replacing pattern, predicate]
+ "&": ["&", null],
+ "<": ["<", null],
+ ">": [">", null],
+ '"': [""", null],
+ "'": ["'", null],
+ "->": ["➔", function (text, insideCodeElement) {
+ return this.exportOptions.translateSymbolArrow && !insideCodeElement;
+ }]
+ },
+
+ replaceRegexp: null,
+
+ // @implement @override
+ escapeSpecialChars: function (text, insideCodeElement) {
+ if (!this.replaceRegexp) {
+ this.replaceRegexp = new RegExp(Object.keys(this.replaceMap).join("|"), "g");
+ }
+
+ var replaceMap = this.replaceMap;
+ var self = this;
+ return text.replace(this.replaceRegexp, function (matched) {
+ if (!replaceMap[matched]) {
+ throw Error("escapeSpecialChars: Invalid match");
+ }
+
+ var predicate = replaceMap[matched][1];
+ if (typeof predicate === "function" &&
+ !predicate.call(self, text, insideCodeElement)) {
+ // Not fullfill the predicate
+ return matched;
+ }
+
+ return replaceMap[matched][0];
+ });
+ },
+
+ // @implement
+ postProcess: function (node, currentText, insideCodeElement) {
+ if (this.exportOptions.exportFromLineNumber &&
+ typeof node.fromLineNumber === "number") {
+ // Wrap with line number information
+ currentText = this.inlineTag("div", currentText, {
+ "data-line-number": node.fromLineNumber
+ });
+ }
+ return currentText;
+ },
+
+ // @implement
+ makeLink: function (url) {
+ return "<a href=\"" + url + "\">" + decodeURIComponent(url) + "</a>";
+ },
+
+ // @implement
+ makeSubscript: function (match, body, subscript) {
+ return "<span class=\"org-subscript-parent\">" +
+ body +
+ "</span><span class=\"org-subscript-child\">" +
+ subscript +
+ "</span>";
+ },
+
+ // ----------------------------------------------------
+ // Specific methods
+ // ----------------------------------------------------
+
+ attributesObjectToString: function (attributesObject) {
+ var attributesString = "";
+ for (var attributeName in attributesObject) {
+ if (attributesObject.hasOwnProperty(attributeName)) {
+ var attributeValue = attributesObject[attributeName];
+ // To avoid id/class name conflicts with other frameworks,
+ // users can add arbitrary prefix to org-js generated
+ // ids/classes via exportOptions.
+ if (attributeName === "class") {
+ attributeValue = this.orgClassName(attributeValue);
+ } else if (attributeName === "id") {
+ attributeValue = this.orgId(attributeValue);
+ }
+ attributesString += " " + attributeName + "=\"" + attributeValue + "\"";
+ }
+ }
+ return attributesString;
+ },
+
+ inlineTag: function (name, innerText, attributesObject, auxAttributesText) {
+ attributesObject = attributesObject || {};
+
+ var htmlString = "<" + name;
+ // TODO: check duplicated attributes
+ if (auxAttributesText)
+ htmlString += " " + auxAttributesText;
+ htmlString += this.attributesObjectToString(attributesObject);
+
+ if (innerText === null)
+ return htmlString + "/>";
+
+ htmlString += ">" + innerText + "</" + name + ">";
+
+ return htmlString;
+ },
+
+ tag: function (name, innerText, attributesObject, auxAttributesText) {
+ return this.inlineTag(name, innerText, attributesObject, auxAttributesText) + "\n";
+ }
+ };
+
+ if (typeof exports !== "undefined")
+ exports.ConverterHTML = ConverterHTML;
+
+ return exports;
+})();
+
+Object.assign(exports, Org)
diff --git a/_org/org2html.js b/_org/org2html.js
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+
+var org = require("./org");
+var parser = new org.Parser();
+
+process.stdin.resume();
+process.stdin.setEncoding('utf8');
+
+var orgCode = "";
+process.stdin.on('data', function (chunk) {
+ orgCode += chunk;
+});
+
+process.stdin.on('end', function () {
+ parseAndOutputHTML();
+});
+
+function parseAndOutputHTML() {
+ var orgDocument = parser.parse(orgCode);
+ var orgHTMLDocument = orgDocument.convert(org.ConverterHTML, {});
+ console.log(orgHTMLDocument.toString());
+}
diff --git a/_org/style.css b/_org/style.css
@@ -0,0 +1,158 @@
+@media print {
+ .noprint {
+ display: none;
+ }
+}
+@media screen {
+ .noscreen {
+ display: none;
+ }
+}
+html {
+ font-family: arial;
+ font-size: 12pt;
+}
+#body > h1 {
+ margin-top: 64px;
+ text-align: center;
+ margin-bottom: 64px;
+}
+.logo {
+ position: absolute;
+ right: 32px;
+ top: 8px;
+ margin-bottom: 100px;
+}
+li {
+ list-style-type: none;
+}
+.section-number {
+ color: #9d80b3;
+ padding-right: 10pt;
+}
+h1 {
+ color: #2c0449;
+ font-size: 24pt;
+}
+h2 {
+ color: #2c0449;
+ font-size: 20pt;
+}
+h3 {
+ color: #2c0449;
+ font-size: 16pt;
+}
+.side-title {
+ color: #2c0449;
+ font-size: 16pt;
+ font-weight: bold;
+}
+:link {
+ border-bottom: 1px dotted #65427e;
+ color: #2c0449;
+ text-decoration: none;
+}
+:visited {
+ border-bottom: 1px dotted #65427e;
+ color: #9d80b3;
+ text-decoration: none;
+}
+:link:hover,
+:visited:hover {
+ color: #65427e;
+}
+b {
+ color: #2c0449;
+}
+code {
+ background-color: #f4e7fd;
+ border: 1px solid #6a4d7f;
+ border-radius: 1pt;
+ font-size: 90%;
+}
+pre code {
+ background-color: none;
+ border: none;
+}
+pre {
+ background-color: #f4e7fd;
+ font-family: courier, monospace;
+ font-size: 90%;
+ overflow: auto;
+ border: 1px solid #6a4d7f;
+ border-radius: 4pt;
+ box-shadow: 2px 2px 3px #9d80b3;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 10pt;
+ margin-top: 15pt;
+ padding: 5pt;
+}
+textarea {
+ overflow-x: auto;
+}
+img {
+ border: 1px solid #6a4d7f;
+ border-radius: 4pt;
+ box-shadow: 2px 2px 3px #9d80b3;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 10pt;
+ margin-top: 15pt;
+ padding: 5pt;
+}
+.noshadow {
+ border: none;
+ box-shadow: none;
+}
+dt {
+ font-weight: bold;
+}
+table {
+ background: #ebfaf9;
+ border: 1px solid #6a4d7f;
+ border-radius: 4pt;
+ box-shadow: 2px 2px 3px #9d80b3;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 10pt;
+ margin-top: 15pt;
+ padding: 5pt;
+}
+.root {
+ border-bottom: none;
+}
+th {
+ background: #d1faf7;
+ border: 1px solid #75f0e8;
+ border-radius: 1pt;
+ padding: 5px;
+ vertical-align: top;
+}
+td {
+ background: #ebfaf9;
+ border: 1px solid #99e5e0;
+ border-radius: 1pt;
+ font-family: courier, monospace;
+ padding: 2px;
+ vertical-align: top;
+}
+#src_file {
+ padding: 8px;
+ float: right;
+ font-size: 10px;
+ color: #ccc;
+}
+.task-status {
+ margin-right: 4px;
+ padding-left: 4px;
+ padding-right: 4px;
+}
+.task-status .todo {
+ background: red;
+ color: white;
+}
+.task-status .done {
+ background: green;
+ color: white;
+}
diff --git a/_org/style.less b/_org/style.less
@@ -0,0 +1,208 @@
+// Paramètres principaux
+// ---------------------
+
+@main-font-size: 12pt;
+@main-hue: 275;
+@table-hue: 176;
+
+.border(@color, @radius: 1pt) {
+ border: 1px solid darken(@color, 20%);
+ border-radius: @radius;
+}
+
+.header(@level) {
+ color: @dark-color;
+ font-size: @main-font-size + (4 - @level) * 4pt;
+}
+
+.spacing() {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 10pt;
+ margin-top: 15pt;
+ padding: 5pt;
+}
+
+.shadow() {
+ .border(@light-color, 4pt);
+ box-shadow: 2px 2px 3px @light-color;
+ .spacing();
+}
+
+// Paramètres dérivés
+// ------------------
+
+@dark-color: hsl(@main-hue, 90%, 15%);
+@light-color: hsl(@main-hue, 25%, 60%);
+@mixed-color: mix(@dark-color, @light-color);
+@background-color: hsl(@main-hue, 85%, 95%);
+
+@table-color: hsl(@table-hue, 60%, 95%);
+@title-color: hsl(@table-hue, 80%, 90%);
+
+// Disposition générale
+// --------------------
+
+@media print { .noprint { display: none; } }
+@media screen { .noscreen { display: none; } }
+
+html {
+ font-family: arial;
+ font-size: @main-font-size;
+}
+
+
+// Title
+#body > h1 {
+ margin-top: 64px;
+ text-align: center;
+ margin-bottom: 64px;
+}
+
+.logo {
+ position: absolute;
+ right: 32px;
+ top: 8px;
+ margin-bottom: 100px;
+}
+
+li { list-style-type: none; }
+// Titres
+// ------
+
+.section-number {
+ color: @light-color;
+ padding-right: 10pt;
+}
+
+h1 {
+ .header(1);
+}
+
+h2 {
+ .header(2);
+}
+
+h3 {
+ .header(3);
+}
+
+.side-title {
+ .header(3);
+ font-weight: bold;
+}
+
+// Liens
+// -----
+
+:link {
+ border-bottom: 1px dotted @mixed-color;
+ color: @dark-color;
+ text-decoration: none;
+}
+
+:visited {
+ border-bottom: 1px dotted @mixed-color;
+ color: @light-color;
+ text-decoration: none;
+}
+
+:link:hover, :visited:hover {
+ color: @mixed-color;
+ }
+
+// Exemples
+// --------
+
+b {
+ color: @dark-color;
+}
+
+code {
+ background-color: @background-color;
+ .border(@light-color);
+ font-size: 90%;
+}
+
+pre code {
+ background-color: none;
+ border: none;
+}
+
+pre {
+ background-color: @background-color;
+ font-family: courier, monospace;
+ font-size: 90%;
+ overflow: auto;
+ .shadow();
+}
+
+textarea {
+ overflow-x: auto;
+}
+
+// Images
+// ------
+
+img {
+ .shadow();
+}
+
+.noshadow {
+ border: none;
+ box-shadow: none;
+}
+
+// Enumérations
+// ------------
+
+dt {
+ font-weight: bold;
+}
+
+// Tables
+// ------
+
+table {
+ background: @table-color;
+ .shadow();
+}
+.root {
+ border-bottom: none;
+}
+th {
+ background: @title-color;
+ .border(@title-color);
+ padding: 5px;
+ vertical-align: top;
+}
+
+td {
+ background: @table-color;
+ .border(@table-color);
+ font-family: courier, monospace;
+ padding: 2px;
+ vertical-align: top;
+}
+
+
+#src_file {
+ padding: 8px;
+ float: right;
+ font-size:10px;
+ color: #ccc;
+}
+
+.task-status {
+ margin-right: 4px;
+ padding-left: 4px;
+ padding-right: 4px;
+ .todo {
+ background: red;
+ color: white;
+ }
+ .done {
+ background: green;
+ color: white;
+ }
+}
diff --git a/cadre/__init__.py b/cadre/__init__.py
@@ -0,0 +1,80 @@
+import sys
+import configparser
+import traceback
+import logging
+import logging.config
+from cadre.controllers import Controller, load_controllers
+from cadre.framework import Framework, DumpRoutes
+from cadre.framework import framework_from_config, validate
+
+logger = logging.getLogger(__name__)
+
+
+class NotReady:
+ def __init__(self, diagnostic):
+ self.diagnostic = diagnostic
+
+ def __getattr__(self, attribute):
+ raise AttributeError("Cannot access %r: %s" %
+ (attribute, self.diagnostic))
+
+ def __getitem__(self, item):
+ raise AttributeError("Cannot find %r: %s" % (item, self.diagnostic))
+
+ def __bool__(self):
+ return False
+
+
+request = cookies = NotReady("Cadre is not setup yet.")
+
+
+class Cadre():
+
+ config = {}
+
+ def __init__(self, config_filename):
+ self.application = getattr(self, 'application',
+ self.__class__.__name__.lower())
+ logger.debug("Starting cadre application '%s'." % self.application)
+ self.config_filename = config_filename
+ if self.config_filename:
+ self.config.update(self.read_config(self.config_filename))
+ try:
+ logging.config.fileConfig(self.config_filename)
+ except Exception:
+ print("You'd better configure logging in '%s':\n%s" %
+ (self.config_filename, traceback.format_exc()))
+ setattr(sys.modules['cadre'], 'config', self.config)
+
+ def read_config(self, filename):
+ config = {self.__class__.__name__.lower(): {}}
+ p = configparser.ConfigParser()
+ p.read(filename)
+ for n, s in p.items():
+ if s:
+ config[n] = dict(s)
+ return config
+
+ def serve(self):
+ assert self.config, "Missing config."
+ f = framework_from_config(self.__class__.__name__.lower(), self.config)
+ f.setup_application(self)
+ load_controllers(f, self.application)
+ f.serve()
+
+ def dump_routes(self):
+ assert self.config, "Missing config."
+ f = DumpRoutes(self.__class__.__name__.lower(), self.config)
+ load_controllers(f, self.application)
+ print(f)
+
+ def get_wsgi_app(self):
+ config = self.read_config(self.config_filename)
+ assert config, "Missing config."
+ f = framework_from_config(self.__class__.__name__.lower(), config)
+ f.setup_application(self)
+ load_controllers(f, self.application)
+ return f.get_wsgi_app()
+
+
+__all__ = ['Cadre', 'Framework', 'Controller', 'validate']
diff --git a/cadre/controllers.py b/cadre/controllers.py
@@ -0,0 +1,135 @@
+import traceback
+import types
+import logging
+import unittest
+import pkg_resources
+from cadre.framework import Framework
+
+logger = logging.getLogger(__name__)
+
+
+class Controller():
+
+ version = 0
+
+ @classmethod
+ def setup(cls, framework, root):
+ methods = []
+ cls.framework = framework
+ ctrl_name = cls.__name__.lower()
+ version = ctrl_name.split('_')[-1]
+ if version.isdigit():
+ ctrl_name = ctrl_name[:-(len(version) + 1)]
+ version = int(version)
+ else:
+ version = framework.first_version
+
+ for name in [a for a in dir(cls) if a[0] != '_']:
+ attr = getattr(cls, name)
+ if isinstance(attr, types.FunctionType) and \
+ not name.startswith('test_'):
+ verbs = ''.join([c for c in name if c.isupper() or c == '_'
+ ]).lstrip('_').split('_')
+ if verbs != ['']:
+ name = name[:name.find("_%s" % verbs[0])]
+ if name == 'index__':
+ if root:
+ if name == 'index__':
+ framework.root_id = id(attr)
+ else:
+ root_id = getattr(framework, 'root_id', None)
+ # Prevent inheritance of index__
+ if root_id == id(attr):
+ continue
+ methods.append(
+ (name, attr,
+ attr.__code__.co_varnames[1:attr.__code__.co_argcount],
+ verbs if verbs != [''] else ['GET']))
+ framework.add_controller((cls, '__root__' if root else ctrl_name,
+ version), methods)
+
+
+def setup_controllers(cls, f, level=0):
+ if cls.__name__ != 'Controller': # exclude abstract
+ cls.setup(f, root=level < 2)
+ for sub in cls.__subclasses__():
+ setup_controllers(sub, f, level + 1)
+
+
+def load_controllers(f, application):
+ for ep in pkg_resources.iter_entry_points('%s.controllers' % application):
+ try:
+ ep.load()
+ except ModuleNotFoundError:
+ logger.error("Error importing '%s': %s" % (
+ ep, traceback.format_exc()))
+ for ep in pkg_resources.iter_entry_points('cadre.controllers'):
+ try:
+ ep.load()
+ except ModuleNotFoundError:
+ logger.error("Error importing '%s': %s" % (
+ ep, traceback.format_exc()))
+ setup_controllers(Controller, f)
+
+
+class CadreTestCase(unittest.TestCase):
+ def setUp(self):
+ self.framework._json = {}
+ self.framework._environ = {}
+ self.framework._headers = {}
+ self.framework._args = {}
+ self.framework._cookies = {}
+
+ def tearDown(self):
+ pass
+
+
+class TestFramework(Framework):
+ def request_json(self):
+ return self._json
+
+ def request_environ(self):
+
+ return self._environ
+
+ def request_headers(self):
+ return self._headers
+
+ def request_args(self):
+ return self._args
+
+ def get_cookie(self, key):
+ return self._cookies.get(key)
+
+ def set_cookie(self, key, value): # pylint: disable=W0221
+ self._cookies[key] = value
+
+ def send_bytes(self, bytes_like, **kwargs): # pylint: disable=W0613
+ return bytes_like.read()
+
+
+def get_controllers_tsts(app):
+ f = TestFramework(app.application, None)
+ load_controllers(f, app.application)
+
+ def missing_test(self):
+ raise Exception("Missing test")
+
+ for c in f.controllers:
+ for v in f.controllers[c]:
+ cls = f.controllers[c][v]['cls']
+ attributes = {}
+ for name in [a for a in dir(cls) if a[0] != '_']:
+ attr = getattr(cls, name)
+ if not name.startswith('test_'):
+ attributes[name] = attr
+ if isinstance(attr, types.FunctionType) and \
+ not name.startswith('test_'):
+ test_name = 'test_%s' % name
+ test_long_name = 'test_%s_%s' % (cls.__name__, name)
+ test_fct = getattr(cls, test_name, missing_test)
+ attributes[test_long_name] = test_fct
+ test_cls = type("Test%s" % cls.__name__,
+ (getattr(cls, 'testcase_cls', CadreTestCase), ),
+ attributes)
+ yield (test_cls.__name__, test_cls)
diff --git a/cadre/framework.py b/cadre/framework.py
@@ -0,0 +1,187 @@
+import sys
+import logging
+import pkg_resources
+import traceback
+from decorator import decorator
+
+logger = logging.getLogger(__name__)
+ENI = Exception("Not implemented.")
+
+
+class Cookies():
+ def __init__(self, framework):
+ self.f = framework
+
+ def get(self, key):
+ return self.f.get_cookie(key)
+
+ def set(self, *args, **kwargs):
+ return self.f.set_cookie(*args, **kwargs)
+
+
+class Request():
+ def __init__(self, framework):
+ self.f = framework
+ self._json = None
+ self._environ = None
+ self._headers = None
+ self._args = None
+
+ def _get_json(self):
+ return self._json or self.f.request_json()
+
+ # Let unit tests set some json
+ def _set_json(self, json):
+ self._json = json
+
+ json = property(_get_json, _set_json)
+
+ def _get_environ(self):
+ return self._environ or self.f.request_environ()
+
+ # Let unit tests set environ
+ def _set_environ(self, environ):
+ self._environ = environ
+
+ environ = property(_get_environ, _set_environ)
+
+ def _get_headers(self):
+ return self._headers or self.f.request_headers()
+
+ # Let unit tests set environ
+ def _set_headers(self, headers):
+ self._headers = headers
+
+ headers = property(_get_headers, _set_headers)
+
+ def _get_args(self):
+ return self._args or self.f.request_args()
+
+ # Let unit tests set environ
+ def _set_args(self, args):
+ self._args = args
+
+ args = property(_get_args, _set_args)
+
+
+def validate(vargs):
+ def helper(func, *args, **kwargs):
+ if hasattr(args[0].framework, 'validate_query'):
+ args[0].framework.validate_query(vargs)
+ return func(*args, **kwargs)
+
+ return decorator(helper)
+
+
+class Framework():
+
+ request_class = Request
+ cookies_class = Cookies
+
+ def __init__(self, application, config):
+ self.application = application
+ self.config = config
+ self.first_version = int((self.config or {}).get(application, {}).get(
+ 'first_version', '0'))
+ if self.first_version:
+ logger.debug("Config set 'first_version' to %s." %
+ self.first_version)
+ self.versioning_use_prefix = (self.config or {}).get(
+ application, {}).get('versioning', 'headers').lower() == 'prefix'
+ if self.versioning_use_prefix:
+ logger.debug("Config set versioning to 'prefix' mode.")
+ self.routes = {}
+ self.controllers = {}
+ self.request = self.request_class(self)
+ setattr(sys.modules['cadre'], 'request', self.request)
+ self.cookies = self.cookies_class(self)
+ setattr(sys.modules['cadre'], 'cookies', self.cookies)
+
+ def add_controller(self, ctrl, methods):
+ ctrl_cls, ctrl_name, ctrl_version = ctrl
+ self.controllers.setdefault(ctrl_name, {})
+ self.controllers[ctrl_name][ctrl_version] = {
+ 'doc': (ctrl_cls.__doc__ or '').strip(),
+ 'cls': ctrl_cls,
+ 'methods': {}}
+ for method_name, method, args, verbs in methods:
+ self.controllers[ctrl_name][ctrl_version]['methods'].setdefault(
+ method_name, {})
+ for verb in verbs:
+ self.controllers[ctrl_name][ctrl_version]['methods'][
+ method_name][verb] = {
+ 'fct': method,
+ 'doc': (method.__doc__ or '').strip(),
+ 'args': args}
+
+ def setup_application(self, instance):
+ self.instance = instance
+ self.before_call_chain = []
+ if hasattr(instance, 'before_call'):
+ self.before_call_chain.append(self.instance_before_call)
+ self.after_call_chain = []
+ if hasattr(instance, 'after_call'):
+ self.after_call_chain.append(self.instance_after_call)
+ if hasattr(instance, 'validate_query'):
+ self.validate_query = instance.validate_query
+
+ def instance_before_call(self):
+ getattr(self.instance, 'before_call')()
+
+ def instance_after_call(self, response):
+ getattr(self.instance, 'after_call')()
+ return response
+
+ def not_implemented(self):
+ raise ENI
+
+ request_json = request_environ = request_headers = \
+ request_args = get_cookie = set_cookie = send_bytes = not_implemented
+
+ def format_exception(self, e):
+ ret = None
+ code = 500
+ logger.exception(traceback.format_exc())
+ if hasattr(self.instance, "format_exception"):
+ ret, code = self.instance.format_exception(e)
+ else:
+ ha = (self.config or {}).get(self.application, {}).get(
+ 'on_exception', '').lower()
+ if ha == 'traceback':
+ ret = traceback.format_exc()
+ if ha == 'str':
+ ret = str(e)
+ if ret is None:
+ raise e
+ return ret, code
+
+
+class DumpRoutes(Framework):
+
+ def __repr__(self):
+ ret = []
+ for c in sorted(self.controllers):
+ ret.append("* Controller: %s" % (c))
+ for v in self.controllers[c]:
+ ret.append("** Version: %s: %s" %
+ (v, self.controllers[c][v]['doc']))
+ for m in self.controllers[c][v]['methods']:
+ for _v in self.controllers[c][v]['methods'][m]:
+ ret.append(
+ "- [%s] %s%s: %s" %
+ (_v, m,
+ self.controllers[c][v]['methods'][m][_v]['args'],
+ self.controllers[c][v]['methods'][m][_v]['doc']))
+ return "\n".join(ret)
+
+
+def framework_from_config(application, config):
+ fw_name = (config.get(application) or {}).get('framework')
+ assert fw_name, "No framework configured."
+ for ep in pkg_resources.iter_entry_points('%s.frameworks' % application):
+ if ep.name == fw_name:
+ return ep.load()(application, config)
+ for ep in pkg_resources.iter_entry_points('cadre.frameworks'):
+ if ep.name == fw_name:
+ return ep.load()(application, config)
+ raise Exception("No framework available")
diff --git a/cadre_flask/__init__.py b/cadre_flask/__init__.py
@@ -0,0 +1,166 @@
+import sys
+import re
+import logging
+from flask import Flask, request, send_file, abort
+from flask_cors import CORS
+
+from cadre import Framework
+
+logger = logging.getLogger(__name__)
+
+
+class FlaskFramework(Framework):
+ def __init__(self, application, config):
+ Framework.__init__(self, application, config)
+ self.versions = {}
+ self._endpoints = set()
+ self.application = application
+ self.flask_app = Flask(application)
+ self.flask_app.url_map.strict_slashes = False
+ self.flask_app.register_error_handler(404, self.handle_exception)
+ for k, v in (self.config.get('flask') or {}).items():
+ _v = v
+ if v.lower() == 'false':
+ _v = False
+ if v.lower() == 'true':
+ _v = True
+ if v.isdigit():
+ _v = int(v)
+ self.flask_app.config[k.upper()] = _v
+ if not (self.config.get('flask') or {}).get('external_cors'):
+ logger.debug("We handle CORS.")
+ CORS(self.flask_app)
+ else:
+ logger.debug("CORS must be handheld upstream.")
+
+ def setup_application(self, instance):
+ Framework.setup_application(self, instance)
+ for fct in self.before_call_chain:
+ self.flask_app.before_request(fct)
+ for fct in self.after_call_chain:
+ self.flask_app.after_request(fct)
+ self.flask_app.after_request(self._set_cookies)
+
+ def _add_method(self, ctrl_name, method_name):
+ def accessor(**args):
+ import flask
+ v = None
+ if self.versioning_use_prefix:
+ v = flask.request.environ.get('PATH_INFO').lstrip('/').split(
+ '/')[0]
+ logger.debug("Got version '%s' from URL." % v)
+ else:
+ # flask handle case and underscores
+ h = 'X-%s-Version' % self.application
+ v = flask.request.headers.get(h)
+ logger.debug("Got version '%s' from header '%s'." % (v, h))
+ if v and not v.isdigit():
+ flask.abort(405)
+ v = int(v) if v else sys.maxsize
+ v = v if v in self.controllers[ctrl_name] else max(
+ self.controllers[ctrl_name].keys())
+ verb = flask.request.environ['REQUEST_METHOD']
+ cls = self.controllers[ctrl_name][v]['cls']
+ try:
+ m = self.controllers[ctrl_name][v]['methods'][method_name][
+ verb]['fct'].__name__
+ except KeyError:
+ logger.error("Cannot found method for: controller: '%s', "
+ "method: '%s', version: '%s', verb: '%s'" %
+ (ctrl_name, method_name, v, verb))
+ flask.abort(405)
+ self._cookies = []
+ try:
+ ret = getattr(cls(), m)(**args)
+ except Exception as e:
+ ret = self.handle_exception(e)
+ return ret
+
+ setattr(self, '_m_%s_%s' % (ctrl_name, method_name), accessor)
+
+ def handle_exception(self, e):
+ message, code = self.format_exception(e)
+ if not message:
+ abort(code)
+ return message, code
+
+ def _mk_endpoint(self, ctrl_name, method_name, args, version):
+ ep = "/%s/%s%s" % (ctrl_name.lower(),
+ method_name if method_name != 'index__' else '',
+ ('/' + '/'.join(['<%s>' % a
+ for a in args]) if args else ''))
+ return '/%s%s' % (version, ep) if version else ep
+
+ def _setup_routes(self):
+ """ Add routes to flask """
+ endpoints = {}
+ methods = {}
+ for c in sorted(self.controllers):
+ for v in self.controllers[c]:
+ for m in self.controllers[c][v]['methods']:
+ for _v in self.controllers[c][v]['methods'][m]:
+ fct = self.controllers[c][v]['methods'][m][_v]['fct']
+ args = self.controllers[c][v]['methods'][m][_v]['args']
+ e = self._mk_endpoint(
+ c,
+ m,
+ args,
+ version=v if self.versioning_use_prefix else None)
+ endpoints.setdefault(e, set())
+ if isinstance(fct.__defaults__, tuple):
+ url = re.search(r'^(.*[\\\/])', e).group()
+ endpoints.setdefault(url, set())
+ endpoints[url].add(_v)
+ methods[url] = (c, m)
+ endpoints[e].add(_v)
+ methods[e] = (c, m)
+ for e, verbs in endpoints.items():
+ self._add_method(*methods[e])
+ logger.debug("Adding endpoint: '%s' allowing with verbs: '%s'." %
+ (e, verbs))
+ route = e
+ if route.startswith('/__root__'):
+ route = e.replace('/__root__', '', 1)
+ self.flask_app.add_url_rule(route,
+ route,
+ getattr(self, '_m_%s_%s' % methods[e]),
+ methods=verbs)
+
+ def serve(self):
+ self._setup_routes()
+ self.flask_app.run(
+ host=self.flask_app.config.get('HOST', '0.0.0.0'),
+ port=self.flask_app.config.get('PORT', 5001),
+ debug=self.flask_app.config.get('DEBUG', True),
+ threaded=self.flask_app.config.get('THREADED', False),
+ )
+
+ def get_wsgi_app(self):
+ self._setup_routes()
+ return self.flask_app
+
+ def _set_cookies(self, response):
+ for args, kwargs in getattr(self, '_cookies', {}):
+ response.set_cookie(*args, **kwargs)
+ return response
+
+ def request_json(self):
+ return request.json
+
+ def request_environ(self):
+ return request.environ
+
+ def request_headers(self):
+ return request.headers
+
+ def request_args(self):
+ return request.args
+
+ def get_cookie(self, key):
+ return request.cookies.get(key)
+
+ def set_cookie(self, *args, **kwargs):
+ self._cookies.append((args, kwargs))
+
+ def send_bytes(self, bytes_like, **kwargs):
+ return send_file(bytes_like, **kwargs)
diff --git a/cadre_flask/test.py b/cadre_flask/test.py
@@ -0,0 +1,25 @@
+import unittest
+from cadre import Cadre
+from cadre.framework import framework_from_config
+from cadre.controllers import load_controllers
+
+
+class CadreFlask(Cadre):
+
+ application = 'cadre_test_app'
+
+ def __init__(self):
+ Cadre.__init__(self, None)
+ self.framework = framework_from_config(self.application,
+ {self.application:
+ {'framework': 'flask'}})
+ load_controllers(self.framework, self.application)
+
+
+class TestCadreFlask(unittest.TestCase):
+
+ def setUp(self):
+ self.cadre = CadreFlask()
+
+ def test_setup_routes(self):
+ self.cadre.framework._setup_routes()
diff --git a/cadre_test_app.wsgi b/cadre_test_app.wsgi
@@ -0,0 +1,2 @@
+from cadre_test_app import Cadre_Test_App
+application = Cadre_Test_App("config.ini").get_wsgi_app()
diff --git a/cadre_test_app/__init__.py b/cadre_test_app/__init__.py
@@ -0,0 +1,25 @@
+import sys
+
+import fastjsonschema
+from cadre import Cadre
+
+
+class Cadre_Test_App(Cadre):
+
+ def validate_query(self, schema):
+ from cadre import request
+ fastjsonschema.compile(schema)(request.json)
+
+
+def serve():
+ if len(sys.argv) != 2:
+ print("Usage: %s config.ini")
+ sys.exit(1)
+ Cadre_Test_App(sys.argv[1]).serve()
+
+
+def dump_routes():
+ if len(sys.argv) != 2:
+ print("Usage: %s config.ini")
+ sys.exit(1)
+ Cadre_Test_App(sys.argv[1]).dump_routes()
diff --git a/cadre_test_controllers/__init__.py b/cadre_test_controllers/__init__.py
@@ -0,0 +1,191 @@
+import unittest
+import random
+from cadre import Controller, validate
+
+
+class Hello(Controller):
+ """ A very polite controller """
+
+ def index__(self):
+ """ Says hello ! """
+ return "Hello world"
+
+ def test_index__(self):
+ self.assertTrue(self.index__() == "Hello world")
+
+ def config(self):
+ """ Dump the config """
+ from cadre import config
+ return config
+
+ def test_config(self):
+ from cadre import config
+ k = 'test_%s' % random.randint(0, 1000)
+ v = random.randint(0, 1000)
+ config.update({k: v})
+ self.assertTrue(self.config()[k] == v)
+
+ @validate({
+ 'title': 'Dummy test',
+ 'type': 'object',
+ 'properties': {
+ 'numbers': {
+ 'description': "We want an array of numbers",
+ 'type': 'array',
+ 'items': {
+ 'type': 'integer'
+ },
+ 'minItems': 1
+ }
+ },
+ 'required': ['numbers']
+ })
+ def i_am_validated_POST(self):
+ from cadre import request
+ return request.json
+
+ def test_i_am_validated_POST(self):
+ from cadre import request
+ request.json = {'numbers': [1, 2]}
+ self.assertTrue(self.i_am_validated_POST() == {
+ 'numbers': [1, 2]})
+
+ @validate({
+ 'title': 'Dummy test',
+ 'type': 'object',
+ 'properties': {
+ 'numbers': {
+ 'description': "We want an array of numbers",
+ 'type': 'array',
+ 'items': {
+ 'type': 'integer'
+ },
+ 'minItems': 1
+ }
+ },
+ 'required': ['numbers']
+ })
+ def i_will_not_pass_validation_POST(self):
+ from cadre import request
+ return request.json
+
+ def test_i_will_not_pass_validation_POST(self):
+ from cadre import request
+ request.json = {'xnumbers': [1, 2]}
+ self.i_am_validated_POST()
+ self.assertRaises(Exception)
+
+
+class Hello_2(Controller):
+ """ A very polite controller (version 2) """
+
+ def index__(self):
+ """ Says hello ! """
+ return "Hello world (2)"
+
+ def test_index__(self):
+ self.assertTrue(self.index__() == "Hello world (2)")
+
+ def to(self, who):
+ """ Says hello to whom ask for """
+ return "Hello %s !" % who
+
+ def test_to(self):
+ assert True
+
+ def foo(self):
+ return "Foo"
+
+ def test_foo(self):
+ assert True
+
+ def foo_(self):
+ return "Foo with an '_' at the end !"
+
+ def test_foo_(self):
+ assert True
+
+ def bar_GET(self):
+ return "Bar"
+
+ def test_bar_GET(self):
+ assert True
+
+ def baz_POST(self):
+ from cadre import request
+ return request.json
+
+ def test_baz_POST(self):
+ from cadre import request
+ k = 'test_%s' % random.randint(0, 1000)
+ v = random.randint(0, 1000)
+ request.json = {k: v}
+ self.assertTrue(self.baz_POST()[k] == v)
+
+ def qux_GET_POST(self):
+ return "Qux"
+
+ def test_qux_GET_POST(self):
+ assert True
+
+ def args(self):
+ from cadre import request
+ return request.args.get('qwe', "no 'qwe' arg")
+
+ def test_args(self):
+ from cadre import request
+ v = 'test_%s' % random.randint(0, 1000)
+ request.args['qwe'] = v
+ self.assertTrue(self.args() == v)
+
+
+class Hello_3(Hello_2):
+ """ A very polite controller """
+
+ def index__(self):
+ """ Says hello ! """
+ return "Hello world (3)"
+
+ def test_index__(self):
+ self.assertTrue(self.index__() == "Hello world (3)")
+
+ def bar_GET(self):
+ """ Better 'bar' """
+ return "Better Bar"
+
+ def test_bar_GET(self):
+ assert True
+
+ def bar_POST(self):
+ """ That one handle POST too ! """
+ return "Handle POST too."
+
+ def test_bar_POST(self):
+ assert True
+
+ def send_bytes(self):
+ return self.framework.send_bytes(
+ open("/etc/passwd", "rb"),
+ as_attachment=True,
+ attachment_filename='etc-passwd.txt',
+ mimetype='text/plain')
+
+ def test_send_bytes(self):
+ self.assertTrue(b'root' in self.send_bytes())
+
+ def dividebyzero(self):
+ return 1 / 0
+
+
+class StrangeTestCase(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+
+class Hello_4(Hello_3):
+
+ testcase_cls = StrangeTestCase
diff --git a/config.ini b/config.ini
@@ -0,0 +1,48 @@
+[cadre_test_app]
+framework = flask
+# versioning = headers # can be set to prefix
+# first_version = 0
+
+# Choose what to do with an exception
+# either throw the 'traceback' or just the message 'string'
+# and return it along with a 500
+# or let reraise by default
+on_exception = traceback
+
+
+[flask]
+PORT = 5001
+DEBUG = true
+HOST = 0.0.0.0
+
+[uwsgi]
+http = localhost:5001
+file = cadre_test_app.wsgi
+
+[loggers]
+keys = root, cadre, cadre_flask
+
+[logger_root]
+handlers =
+
+[logger_cadre]
+level = DEBUG
+handlers = console
+qualname = cadre
+
+[logger_cadre_flask]
+level = DEBUG
+handlers = console
+qualname = cadre_flask
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[formatter_generic]
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
diff --git a/routes.html b/routes.html
@@ -0,0 +1,171 @@
+<!doctype html>
+<html lang="fr">
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
+ <style>@media print {
+ .noprint {
+ display: none;
+ }
+}
+@media screen {
+ .noscreen {
+ display: none;
+ }
+}
+html {
+ font-family: arial;
+ font-size: 12pt;
+}
+#body > h1 {
+ margin-top: 64px;
+ text-align: center;
+ margin-bottom: 64px;
+}
+.logo {
+ position: absolute;
+ right: 32px;
+ top: 8px;
+ margin-bottom: 100px;
+}
+li {
+ list-style-type: none;
+}
+.section-number {
+ color: #9d80b3;
+ padding-right: 10pt;
+}
+h1 {
+ color: #2c0449;
+ font-size: 24pt;
+}
+h2 {
+ color: #2c0449;
+ font-size: 20pt;
+}
+h3 {
+ color: #2c0449;
+ font-size: 16pt;
+}
+.side-title {
+ color: #2c0449;
+ font-size: 16pt;
+ font-weight: bold;
+}
+:link {
+ border-bottom: 1px dotted #65427e;
+ color: #2c0449;
+ text-decoration: none;
+}
+:visited {
+ border-bottom: 1px dotted #65427e;
+ color: #9d80b3;
+ text-decoration: none;
+}
+:link:hover,
+:visited:hover {
+ color: #65427e;
+}
+b {
+ color: #2c0449;
+}
+code {
+ background-color: #f4e7fd;
+ border: 1px solid #6a4d7f;
+ border-radius: 1pt;
+ font-size: 90%;
+}
+pre code {
+ background-color: none;
+ border: none;
+}
+pre {
+ background-color: #f4e7fd;
+ font-family: courier, monospace;
+ font-size: 90%;
+ overflow: auto;
+ border: 1px solid #6a4d7f;
+ border-radius: 4pt;
+ box-shadow: 2px 2px 3px #9d80b3;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 10pt;
+ margin-top: 15pt;
+ padding: 5pt;
+}
+textarea {
+ overflow-x: auto;
+}
+img {
+ border: 1px solid #6a4d7f;
+ border-radius: 4pt;
+ box-shadow: 2px 2px 3px #9d80b3;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 10pt;
+ margin-top: 15pt;
+ padding: 5pt;
+}
+.noshadow {
+ border: none;
+ box-shadow: none;
+}
+dt {
+ font-weight: bold;
+}
+table {
+ background: #ebfaf9;
+ border: 1px solid #6a4d7f;
+ border-radius: 4pt;
+ box-shadow: 2px 2px 3px #9d80b3;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 10pt;
+ margin-top: 15pt;
+ padding: 5pt;
+}
+.root {
+ border-bottom: none;
+}
+th {
+ background: #d1faf7;
+ border: 1px solid #75f0e8;
+ border-radius: 1pt;
+ padding: 5px;
+ vertical-align: top;
+}
+td {
+ background: #ebfaf9;
+ border: 1px solid #99e5e0;
+ border-radius: 1pt;
+ font-family: courier, monospace;
+ padding: 2px;
+ vertical-align: top;
+}
+#src_file {
+ padding: 8px;
+ float: right;
+ font-size: 10px;
+ color: #ccc;
+}
+.task-status {
+ margin-right: 4px;
+ padding-left: 4px;
+ padding-right: 4px;
+}
+.task-status .todo {
+ background: red;
+ color: white;
+}
+.task-status .done {
+ background: green;
+ color: white;
+}
+ </style>
+ </head>
+ <body><h1>Untitled</h1>
+<ul></ul>
+
+
+ </body>
+</html>
diff --git a/routes.org b/routes.org
diff --git a/setup.cfg b/setup.cfg
@@ -0,0 +1,3 @@
+[nosetests]
+verbosity=3
+with-doctest=1
diff --git a/setup.py b/setup.py
@@ -0,0 +1,23 @@
+from setuptools import setup
+
+console_scripts = """
+cadre-test-app-serve = cadre_test_app:serve
+cadre-test-app-dump-routes = cadre_test_app:dump_routes
+"""
+
+setup(name='cadre',
+ version='0.1.0',
+ description="Cadre framework",
+ author="Jean Schurger",
+ author_email='jean@schurger.org',
+ packages=['cadre', 'cadre_flask'],
+ install_requires=['Flask', 'fastjsonschema', 'flask-cors',
+ 'decorator', 'nose', 'coverage'],
+ entry_points={
+ 'console_scripts': console_scripts,
+ 'cadre.frameworks':
+ ['flask = cadre_flask:FlaskFramework'],
+ 'cadre_test_app.controllers':
+ ['test_controllers = cadre_test_controllers']
+ },
+ license='GPLv3')
diff --git a/test/__init__.py b/test/__init__.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python3
+
+from cadre_test_app import Cadre_Test_App
+from cadre.controllers import get_controllers_tsts
+
+locals().update(get_controllers_tsts(Cadre_Test_App(None)))