From 3035a9f009d53c332240e897d192fadf6c74ccb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 28 Oct 2019 09:03:55 +0100 Subject: [PATCH] [DOC] move reference doc in subfolder --- README.md | 12 +- doc/{ => architecture}/vdom.md | 0 doc/comparison.md | 8 +- doc/readme.md | 26 ++-- doc/{ => reference}/animations.md | 0 doc/{ => reference}/component.md | 2 +- doc/{ => reference}/context.md | 0 doc/{ => reference}/event_bus.md | 0 doc/{ => reference}/hooks.md | 0 doc/{ => reference}/misc.md | 0 doc/{ => reference}/observer.md | 0 doc/{ => reference}/qweb.md | 2 +- doc/{ => reference}/router.md | 0 doc/{ => reference}/store.md | 0 doc/{ => reference}/tags.md | 2 +- doc/{ => reference}/utils.md | 0 doc/tooling.md | 4 +- tests/doc_link_checker.test.ts | 213 +++++++++++++++++++----------- 18 files changed, 163 insertions(+), 106 deletions(-) rename doc/{ => architecture}/vdom.md (100%) rename doc/{ => reference}/animations.md (100%) rename doc/{ => reference}/component.md (99%) rename doc/{ => reference}/context.md (100%) rename doc/{ => reference}/event_bus.md (100%) rename doc/{ => reference}/hooks.md (100%) rename doc/{ => reference}/misc.md (100%) rename doc/{ => reference}/observer.md (100%) rename doc/{ => reference}/qweb.md (99%) rename doc/{ => reference}/router.md (100%) rename doc/{ => reference}/store.md (100%) rename doc/{ => reference}/tags.md (95%) rename doc/{ => reference}/utils.md (100%) diff --git a/README.md b/README.md index 0c7e7ced..2d2c480e 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ const app = new App({ qweb: new QWeb() }); app.mount(document.body); ``` -Note that the counter component is made reactive with the [`useState` hook](doc/hooks.md#usestate). -Also, all examples here uses the [`xml` helper](doc/tags.md#xml-tag) to define inline templates. +Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate). +Also, all examples here uses the [`xml` helper](doc/reference/tags.md#xml-tag) to define inline templates. But this is not mandatory, many applications will load templates separately. More interesting examples can be found on the @@ -93,8 +93,8 @@ A complete documentation for Owl can be found here: The most important sections are: - [Quick Start](doc/quick_start.md) -- [Component](doc/component.md) -- [Hooks](doc/hooks.md) +- [Component](doc/reference/component.md) +- [Hooks](doc/reference/hooks.md) Found an issue in the documentation? A broken link? Some outdated information? Submit a PR! @@ -128,7 +128,7 @@ Owl components in an application are used to define a (dynamic) tree of componen ``` **Environment:** the root component is special: it is created with an environment, -which should contain a [`QWeb` instance](doc/qweb.md). The environment is then automatically +which should contain a [`QWeb` instance](doc/reference/qweb.md). The environment is then automatically propagated to each sub components (and accessible in the `this.env` property). ```js @@ -255,7 +255,7 @@ class Parent extends Component { In this example, the `OrderLine` component trigger a `add-to-order` event. This will generate a DOM event which will bubble along the DOM tree. It will then be intercepted by the parent component, which will then get the line (from the -`detail` key) and then increment its quantity. See the section on [event handling](doc/component.md#event-handling) +`detail` key) and then increment its quantity. See the section on [event handling](doc/reference/component.md#event-handling) for more details on how events work. Note that this example would have also worked if the `OrderLine` component diff --git a/doc/vdom.md b/doc/architecture/vdom.md similarity index 100% rename from doc/vdom.md rename to doc/architecture/vdom.md diff --git a/doc/comparison.md b/doc/comparison.md index 4084f6a6..800427ff 100644 --- a/doc/comparison.md +++ b/doc/comparison.md @@ -79,7 +79,7 @@ additional tools, we made a lot of effort to make the most of the web platform. For example, Owl uses the standard `xml` parser that comes with every browser. Because of that, Owl did not have to write its own template parser. Another -example is the [`xml`](tags.md#xml-tag) tag helper function, which makes use of +example is the [`xml`](reference/tags.md#xml-tag) tag helper function, which makes use of native template literals to allow in a natural way to write `xml` templates directly in the javascript code. This can be easily integrated with editor plugins to have autocompletion inside the template. @@ -127,7 +127,7 @@ structured than a template language. Note that the tooling is quite impressive: there is a syntax highlighter for jsx here on github! By comparison, here is the equivalent Owl component, written with the -[`xml`](tags.md#xml-tag) tag helper: +[`xml`](reference/tags.md#xml-tag) tag helper: ```js class Clock extends Component { @@ -251,7 +251,7 @@ keeps track of who get data, and retrigger a render when it was changed. Owl store is a little bit like a mix of redux and vuex: it has actions (but not mutations), and like VueX, it keeps track of the state changes. However, it does not notify a component when the state changes. Instead, components need to connect -to the store like in redux, with the `useStore` hook (see the [store documentation](store.md#connecting-a-component)). +to the store like in redux, with the `useStore` hook (see the [store documentation](reference/store.md#connecting-a-component)). ```javascript const actions = { @@ -314,7 +314,7 @@ This work is based on the new ideas introduced by React hooks. From the way React and Vue introduce their hooks, it may look like hooks are not compatible with class components. However, this is not the case, as shown by -Owl [hooks](hooks.md). They are inspired by both React and Vue. For example, +Owl [hooks](reference/hooks.md). They are inspired by both React and Vue. For example, the `useState` hook is named after React, but its API is closer to the `reactive` Vue hook. diff --git a/doc/readme.md b/doc/readme.md index e79f3942..6cb81edd 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -50,19 +50,19 @@ Note that for convenience, the `useState` hook is also exported at the root of t ## Reference -- [Animations](animations.md) -- [Component](component.md) -- [Context](context.md) -- [Event Bus](event_bus.md) -- [Hooks](hooks.md) -- [Misc](misc.md) -- [Observer](observer.md) -- [QWeb](qweb.md) -- [Router](router.md) -- [Store](store.md) -- [Tags](tags.md) -- [Utils](utils.md) -- [Virtual DOM](vdom.md) +- [Animations](reference/animations.md) +- [Component](reference/component.md) +- [Context](reference/context.md) +- [Event Bus](reference/event_bus.md) +- [Hooks](reference/hooks.md) +- [Misc](reference/misc.md) +- [Observer](reference/observer.md) +- [QWeb](reference/qweb.md) +- [Router](reference/router.md) +- [Store](reference/store.md) +- [Tags](reference/tags.md) +- [Utils](reference/utils.md) +- [Virtual DOM](architecture/vdom.md) ## Learning Resources diff --git a/doc/animations.md b/doc/reference/animations.md similarity index 100% rename from doc/animations.md rename to doc/reference/animations.md diff --git a/doc/component.md b/doc/reference/component.md similarity index 99% rename from doc/component.md rename to doc/reference/component.md index 8f35c95f..91a49307 100644 --- a/doc/component.md +++ b/doc/reference/component.md @@ -986,7 +986,7 @@ of the props. Here is how it works in Owl: - `props` key is a static key (so, different from `this.props` in a component instance) - it is optional: it is ok for a component to not define a `props` key. - props are validated whenever a component is created/updated -- props are only validated in `dev` mode (see [tooling page](tooling.md#development-mode)) +- props are only validated in `dev` mode (see [tooling page](../tooling.md#development-mode)) - if a key does not match the description, an error is thrown - it validates keys defined in (static) `props`. Additional keys given by the parent will cause an error. diff --git a/doc/context.md b/doc/reference/context.md similarity index 100% rename from doc/context.md rename to doc/reference/context.md diff --git a/doc/event_bus.md b/doc/reference/event_bus.md similarity index 100% rename from doc/event_bus.md rename to doc/reference/event_bus.md diff --git a/doc/hooks.md b/doc/reference/hooks.md similarity index 100% rename from doc/hooks.md rename to doc/reference/hooks.md diff --git a/doc/misc.md b/doc/reference/misc.md similarity index 100% rename from doc/misc.md rename to doc/reference/misc.md diff --git a/doc/observer.md b/doc/reference/observer.md similarity index 100% rename from doc/observer.md rename to doc/reference/observer.md diff --git a/doc/qweb.md b/doc/reference/qweb.md similarity index 99% rename from doc/qweb.md rename to doc/reference/qweb.md index c12706f9..6815cbd5 100644 --- a/doc/qweb.md +++ b/doc/reference/qweb.md @@ -121,7 +121,7 @@ Its API is quite simple: ``` - **`render(name, context, extra)`**: renders a template. This returns a `vnode`, - which is a virtual representation of the DOM (see [vdom doc](vdom.md)). + which is a virtual representation of the DOM (see [vdom doc](../architecture/vdom.md)). ```js const vnode = qweb.render("App", component); diff --git a/doc/router.md b/doc/reference/router.md similarity index 100% rename from doc/router.md rename to doc/reference/router.md diff --git a/doc/store.md b/doc/reference/store.md similarity index 100% rename from doc/store.md rename to doc/reference/store.md diff --git a/doc/tags.md b/doc/reference/tags.md similarity index 95% rename from doc/tags.md rename to doc/reference/tags.md index 24defba5..f235303e 100644 --- a/doc/tags.md +++ b/doc/reference/tags.md @@ -9,7 +9,7 @@ Tags are very small helpers to make it easy to write inline templates. There is only one currently available tag: `xml`, but we plan to add other tags later, -such as a `css` tag, which will be used to write [single file components](tooling.md#single-file-component). +such as a `css` tag, which will be used to write [single file components](../tooling.md#single-file-component). ## XML tag diff --git a/doc/utils.md b/doc/reference/utils.md similarity index 100% rename from doc/utils.md rename to doc/reference/utils.md diff --git a/doc/tooling.md b/doc/tooling.md index d206b766..6e0a5841 100644 --- a/doc/tooling.md +++ b/doc/tooling.md @@ -64,8 +64,8 @@ It is very useful to group code by feature instead of by type of file. It makes it easier to scale application to larger size. To do so, Owl currently has a small helper that makes it easy to define a -template inside a javascript (or typescript) file: the [`xml`](tags.md#xml-tag) -helper. With this, a template is automatically registered to [QWeb](qweb.md). +template inside a javascript (or typescript) file: the [`xml`](reference/tags.md#xml-tag) +helper. With this, a template is automatically registered to [QWeb](reference/qweb.md). This means that the template and the javascript code can be defined in the same file. It is not currently possible to add css to the same file, but Owl may diff --git a/tests/doc_link_checker.test.ts b/tests/doc_link_checker.test.ts index 36c1429b..bfdc5223 100644 --- a/tests/doc_link_checker.test.ts +++ b/tests/doc_link_checker.test.ts @@ -6,36 +6,9 @@ */ import * as fs from "fs"; -const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g; -const HEADING_REGEXP = /\n(#+\s*)(.*)/g; - -// files to be checked -function getFiles(): string[] { - const DOCFILES = fs.readdirSync("doc").map(f => `doc/${f}`); - const MAINREADME = "README.md"; - DOCFILES.push("roadmap.md", MAINREADME); - return DOCFILES; -} - -test("All markdown links work", () => { - let linkNumber = 0; - let invalidLinkNumber = 0; - const files = getFiles(); - const data = readDocData(files); - for (let file of data) { - for (let link of file.links) { - // DEBUG: uncomment next line - // console.warn(`Checking "${link.name}" in "${file.name}"`); - linkNumber++; - if (!isLinkValid(link, file, data)) { - console.warn(`Invalid Link: "${link.name}" in "${file.name}"`); - invalidLinkNumber++; - } - } - } - expect(invalidLinkNumber).toBe(0); - expect(linkNumber).toBeGreaterThan(10); -}); +//-------------------------------------------------------------------------- +// Helpers +//-------------------------------------------------------------------------- interface MarkDownLink { name: string; @@ -49,38 +22,130 @@ interface MarkDownSection { interface FileData { name: string; + path: string[]; + fullName: string links: MarkDownLink[]; sections: MarkDownSection[]; } -function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean { +const LINK_REGEXP = /\[([^\[]+)\]\(([^\)]+)\)/g; +const HEADING_REGEXP = /\n(#+\s*)(.*)/g; + + + +export function addMardownData(fileData): void { + const sep = fileData.path.length > 0 ? '/' : ''; + const fullName = fileData.path.join('/') + sep + fileData.name; + const content = fs.readFileSync(fullName, { encoding: "utf8" }); + let m; + // get links info + do { + m = LINK_REGEXP.exec(content); + if (m) { + fileData.links.push({ name: m[0], link: m[2] }); + } + } while (m); + // get sections info + do { + m = HEADING_REGEXP.exec(content); + if (m) { + fileData.sections.push({ name: m[0], slug: slugify(m[2]) }); + } + } while (m); +} + + +/** + * Returns a list of FileData corresponding to all files that need to be + * validated. + */ +function getFiles(path: string[] = []): FileData[] { + if (path.length === 0) { + const baseFiles: FileData[] = [ + { name: "README.md", path: [], links: [], sections: [], fullName: "README.md" }, + { name: "roadmap.md", path: [], links: [], sections: [], fullName: "roadmap.md" } + ]; + const rest = getFiles(["doc"]); + const result = baseFiles.concat(rest); + result.forEach(addMardownData); + return result; + } + const files = fs.readdirSync(path.join("/"), { withFileTypes: true }).map(f => { + if (f.isDirectory()) { + return getFiles(path.concat(f.name)); + } + const fullName = path.join('/') + (path.length > 0 ? '/' : '') + f.name; + return [ + { + name: f.name, + path, + links: [], + sections: [], + fullName + } + ]; + }); + return Array.prototype.concat(...files); +} + +const LOCAL_FILES = ['LICENSE']; +export function isLinkValid(link: MarkDownLink, current: FileData, files: FileData[]): boolean { if (link.link.startsWith("http")) { // no check on external links return true; } - const parts = link.link.split("#"); - const currentParts = current.name.split("/"); - const path = currentParts.length > 1 ? currentParts[0] + "/" : ""; - const fullName = path + parts[0]; - if (parts.length === 1) { - // no # in url - if (parts[0].endsWith(".md")) { - // it is a local md file - if (!files.find(f => f.name === fullName)) { + // Step 1: extract path, name, hash + // path = ['doc', 'architecture] + // name = 'rendering.md' + // hash = 'blabla' (or '' if no hash) + + const parts = link.link.split('#'); + const hash = parts[1] || ''; + let name; + let path; + if (parts[0]) { + let temp = parts[0].split('/'); + name = temp[temp.length - 1]; + temp.splice(-1); + path = current.path.slice(); + for (let elem of temp) { + if (elem === '..') { + path.splice(-1); + } else if (elem !== '.') { + path.push(elem); + } + } + } else { + // there are no file name, so this is a relative link to the current file + name = current.name; + path = current.path; + } + + // Step 2: build normalized link file name + const linkFullName = path.join('/') + (path.length > 0 ? '/' : '') + name; + + // Step 3: check link name against white list of local files + if (LOCAL_FILES.includes(linkFullName)) { + return true; + } + + // Step 4: check if there is a matching file + let target: FileData | undefined = files.find(f => f.fullName === linkFullName); + if (!target) { return false; - } } - } else { - const file = parts[0] === "" ? current : files.find(f => f.name === fullName); - if (!file) { - return false; + + // Step 5: if necessary, check if there is a corresponding link inside the target + // link name + if (hash) { + if (!target.sections.find(s => s.slug === hash)) { + return false; + } } - if (!file.sections.find(s => s.slug === parts[1])) { - return false; - } - } - return true; + + return true; } + // adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1 function slugify(str) { const a = "àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœøṕŕßśșțùúüûǘẃẍÿź·_,:;"; @@ -99,33 +164,25 @@ function slugify(str) { .replace(/-+$/, ""); // Trim - from end of text } -function readDocData(files: string[]): FileData[] { - const result: FileData[] = []; - for (let file of files) { - const fileData: FileData = { - name: file, - links: [], - sections: [] - }; - const content = fs.readFileSync(file, { encoding: "utf8" }); - let m; - // get links info - do { - m = LINK_REGEXP.exec(content); - if (m) { - fileData.links.push({ name: m[0], link: m[2] }); - } - } while (m); - // get sections info - do { - m = HEADING_REGEXP.exec(content); - if (m) { - fileData.sections.push({ name: m[0], slug: slugify(m[2]) }); - } - } while (m); - result.push(fileData); - } - return result; -} +//-------------------------------------------------------------------------- +// Test +//-------------------------------------------------------------------------- + +test("All markdown links work", () => { + let linkNumber = 0; + let invalidLinkNumber = 0; + const data = getFiles(); + for (let file of data) { + for (let link of file.links) { + linkNumber++; + if (!isLinkValid(link, file, data)) { + console.warn(`Invalid Link: "${link.name}" in "${file.name}"`); + invalidLinkNumber++; + } + } + } + expect(invalidLinkNumber).toBe(0); + expect(linkNumber).toBeGreaterThan(10); +});