Format + lint (#1016)

* add prettierignore

* switch to eslint:recommended + eslint-config-prettier

* fix eslint violations

* remove more .jshintrc files

* better conditional structure

* add prettier and update prettier ignore

* add precommit hook to run prettier

* add lint check to precommit and format check to ci

* format all the things

* add generated files

* let npm do it's thing with package.json
This commit is contained in:
Michael Rawlings 2018-03-09 10:02:11 -08:00 committed by Dylan Piercey
parent ee17aa53ab
commit 0f5e639775
2252 changed files with 22108 additions and 18102 deletions

17
.eslintignore Normal file
View File

@ -0,0 +1,17 @@
# minified
*.min.js
# test
*actual*
*expected*
input.js
# generated
*dist*
*generated*
*.marko.js
*.marko.*.js
*.html.js
~vdom.skip
node_modules

14
.eslintrc Normal file
View File

@ -0,0 +1,14 @@
{
"extends": [
"eslint:recommended",
"prettier"
],
"env": {
"node": true,
"es6": true
},
"globals": {
"document": true,
"ShadowRoot": true
}
}

View File

@ -3,20 +3,20 @@
This project adheres to the [eBay Code of Conduct](http://ebay.github.io/codeofconduct). This project adheres to the [eBay Code of Conduct](http://ebay.github.io/codeofconduct).
By participating in this project you agree to abide by its terms. By participating in this project you agree to abide by its terms.
- Be friendly and patient. * Be friendly and patient.
- Be welcoming: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. * Be welcoming: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
- Be considerate: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that were a world-wide community, so you might not be communicating in someone elses primary language. * Be considerate: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that were a world-wide community, so you might not be communicating in someone elses primary language.
- Be respectful: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. Its important to remember that a community where people feel uncomfortable or threatened is not a productive one. * Be respectful: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. Its important to remember that a community where people feel uncomfortable or threatened is not a productive one.
- Be careful in the words that we choose: we are a community of professionals, and we conduct ourselves professionally. * Be careful in the words that we choose: we are a community of professionals, and we conduct ourselves professionally.
- Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior arent acceptable. * Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior arent acceptable.
- Try to understand why we disagree: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. * Try to understand why we disagree: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively.
- Remember that were different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnt mean that theyre wrong. Dont forget that it is human to err and blaming each other doesnt get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. * Remember that were different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnt mean that theyre wrong. Dont forget that it is human to err and blaming each other doesnt get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
Please visit http://ebay.github.io/codeofconduct for the full code of conduct. Please visit http://ebay.github.io/codeofconduct for the full code of conduct.

View File

@ -10,9 +10,10 @@
## I just have a question ## I just have a question
Before you ask, check our [existing questions](https://github.com/marko-js/marko/issues?page=2&q=is%3Aissue+label%3Atype%3Aquestion&utf8=%E2%9C%93) to see if your question has already been answered. If not, go ahead an open an issue or join us in [gitter](https://gitter.im/marko-js/marko) to ask a question. Before you ask, check our [existing questions](https://github.com/marko-js/marko/issues?page=2&q=is%3Aissue+label%3Atype%3Aquestion&utf8=%E2%9C%93) to see if your question has already been answered. If not, go ahead an open an issue or join us in [gitter](https://gitter.im/marko-js/marko) to ask a question.
Please be sure to use [markdown code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/) when posting code on GitHub or Gitter: Please be sure to use [markdown code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/) when posting code on GitHub or Gitter:
```` ````
```marko ```marko
<div>some marko ${code}</div> <div>some marko ${code}</div>
@ -29,25 +30,27 @@ Not sure if that typo is worth a pull request? Found a bug and know how to fix i
We are always thrilled to receive pull requests. We do our best to process them quickly. If your pull request is not accepted on the first try, don't get discouraged! We'll work with you to come to an acceptable solution. We are always thrilled to receive pull requests. We do our best to process them quickly. If your pull request is not accepted on the first try, don't get discouraged! We'll work with you to come to an acceptable solution.
Prior to merging your PR, you will need to sign the [JS Foundation CLA](https://cla.js.foundation/marko-js/marko). It's pretty straight-forward and only takes a minute. You can even sign it now if you're thinking about contributing. Prior to merging your PR, you will need to sign the [JS Foundation CLA](https://cla.js.foundation/marko-js/marko). It's pretty straight-forward and only takes a minute. You can even sign it now if you're thinking about contributing.
> **TIP:** If you're new to GitHub or open source you can check out this [free course](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) on how to contribute to an open source project. > **TIP:** If you're new to GitHub or open source you can check out this [free course](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) on how to contribute to an open source project.
### Running tests ### Running tests
Before submitting your PR, make sure that all new and previous tests pass and that [coverage](https://coveralls.io/github/marko-js/marko?branch=master) has not decreased: Before submitting your PR, make sure that all new and previous tests pass and that [coverage](https://coveralls.io/github/marko-js/marko?branch=master) has not decreased:
``` ```
npm run test-coverage npm run test-coverage
``` ```
While developing you can run a single test group and use [grep](https://mochajs.org/#-g---grep-pattern) to filter the tests: While developing you can run a single test group and use [grep](https://mochajs.org/#-g---grep-pattern) to filter the tests:
``` ```
npm run mocha -- --grep=lifecycle npm run mocha -- --grep=lifecycle
``` ```
### Adding tests ### Adding tests
Marko makes use of directory based test suites. Take a look at the `render` test suite: Marko makes use of directory based test suites. Take a look at the `render` test suite:
<pre> <pre>
<a href="../test/">test/</a> <a href="../test/">test/</a>
@ -66,15 +69,16 @@ Marko makes use of directory based test suites. Take a look at the `render` tes
The `html.test.js` file will run and read all the directories under `render/fixtures` and for each directory (`attrs`, `for-tag`, etc.) it will run `test.js`, render `template.marko` and assert that it is equivalent to the content of `expected.html`. The `html.test.js` file will run and read all the directories under `render/fixtures` and for each directory (`attrs`, `for-tag`, etc.) it will run `test.js`, render `template.marko` and assert that it is equivalent to the content of `expected.html`.
To add a new test, you'll find the appropriate test suite, copy a fixture, and modify it to add the new test. To add a new test, you'll find the appropriate test suite, copy a fixture, and modify it to add the new test.
#### Skipping a test #### Skipping a test
A few of the tests suites use the same fixtures for multiple test scenarios. For example, the `component-browser` tests run once rendering the component in a browser environment and a second time rendering in a server environment, then hydrating in the browser. A few of the tests suites use the same fixtures for multiple test scenarios. For example, the `component-browser` tests run once rendering the component in a browser environment and a second time rendering in a server environment, then hydrating in the browser.
For some tests, it might be necessary to skip the test in one of these scenarios. This is done by exporting a `skipHydrate` (or similiarly named) property from the fixture. The value of the property should be a string explaining why the test is skipped. For some tests, it might be necessary to skip the test in one of these scenarios. This is done by exporting a `skipHydrate` (or similiarly named) property from the fixture. The value of the property should be a string explaining why the test is skipped.
#### Adding a failing test case #### Adding a failing test case
If you've discovered an issue and are able to reproduce it, but don't have a fix, consider submitting a PR with a failing test case. You can mark a fixture as expected to fail by appending `.fails` to the directory name: If you've discovered an issue and are able to reproduce it, but don't have a fix, consider submitting a PR with a failing test case. You can mark a fixture as expected to fail by appending `.fails` to the directory name:
``` ```
⤷ fixtures/ ⤷ fixtures/
@ -87,7 +91,8 @@ Expected failures won't cause [Travis CI](https://travis-ci.org/marko-js/marko)
### Debugging tests ### Debugging tests
If you need to dig a bit deeper into a failing test, use the `--inspect-brk` flag, open Chrome DevTools, and click on the green nodejs icon (<img height="16" src="https://user-images.githubusercontent.com/1958812/37050480-d53e4276-2128-11e8-8c7a-f5d842956c98.png"/>) to start debugging. Learn more about [debugging node](https://www.youtube.com/watch?v=Xb_0awoShR8&t=103s) from this video. If you need to dig a bit deeper into a failing test, use the `--inspect-brk` flag, open Chrome DevTools, and click on the green nodejs icon (<img height="16" src="https://user-images.githubusercontent.com/1958812/37050480-d53e4276-2128-11e8-8c7a-f5d842956c98.png"/>) to start debugging. Learn more about [debugging node](https://www.youtube.com/watch?v=Xb_0awoShR8&t=103s) from this video.
``` ```
npm run mocha -- --grep=test-name --inspect-brk npm run mocha -- --grep=test-name --inspect-brk
``` ```
@ -101,9 +106,9 @@ $ debugger;
### Updating snapshots ### Updating snapshots
A number of the test suites make use snapshot comparisons. For example, the `render` tests compare the rendered html against a stored snapshot. Similarly, the `compiler` tests compare the generated JavaScript module againt a stored snapshot. Any changes compared to the snapshot should be looked at closely, but there are some cases where it is fine that the output has changed and the snapshot needs to be updated. A number of the test suites make use snapshot comparisons. For example, the `render` tests compare the rendered html against a stored snapshot. Similarly, the `compiler` tests compare the generated JavaScript module againt a stored snapshot. Any changes compared to the snapshot should be looked at closely, but there are some cases where it is fine that the output has changed and the snapshot needs to be updated.
To update a snapshot, you can copy the contents from the `actual` file to the `expected` file in the fixture directory. You can also use the `UPDATE_EXPECTATIONS` env variable to cause the test runner to update the `expected` file for all currently failing tests in a suite: To update a snapshot, you can copy the contents from the `actual` file to the `expected` file in the fixture directory. You can also use the `UPDATE_EXPECTATIONS` env variable to cause the test runner to update the `expected` file for all currently failing tests in a suite:
``` ```
UPDATE_EXPECTATIONS=1 npm run mocha UPDATE_EXPECTATIONS=1 npm run mocha
@ -115,17 +120,17 @@ Comment on the issue and let us know you'd like to tackle it. If for some reason
Here's some to get started with: Here's some to get started with:
- [good first issue](https://github.com/marko-js/marko/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22): great for new contributors * [good first issue](https://github.com/marko-js/marko/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22): great for new contributors
- [help wanted](https://github.com/marko-js/marko/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) issues: won't be tackled in the near future by the maintainers... we need your help! * [help wanted](https://github.com/marko-js/marko/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) issues: won't be tackled in the near future by the maintainers... we need your help!
- [unassigned](https://github.com/marko-js/marko/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20no%3Aassignee%20) issues: open issues that no one has claimed... yet * [unassigned](https://github.com/marko-js/marko/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20no%3Aassignee%20) issues: open issues that no one has claimed... yet
## Reporting bugs and other issues ## Reporting bugs and other issues
A great way to contribute to the project is to send a detailed report when you encounter an issue. Even better: submit a PR with a failing test case ([see how](#adding-a-failing-test-case)). A great way to contribute to the project is to send a detailed report when you encounter an issue. Even better: submit a PR with a failing test case ([see how](#adding-a-failing-test-case)).
Check that [our issue database](https://github.com/marko-js/marko/issues) doesn't already include that problem or suggestion before submitting an issue. If you find a match, you can use the "subscribe" button to get notified on updates. Rather than leaving a "+1" or "I have this too" comment, you can add a [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) to let us know that this is also affecting you without cluttering the conversation. However, if you have ways to reproduce the issue or have additional information that may help resolving the issue, please leave a comment. Check that [our issue database](https://github.com/marko-js/marko/issues) doesn't already include that problem or suggestion before submitting an issue. If you find a match, you can use the "subscribe" button to get notified on updates. Rather than leaving a "+1" or "I have this too" comment, you can add a [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) to let us know that this is also affecting you without cluttering the conversation. However, if you have ways to reproduce the issue or have additional information that may help resolving the issue, please leave a comment.
We have an [ISSUE_TEMPLATE](ISSUE_TEMPLATE.md) that will populate your textarea when you go to open an issue. Use the relevant section and remove the rest. We have an [ISSUE_TEMPLATE](ISSUE_TEMPLATE.md) that will populate your textarea when you go to open an issue. Use the relevant section and remove the rest.
Please provide as much detail as possible. Please provide as much detail as possible.
@ -140,6 +145,7 @@ Security reports are greatly appreciated and we will publicly thank you for it.
Once you post an issue, a maintainer will add one or more labels to it. Below is a guideline for the maintainers and anyone else who is interested in what the various labels mean. Once you post an issue, a maintainer will add one or more labels to it. Below is a guideline for the maintainers and anyone else who is interested in what the various labels mean.
### Type ### Type
![](https://img.shields.io/badge/type-bug-dd0000.svg) ![](https://img.shields.io/badge/type-bug-dd0000.svg)
![](https://img.shields.io/badge/type-unverified%20bug-aa3300.svg) ![](https://img.shields.io/badge/type-unverified%20bug-aa3300.svg)
![](https://img.shields.io/badge/type-feature-0099dd.svg) ![](https://img.shields.io/badge/type-feature-0099dd.svg)
@ -150,15 +156,16 @@ Once you post an issue, a maintainer will add one or more labels to it. Below is
Every issue should be assigned one of these. Every issue should be assigned one of these.
- **bug**: A bug report * **bug**: A bug report
- **unverified-bug**: A bug report that has not been verified * **unverified-bug**: A bug report that has not been verified
- **feature**: A feature request * **feature**: A feature request
- **question**: A question about how to do something in Marko * **question**: A question about how to do something in Marko
- **community**: Related to community building, improving the contribution process, etc. * **community**: Related to community building, improving the contribution process, etc.
- **tech debt**: Related to refactoring code, test structure, etc. * **tech debt**: Related to refactoring code, test structure, etc.
- **docs**: Related to documentation/website * **docs**: Related to documentation/website
### Scope ### Scope
![](https://img.shields.io/badge/scope-parser-5500cc.svg) ![](https://img.shields.io/badge/scope-parser-5500cc.svg)
![](https://img.shields.io/badge/scope-compiler-cc0077.svg) ![](https://img.shields.io/badge/scope-compiler-cc0077.svg)
![](https://img.shields.io/badge/scope-runtime-eebb00.svg) ![](https://img.shields.io/badge/scope-runtime-eebb00.svg)
@ -168,47 +175,51 @@ Every issue should be assigned one of these.
What part of the Marko stack does this issue apply to? In most cases there should only be one of these. What part of the Marko stack does this issue apply to? In most cases there should only be one of these.
- **parser**: Relates to [`htmljs-parser`](https://github.com/marko-js/htmljs-parser) * **parser**: Relates to [`htmljs-parser`](https://github.com/marko-js/htmljs-parser)
- **compiler**: Relates to the [compiler](../src/compiler) (server only) * **compiler**: Relates to the [compiler](../src/compiler) (server only)
- **runtime**: Relates to the [runtime](../src/runtime) (isomorphic/universal) * **runtime**: Relates to the [runtime](../src/runtime) (isomorphic/universal)
- **core-taglib**: Relates to [custom tags](../src/taglib) that ship with Marko * **core-taglib**: Relates to [custom tags](../src/taglib) that ship with Marko
- **components**: Relates to [components](../src/components) * **components**: Relates to [components](../src/components)
- **tools**: Relates to editor plugins, commandline tools, etc. * **tools**: Relates to editor plugins, commandline tools, etc.
### Status ### Status
![](https://img.shields.io/badge/status-backlog-223344.svg) ![](https://img.shields.io/badge/status-backlog-223344.svg)
![](https://img.shields.io/badge/status-in%20progress-006b75.svg) ![](https://img.shields.io/badge/status-in%20progress-006b75.svg)
![](https://img.shields.io/badge/status-needs%20review-0e8a16.svg) ![](https://img.shields.io/badge/status-needs%20review-0e8a16.svg)
In many cases, additional *actions* should be taken when applying one of these. In many cases, additional _actions_ should be taken when applying one of these.
- **backlog**: Tasks planned to be worked on * **backlog**: Tasks planned to be worked on
- **in progress**: This is currently being worked on. * **in progress**: This is currently being worked on.
- **needs review**: This issue needs to be followed up on. * **needs review**: This issue needs to be followed up on.
### Reason closed ### Reason closed
![](https://img.shields.io/badge/reason%20closed-resolved-99cc99.svg) ![](https://img.shields.io/badge/reason%20closed-resolved-99cc99.svg)
![](https://img.shields.io/badge/reason%20closed-duplicate-cc99cc.svg) ![](https://img.shields.io/badge/reason%20closed-duplicate-cc99cc.svg)
![](https://img.shields.io/badge/reason%20closed-declined-bb6666.svg) ![](https://img.shields.io/badge/reason%20closed-declined-bb6666.svg)
![](https://img.shields.io/badge/reason%20closed-not%20a%20bug-997744.svg) ![](https://img.shields.io/badge/reason%20closed-not%20a%20bug-997744.svg)
![](https://img.shields.io/badge/reason%20closed-inactivity-bfd4f2.svg) ![](https://img.shields.io/badge/reason%20closed-inactivity-bfd4f2.svg)
![](https://img.shields.io/badge/reason%20closed-no%20issue-c5def5.svg) ![](https://img.shields.io/badge/reason%20closed-no%20issue-c5def5.svg)
- **resolved**: The question was answered, the bug was fixed, or the feature was implemented.
- **duplicate**: Someone has already posted the same or a very similar issue. A comment should be added that references the original issue. * **resolved**: The question was answered, the bug was fixed, or the feature was implemented.
- **declined**: This feature will not be implemented. * **duplicate**: Someone has already posted the same or a very similar issue. A comment should be added that references the original issue.
- **not a bug**: This is not a bug, but either user error or intended behavior. * **declined**: This feature will not be implemented.
- **inactivity**: There was not enough info to reproduce the bug or not enough interest in the feature to hash out an implementation plan and the conversation has stalled. * **not a bug**: This is not a bug, but either user error or intended behavior.
- **no issue**: This wasn't so much an issue as a comment * **inactivity**: There was not enough info to reproduce the bug or not enough interest in the feature to hash out an implementation plan and the conversation has stalled.
* **no issue**: This wasn't so much an issue as a comment
### Other ### Other
![](https://img.shields.io/badge/-good%20first%20issue-00cccc.svg) ![](https://img.shields.io/badge/-good%20first%20issue-00cccc.svg)
![](https://img.shields.io/badge/-help%20wanted-33cc88.svg) ![](https://img.shields.io/badge/-help%20wanted-33cc88.svg)
![](https://img.shields.io/badge/-blocked-6b0c0c.svg) ![](https://img.shields.io/badge/-blocked-6b0c0c.svg)
![](https://img.shields.io/badge/-needs%20more%20info-dd9944.svg) ![](https://img.shields.io/badge/-needs%20more%20info-dd9944.svg)
![](https://img.shields.io/badge/-user%20land-e8c9c9.svg) ![](https://img.shields.io/badge/-user%20land-e8c9c9.svg)
- **good first issue**: Small tasks that would be good for first time contributors. * **good first issue**: Small tasks that would be good for first time contributors.
- **help wanted**: Not on the roadmap, but we'd love for someone in the community to tackle it. * **help wanted**: Not on the roadmap, but we'd love for someone in the community to tackle it.
- **blocked**: Cannot be completed until something else happens first. This should be described in a comment with a link to the blocking issue. * **blocked**: Cannot be completed until something else happens first. This should be described in a comment with a link to the blocking issue.
- **needs more info**: The original poster needs to provide more information before action can be taken. * **needs more info**: The original poster needs to provide more information before action can be taken.
- **user land**: Something that probably won't be added to core, but could be implemented/proved out as a separate module. * **user land**: Something that probably won't be added to core, but could be implemented/proved out as a separate module.

View File

@ -1,40 +1,52 @@
<!----------------------------------------------------------------------- <!-----------------------------------------------------------------------
| IF BUG REPORT (skip to next section for feature suggestion) | | IF BUG REPORT (skip to next section for feature suggestion) |
-----------------------------------------------------------------------> ----------------------------------------------------------------------->
## Bug Report ## Bug Report
### Marko Version: x.x.x ### Marko Version: x.x.x
<!--- Provide the exact version of marko in which you see the bug. You can run `npm ls marko` to see this. --> <!--- Provide the exact version of marko in which you see the bug. You can run `npm ls marko` to see this. -->
### Details ### Details
<!--- Provide a more detailed introduction to the issue itself, and why you consider it to be a bug. How has this bug affected you? What were you trying to accomplish? --> <!--- Provide a more detailed introduction to the issue itself, and why you consider it to be a bug. How has this bug affected you? What were you trying to accomplish? -->
### Expected Behavior ### Expected Behavior
<!--- Tell us what should happen --> <!--- Tell us what should happen -->
### Actual Behavior ### Actual Behavior
<!--- Tell us what happens instead --> <!--- Tell us what happens instead -->
### Possible Fix ### Possible Fix
<!--- Not obligatory, but suggest a fix or reason for the bug --> <!--- Not obligatory, but suggest a fix or reason for the bug -->
<details><summary>Additional Info</summary> <details><summary>Additional Info</summary>
### Your Environment ### Your Environment
<!-- Include as many relevant details about the environment you experienced the bug in --> <!-- Include as many relevant details about the environment you experienced the bug in -->
* Environment name and version (e.g. Chrome 39, node.js 5.4): * Environment name and version (e.g. Chrome 39, node.js 5.4):
* Operating System and version (desktop or mobile): * Operating System and version (desktop or mobile):
* Link to your project: * Link to your project:
### Steps to Reproduce ### Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to --> <!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug include code to reproduce, if relevant --> <!--- reproduce this bug include code to reproduce, if relevant -->
1.
2. 1. first...
3. 2.
4. 3.
4.
### Stack Trace ### Stack Trace
<!-- If an error is thrown, provide the stack trace here --> <!-- If an error is thrown, provide the stack trace here -->
</details> </details>
@ -42,18 +54,25 @@
<!-------------------------------- <!--------------------------------
| IF FEATURE SUGGESTION | | IF FEATURE SUGGESTION |
--------------------------------> -------------------------------->
## New Feature ## New Feature
### Description ### Description
<!--- Provide a detailed description of the change or addition you are proposing --> <!--- Provide a detailed description of the change or addition you are proposing -->
### Why ### Why
<!--- Why is this change important to you? How would you use it? --> <!--- Why is this change important to you? How would you use it? -->
<!--- How can it benefit other users? --> <!--- How can it benefit other users? -->
### Possible Implementation & Open Questions ### Possible Implementation & Open Questions
<!--- Not obligatory, but suggest an idea for implementing addition or change --> <!--- Not obligatory, but suggest an idea for implementing addition or change -->
<!--- What still needs to be discussed --> <!--- What still needs to be discussed -->
### Is this something you're interested in working on? ### Is this something you're interested in working on?
<!--- Yes or no --> <!--- Yes or no -->

View File

@ -1,21 +1,27 @@
<!--- Provide a general summary of your changes in the Title above --> <!--- Provide a general summary of your changes in the Title above -->
## Description ## Description
<!--- Describe your changes in detail --> <!--- Describe your changes in detail -->
## Motivation and Context ## Motivation and Context
<!--- Why is this change required? What problem does it solve? --> <!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. --> <!--- If it fixes an open issue, please link to the issue here. -->
## Screenshots (if appropriate): ## Screenshots (if appropriate):
## Checklist: ## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project.
- [ ] I have updated/added documentation affected by my changes. * [ ] My code follows the code style of this project.
- [ ] I have read the **CONTRIBUTING** document. * [ ] I have updated/added documentation affected by my changes.
- [ ] I have added tests to cover my changes. * [ ] I have read the **CONTRIBUTING** document.
- [ ] All new and existing tests passed. * [ ] I have added tests to cover my changes.
* [ ] All new and existing tests passed.
_Disclaimer: Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so._ _Disclaimer: Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so._

View File

@ -1,2 +0,0 @@
/src/taglibs/async/client-reorder-runtime.js
/src/taglibs/async/client-reorder-runtime.min.js

View File

@ -1,33 +0,0 @@
{
"predef": [
"document",
"ShadowRoot"
],
"node" : true,
"esnext": true,
"boss" : false,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": true,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": true,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": false,
"white": false,
"eqeqeq": false,
"latedef": "func",
"unused": "vars",
"strict": false,
"eqnull": true
}

22
.prettierignore Normal file
View File

@ -0,0 +1,22 @@
# minfied
*.min.js
# tests
*actual*
*expected*
input.*
# generated
/dist/
/test-dist/
/test-generated/
*.marko.js
*.html.js
*.xml.js
*.generated.js
.nyc_output
coverage
# controlled by npm's formatter
package-lock.json
package.json

File diff suppressed because it is too large Load Diff

View File

@ -13,12 +13,12 @@ Learn more on [markojs.com](http://markojs.com/), and even [Try Marko Online!](h
# Get Involved # Get Involved
- **Contributing**: Pull requests are welcome! * **Contributing**: Pull requests are welcome!
- Read [`CONTRIBUTING.md`](.github/CONTRIBUTING.md) and check out our [bite-sized](https://github.com/marko-js/marko/issues?q=is%3Aissue+is%3Aopen+label%3Adifficulty%3Abite-sized) and [help-wanted](https://github.com/marko-js/marko/issues?q=is%3Aissue+is%3Aopen+label%3Astatus%3Ahelp-wanted) issues * Read [`CONTRIBUTING.md`](.github/CONTRIBUTING.md) and check out our [bite-sized](https://github.com/marko-js/marko/issues?q=is%3Aissue+is%3Aopen+label%3Adifficulty%3Abite-sized) and [help-wanted](https://github.com/marko-js/marko/issues?q=is%3Aissue+is%3Aopen+label%3Astatus%3Ahelp-wanted) issues
- Submit github issues for any feature enhancements, bugs or documentation problems * Submit github issues for any feature enhancements, bugs or documentation problems
- **Support**: Join our [gitter chat](https://gitter.im/marko-js/marko) to ask questions to get support from the maintainers and other Marko developers * **Support**: Join our [gitter chat](https://gitter.im/marko-js/marko) to ask questions to get support from the maintainers and other Marko developers
- Questions/comments can also be posted as [github issues](https://github.com/marko-js/marko/issues) * Questions/comments can also be posted as [github issues](https://github.com/marko-js/marko/issues)
- **Discuss**: Tweet using the `#MarkoJS` hashtag and follow [@MarkoDevTeam](https://twitter.com/MarkoDevTeam) * **Discuss**: Tweet using the `#MarkoJS` hashtag and follow [@MarkoDevTeam](https://twitter.com/MarkoDevTeam)
# Installation # Installation
@ -39,7 +39,8 @@ the Marko.js website.
The following single-file component renders a button and a counter with the The following single-file component renders a button and a counter with the
number of times the button has been clicked. [Try this example now!](http://markojs.com/try-online/?file=%2Fcomponents%2Fcomponents%2Fclick-count%2Findex.marko) number of times the button has been clicked. [Try this example now!](http://markojs.com/try-online/?file=%2Fcomponents%2Fcomponents%2Fclick-count%2Findex.marko)
__click-count.marko__ **click-count.marko**
```marko ```marko
class { class {
onCreate() { onCreate() {
@ -75,7 +76,8 @@ The same component as above split into an `index.marko` template file,
`component.js` containing your component logic, and `style.css` containing your `component.js` containing your component logic, and `style.css` containing your
component style: component style:
__index.marko__ **index.marko**
```marko ```marko
<div.count> <div.count>
${state.count} ${state.count}
@ -85,27 +87,29 @@ __index.marko__
</button> </button>
``` ```
__component.js__ **component.js**
```js ```js
module.exports = { module.exports = {
onCreate() { onCreate() {
this.state = { count:0 }; this.state = { count: 0 };
}, },
increment() { increment() {
this.state.count++; this.state.count++;
} }
}; };
``` ```
__style.css__ **style.css**
```css ```css
.count { .count {
color:#09c; color: #09c;
font-size:3em; font-size: 3em;
} }
.example-button { .example-button {
font-size:1em; font-size: 1em;
padding:0.5em; padding: 0.5em;
} }
``` ```

8
benchmark/.eslintrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"rules": {
"no-console": 0
},
"parserOptions": {
"sourceType": "module"
}
}

View File

@ -1,40 +0,0 @@
{
"predef": [
],
"globals": {
"define": true,
"require": true
},
"node" : true,
"esnext" : true,
"browser" : true,
"boss" : false,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": true,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": true,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": false,
"white": false,
"eqeqeq": false,
"latedef": false,
"unused": "vars",
"jquery": true,
"strict": false,
"eqnull": true
}

View File

@ -1,20 +1,21 @@
var nodePath = require('path'); var nodePath = require("path");
var Module = require('module').Module; var Module = require("module").Module;
var oldResolveFilename = Module._resolveFilename; var oldResolveFilename = Module._resolveFilename;
var rootDir = nodePath.join(__dirname, '../'); var rootDir = nodePath.join(__dirname, "../");
Module._resolveFilename = function(request, parent, isMain) { Module._resolveFilename = function(request, parent, isMain) {
if (request.charAt(0) !== '.') { if (request.charAt(0) !== ".") {
var firstSlash = request.indexOf('/'); var firstSlash = request.indexOf("/");
var targetPackageName = firstSlash === -1 ? request : request.substring(0, firstSlash); var targetPackageName =
firstSlash === -1 ? request : request.substring(0, firstSlash);
if (targetPackageName === 'marko') { if (targetPackageName === "marko") {
request = request.substring('marko'.length); request = request.substring("marko".length);
request = rootDir + request; request = rootDir + request;
} }
} }
return oldResolveFilename.call(this, request, parent, isMain); return oldResolveFilename.call(this, request, parent, isMain);
}; };

View File

@ -1,37 +1,32 @@
import commonjsPlugin from 'rollup-plugin-commonjs'; import browserifyPlugin from "rollup-plugin-browserify-transform";
import browserifyPlugin from 'rollup-plugin-browserify-transform'; import nodeResolvePlugin from "rollup-plugin-node-resolve";
import nodeResolvePlugin from 'rollup-plugin-node-resolve'; import babelPlugin from "rollup-plugin-babel";
import babelPlugin from 'rollup-plugin-babel'; import envify from "envify";
import envify from 'envify'; import path from "path";
import path from 'path';
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = "production";
// NODE_ENV=production browserify -t envify -t markoify --extension='.marko' --global-transform minprops/browserify -o build/bundles/marko.js marko/client.js // NODE_ENV=production browserify -t envify -t markoify --extension='.marko' --global-transform minprops/browserify -o build/bundles/marko.js marko/client.js
export default { export default {
entry: path.join(__dirname, 'client.jsx'), entry: path.join(__dirname, "client.jsx"),
format: 'iife', format: "iife",
moduleName: 'app', moduleName: "app",
plugins: [ plugins: [
babelPlugin({ babelPlugin({
include: [], include: [],
babelrc: false, babelrc: false,
"presets": [ presets: [["es2015", { loose: true, modules: false }], "stage-0"],
["es2015", { "loose": true, "modules": false }], plugins: ["inferno"]
"stage-0"
],
"plugins": ["inferno"]
}), }),
browserifyPlugin(envify), browserifyPlugin(envify),
nodeResolvePlugin({ nodeResolvePlugin({
jsnext: false, // Default: false jsnext: false, // Default: false
main: true, // Default: true main: true, // Default: true
browser: true, // Default: false browser: true, // Default: false
preferBuiltins: false, preferBuiltins: false,
extensions: [ '.js', '.jsx' ] extensions: [".js", ".jsx"]
}) })
], ],
dest: path.join(__dirname, '../build/bundles/inferno.js') dest: path.join(__dirname, "../build/bundles/inferno.js")
}; };

View File

@ -1,6 +1,7 @@
var app = require('./components/app'); var app = require("./components/app");
app.renderSync({ app
name: 'Frank', .renderSync({
colors: ['red', 'green', 'blue'] name: "Frank",
colors: ["red", "green", "blue"]
}) })
.appendTo(document.body); .appendTo(document.body);

View File

@ -1,3 +1,3 @@
var helpers = require('marko/src/runtime/vdom/helpers'); var helpers = require("marko/src/runtime/vdom/helpers");
console.log('HELPERS:', helpers); console.log("HELPERS:", helpers);

View File

@ -1,35 +1,34 @@
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = "production";
import commonjsPlugin from 'rollup-plugin-commonjs'; import commonjsPlugin from "rollup-plugin-commonjs";
import browserifyPlugin from 'rollup-plugin-browserify-transform'; import browserifyPlugin from "rollup-plugin-browserify-transform";
import nodeResolvePlugin from 'rollup-plugin-node-resolve'; import nodeResolvePlugin from "rollup-plugin-node-resolve";
import markoify from 'markoify'; import markoify from "markoify";
import envify from 'envify'; import envify from "envify";
import minpropsify from 'minprops/browserify'; import minpropsify from "minprops/browserify";
import path from 'path'; import path from "path";
// NODE_ENV=production browserify -t envify -t markoify --extension='.marko' --global-transform minprops/browserify -o build/bundles/marko.js marko/client.js // NODE_ENV=production browserify -t envify -t markoify --extension='.marko' --global-transform minprops/browserify -o build/bundles/marko.js marko/client.js
export default { export default {
entry: path.join(__dirname, 'client.js'), entry: path.join(__dirname, "client.js"),
format: 'iife', format: "iife",
moduleName: 'app', moduleName: "app",
plugins: [ plugins: [
browserifyPlugin(markoify), browserifyPlugin(markoify),
browserifyPlugin(envify), browserifyPlugin(envify),
browserifyPlugin(minpropsify), browserifyPlugin(minpropsify),
nodeResolvePlugin({ nodeResolvePlugin({
jsnext: false, // Default: false jsnext: false, // Default: false
main: true, // Default: true main: true, // Default: true
browser: true, // Default: false browser: true, // Default: false
preferBuiltins: false, preferBuiltins: false,
extensions: [ '.js', '.marko' ] extensions: [".js", ".marko"]
}), }),
commonjsPlugin({ commonjsPlugin({
include: [], include: [],
extensions: [ '.js', '.marko' ] extensions: [".js", ".marko"]
}) })
], ],
dest: path.join(__dirname, '../build/bundles/marko.js') dest: path.join(__dirname, "../build/bundles/marko.js")
}; };

View File

@ -1,28 +1,30 @@
console.log('Minifying JavaScript bundles...'); console.log("Minifying JavaScript bundles...");
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
const zlib = require('zlib'); const zlib = require("zlib");
const UglifyJS = require("uglify-js"); const UglifyJS = require("uglify-js");
const formatNumber = require('format-number')(); const formatNumber = require("format-number")();
var buildDir = path.join(__dirname, 'build'); var buildDir = path.join(__dirname, "build");
var bundlesDir = path.join(__dirname, 'build/bundles'); var bundlesDir = path.join(__dirname, "build/bundles");
var bundlesMinDir = path.join(__dirname, 'build/bundles.min'); var bundlesMinDir = path.join(__dirname, "build/bundles.min");
try { try {
fs.mkdirSync(bundlesMinDir); fs.mkdirSync(bundlesMinDir);
} catch(e) {} } catch (e) {
/* ignore error */
}
var promiseChain = Promise.resolve(); var promiseChain = Promise.resolve();
function getVersion(name) { function getVersion(name) {
return require(name + '/package.json').version; return require(name + "/package.json").version;
} }
function leftPad(str, padding) { function leftPad(str, padding) {
if (str.length < padding) { if (str.length < padding) {
str = new Array(padding - str.length).join(' ') + str; str = new Array(padding - str.length).join(" ") + str;
} }
return str; return str;
@ -30,10 +32,10 @@ function leftPad(str, padding) {
var minifiers = { var minifiers = {
gcc: function minifyGCC(src, file) { gcc: function minifyGCC(src, file) {
const gcc = require('google-closure-compiler-js'); const gcc = require("google-closure-compiler-js");
const options = { const options = {
jsCode: [{src: src}], jsCode: [{ src: src }],
languageIn: 'ES5' languageIn: "ES5"
}; };
const out = gcc.compile(options); const out = gcc.compile(options);
@ -49,7 +51,7 @@ var minifiers = {
return UglifyJS.minify(src, { return UglifyJS.minify(src, {
fromString: true fromString: true
}).code; }).code;
} catch(e) { } catch (e) {
if (e.line != null) { if (e.line != null) {
console.error(`Failed to minify ${file}`); console.error(`Failed to minify ${file}`);
console.error(` Location: ${file}:${e.line}:${e.col}`); console.error(` Location: ${file}:${e.line}:${e.col}`);
@ -58,7 +60,6 @@ var minifiers = {
} }
throw e; throw e;
} }
}, },
both: function(src, file) { both: function(src, file) {
var withGCC = minifiers.gcc(src, file); var withGCC = minifiers.gcc(src, file);
@ -75,14 +76,14 @@ var sizes = {};
var targetLib = process.argv[2]; var targetLib = process.argv[2];
bundleFiles.forEach((filename) => { bundleFiles.forEach(filename => {
if (!filename.endsWith('.js')) { if (!filename.endsWith(".js")) {
return; return;
} }
var file = path.join(bundlesDir, filename); var file = path.join(bundlesDir, filename);
var ext = path.extname(filename); var ext = path.extname(filename);
var lib = filename.slice(0, 0-ext.length); var lib = filename.slice(0, 0 - ext.length);
if (targetLib && lib !== targetLib) { if (targetLib && lib !== targetLib) {
return; return;
@ -90,16 +91,16 @@ bundleFiles.forEach((filename) => {
console.log(`Minifying ${file}...`); console.log(`Minifying ${file}...`);
var src = fs.readFileSync(file, { encoding: 'utf8' }); var src = fs.readFileSync(file, { encoding: "utf8" });
var minifiedSrc = minifier(src, file); var minifiedSrc = minifier(src, file);
console.log(`Done minifying ${file}`); console.log(`Done minifying ${file}`);
var minFile = path.join(bundlesMinDir, filename); var minFile = path.join(bundlesMinDir, filename);
fs.writeFileSync(minFile, minifiedSrc, { encoding: 'utf8' }); fs.writeFileSync(minFile, minifiedSrc, { encoding: "utf8" });
var sizeInfo = sizes[lib] = {}; var sizeInfo = (sizes[lib] = {});
promiseChain = promiseChain.then(() => { promiseChain = promiseChain.then(() => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -110,16 +111,21 @@ bundleFiles.forEach((filename) => {
} }
// Compare the sizes // Compare the sizes
var minifiedBuffer = new Buffer(minifiedSrc, 'utf8'); var minifiedBuffer = new Buffer(minifiedSrc, "utf8");
// console.log(nodePath.basename(templateInfo.outputCompileMinifiedFile) + ': ' + gzippedBuffer.length + ' bytes gzipped (' + minifiedBuffer.length + ' bytes uncompressed)'); // console.log(nodePath.basename(templateInfo.outputCompileMinifiedFile) + ': ' + gzippedBuffer.length + ' bytes gzipped (' + minifiedBuffer.length + ' bytes uncompressed)');
sizeInfo.gzipped = gzippedBuffer.length; sizeInfo.gzipped = gzippedBuffer.length;
sizeInfo.min = minifiedBuffer.length; sizeInfo.min = minifiedBuffer.length;
var libVersion = getVersion(lib); var libVersion = getVersion(lib);
var sizeFilename = lib + (libVersion ? '-' + libVersion : '') + '.json'; var sizeFilename =
lib + (libVersion ? "-" + libVersion : "") + ".json";
fs.writeFileSync(path.join(buildDir, sizeFilename), JSON.stringify(sizeInfo, null, 4), { encoding: 'utf8' }); fs.writeFileSync(
path.join(buildDir, sizeFilename),
JSON.stringify(sizeInfo, null, 4),
{ encoding: "utf8" }
);
resolve(); resolve();
}); });
@ -132,11 +138,15 @@ promiseChain.then(() => {
for (var lib in sizes) { for (var lib in sizes) {
var sizeInfo = sizes[lib]; var sizeInfo = sizes[lib];
console.log('[' + lib + ']'); console.log("[" + lib + "]");
console.log(' gzip: ' + leftPad(formatNumber(sizeInfo.gzipped), 8) + ' bytes'); console.log(
console.log(' min: ' + leftPad(formatNumber(sizeInfo.min), 8) + ' bytes'); " gzip: " + leftPad(formatNumber(sizeInfo.gzipped), 8) + " bytes"
);
console.log(
" min: " + leftPad(formatNumber(sizeInfo.min), 8) + " bytes"
);
console.log(); console.log();
} }
console.log('Minification complete.'); console.log("Minification complete.");
}); });

View File

@ -1,65 +1,70 @@
{ {
"name": "size-benchmark", "name": "size-benchmark",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"setup": "npm install --silent && npm link ../../", "setup": "npm install --silent && npm link ../../",
"build": "npm run bundle --silent && npm run minify --silent", "build": "npm run bundle --silent && npm run minify --silent",
"build-marko": "npm run bundle-marko --silent && node minify.js marko", "build-marko": "npm run bundle-marko --silent && node minify.js marko",
"build-vue": "npm run bundle-vue --silent && node minify.js vue", "build-vue": "npm run bundle-vue --silent && node minify.js vue",
"build-react": "npm run bundle-react --silent && node minify.js react", "build-react": "npm run bundle-react --silent && node minify.js react",
"build-inferno": "npm run bundle-inferno --silent && node minify.js inferno", "build-inferno":
"bundle": "mkdir -p build/bundles && npm run bundle-marko && npm run bundle-react && npm run bundle-vue && npm run bundle-preact && npm run bundle-inferno", "npm run bundle-inferno --silent && node minify.js inferno",
"bundle-marko": "node ../../scripts/build src && NODE_ENV=production rollup -c marko/rollup.config.js", "bundle":
"bundle-react": "NODE_ENV=production rollup -c react/rollup.config.js", "mkdir -p build/bundles && npm run bundle-marko && npm run bundle-react && npm run bundle-vue && npm run bundle-preact && npm run bundle-inferno",
"bundle-preact": "NODE_ENV=production rollup -c preact/rollup.config.js", "bundle-marko":
"bundle-vue": "NODE_ENV=production rollup -c vue/rollup.config.js", "node ../../scripts/build src && NODE_ENV=production rollup -c marko/rollup.config.js",
"bundle-inferno": "NODE_ENV=production rollup -c inferno/rollup.config.js", "bundle-react": "NODE_ENV=production rollup -c react/rollup.config.js",
"minify": "node minify.js", "bundle-preact":
"http-server": "http-server" "NODE_ENV=production rollup -c preact/rollup.config.js",
}, "bundle-vue": "NODE_ENV=production rollup -c vue/rollup.config.js",
"author": "Patrick Steele-Idem <pnidem@gmail.com>", "bundle-inferno":
"license": "MIT", "NODE_ENV=production rollup -c inferno/rollup.config.js",
"dependencies": { "minify": "node minify.js",
"babel-plugin-inferno": "^1.8.0", "http-server": "http-server"
"babel-plugin-transform-es2015-block-scoping": "^6.21.0", },
"babel-plugin-transform-react-constant-elements": "^6.9.1", "author": "Patrick Steele-Idem <pnidem@gmail.com>",
"babel-plugin-transform-react-jsx": "^6.8.0", "license": "MIT",
"babel-plugin-transform-runtime": "^6.15.0", "dependencies": {
"babel-preset-es2015": "^6.18.0", "babel-plugin-inferno": "^1.8.0",
"babel-preset-es2015-loose": "^8.0.0", "babel-plugin-transform-es2015-block-scoping": "^6.21.0",
"babel-preset-react": "^6.16.0", "babel-plugin-transform-react-constant-elements": "^6.9.1",
"babel-preset-stage-0": "^6.16.0", "babel-plugin-transform-react-jsx": "^6.8.0",
"babelify": "^7.3.0", "babel-plugin-transform-runtime": "^6.15.0",
"envify": "^4.0.0", "babel-preset-es2015": "^6.18.0",
"format-number": "^2.0.1", "babel-preset-es2015-loose": "^8.0.0",
"google-closure-compiler-js": "^20161201.0.0", "babel-preset-react": "^6.16.0",
"http-server": "^0.9.0", "babel-preset-stage-0": "^6.16.0",
"inferno": "^1.3.0-rc.1", "babelify": "^7.3.0",
"inferno-component": "^1.3.0-rc.1", "envify": "^4.0.0",
"inferno-server": "^1.3.0-rc.1", "format-number": "^2.0.1",
"markoify": "^2.1.1", "google-closure-compiler-js": "^20161201.0.0",
"minprops": "^1.0.0", "http-server": "^0.9.0",
"preact": "^7.1.0", "inferno": "^1.3.0-rc.1",
"react": "^15.4.1", "inferno-component": "^1.3.0-rc.1",
"react-dom": "^15.4.1", "inferno-server": "^1.3.0-rc.1",
"rollup": "^0.41.6", "markoify": "^2.1.1",
"rollup-plugin-babel": "^2.7.1", "minprops": "^1.0.0",
"rollup-plugin-browserify-transform": "^0.1.0", "preact": "^7.1.0",
"rollup-plugin-commonjs": "^8.0.2", "react": "^15.4.1",
"rollup-plugin-marko": "0.0.2", "react-dom": "^15.4.1",
"rollup-plugin-node-resolve": "^3.0.0", "rollup": "^0.41.6",
"uglify-js": "^2.7.5", "rollup-plugin-babel": "^2.7.1",
"vue": "^2.1.6", "rollup-plugin-browserify-transform": "^0.1.0",
"vueify": "^9.4.0" "rollup-plugin-commonjs": "^8.0.2",
}, "rollup-plugin-marko": "0.0.2",
"repository": { "rollup-plugin-node-resolve": "^3.0.0",
"type": "git", "uglify-js": "^2.7.5",
"url": "https://github.com/marko-js/marko.git" "vue": "^2.1.6",
}, "vueify": "^9.4.0"
"browser": { },
"events": "events-light" "repository": {
}, "type": "git",
"devDependencies": {} "url": "https://github.com/marko-js/marko.git"
},
"browser": {
"events": "events-light"
},
"devDependencies": {}
} }

View File

@ -1,35 +1,34 @@
import commonjsPlugin from 'rollup-plugin-commonjs'; import commonjsPlugin from "rollup-plugin-commonjs";
import browserifyPlugin from 'rollup-plugin-browserify-transform'; import browserifyPlugin from "rollup-plugin-browserify-transform";
import nodeResolvePlugin from 'rollup-plugin-node-resolve'; import nodeResolvePlugin from "rollup-plugin-node-resolve";
import babelPlugin from 'rollup-plugin-babel'; import babelPlugin from "rollup-plugin-babel";
import envify from 'envify'; import envify from "envify";
import path from 'path'; import path from "path";
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = "production";
// NODE_ENV=production browserify -t envify -t markoify --extension='.marko' --global-transform minprops/browserify -o build/bundles/marko.js marko/client.js // NODE_ENV=production browserify -t envify -t markoify --extension='.marko' --global-transform minprops/browserify -o build/bundles/marko.js marko/client.js
export default { export default {
entry: path.join(__dirname, 'client.jsx'), entry: path.join(__dirname, "client.jsx"),
format: 'iife', format: "iife",
moduleName: 'app', moduleName: "app",
plugins: [ plugins: [
babelPlugin({ babelPlugin({
// include: ['node_modules/**', '**/*.js', '**/*.jsx'] // include: ['node_modules/**', '**/*.js', '**/*.jsx']
}), }),
browserifyPlugin(envify), browserifyPlugin(envify),
nodeResolvePlugin({ nodeResolvePlugin({
jsnext: false, // Default: false jsnext: false, // Default: false
main: true, // Default: true main: true, // Default: true
browser: true, // Default: false browser: true, // Default: false
preferBuiltins: false, preferBuiltins: false,
extensions: [ '.js', '.jsx' ] extensions: [".js", ".jsx"]
}), }),
commonjsPlugin({ commonjsPlugin({
include: [], include: [],
extensions: [ '.js', '.jsx' ] extensions: [".js", ".jsx"]
}) })
], ],
dest: path.join(__dirname, '../build/bundles/preact.js') dest: path.join(__dirname, "../build/bundles/preact.js")
}; };

View File

@ -1,35 +1,34 @@
import commonjsPlugin from 'rollup-plugin-commonjs'; import commonjsPlugin from "rollup-plugin-commonjs";
import browserifyPlugin from 'rollup-plugin-browserify-transform'; import browserifyPlugin from "rollup-plugin-browserify-transform";
import nodeResolvePlugin from 'rollup-plugin-node-resolve'; import nodeResolvePlugin from "rollup-plugin-node-resolve";
import babelPlugin from 'rollup-plugin-babel'; import babelPlugin from "rollup-plugin-babel";
import envify from 'envify'; import envify from "envify";
import path from 'path'; import path from "path";
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = "production";
// NODE_ENV=production browserify -t envify -t markoify --extension='.marko' --global-transform minprops/browserify -o build/bundles/marko.js marko/client.js // NODE_ENV=production browserify -t envify -t markoify --extension='.marko' --global-transform minprops/browserify -o build/bundles/marko.js marko/client.js
export default { export default {
entry: path.join(__dirname, 'client.jsx'), entry: path.join(__dirname, "client.jsx"),
format: 'iife', format: "iife",
moduleName: 'app', moduleName: "app",
plugins: [ plugins: [
babelPlugin({ babelPlugin({
exclude: 'node_modules/**' exclude: "node_modules/**"
}), }),
browserifyPlugin(envify), browserifyPlugin(envify),
nodeResolvePlugin({ nodeResolvePlugin({
jsnext: true, // Default: false jsnext: true, // Default: false
main: true, // Default: true main: true, // Default: true
browser: true, // Default: false browser: true, // Default: false
preferBuiltins: false, preferBuiltins: false,
extensions: [ '.js', '.jsx' ] extensions: [".js", ".jsx"]
}), }),
commonjsPlugin({ commonjsPlugin({
include: [], include: [],
extensions: [ '.js', '.jsx' ] extensions: [".js", ".jsx"]
}) })
], ],
dest: path.join(__dirname, '../build/bundles/react.js') dest: path.join(__dirname, "../build/bundles/react.js")
}; };

View File

@ -1,5 +1,5 @@
var Vue = require('vue'); var Vue = require("vue");
var App = require('./components/App'); var App = require("./components/App");
// var app = new App({ // var app = new App({
// el: document.body, // el: document.body,
@ -11,7 +11,7 @@ var App = require('./components/App');
new Vue({ new Vue({
el: document.body, el: document.body,
render: function (createElement) { render: function(createElement) {
return createElement(App); return createElement(App);
} }
}); });

View File

@ -1,33 +1,32 @@
import commonjsPlugin from 'rollup-plugin-commonjs'; import commonjsPlugin from "rollup-plugin-commonjs";
import browserifyPlugin from 'rollup-plugin-browserify-transform'; import browserifyPlugin from "rollup-plugin-browserify-transform";
import nodeResolvePlugin from 'rollup-plugin-node-resolve'; import nodeResolvePlugin from "rollup-plugin-node-resolve";
import vueify from 'vueify'; import vueify from "vueify";
import envify from 'envify'; import envify from "envify";
import minpropsify from 'minprops/browserify'; import minpropsify from "minprops/browserify";
import path from 'path'; import path from "path";
process.env.NODE_ENV = 'production';
process.env.NODE_ENV = "production";
export default { export default {
entry: path.join(__dirname, 'client.js'), entry: path.join(__dirname, "client.js"),
format: 'iife', format: "iife",
moduleName: 'app', moduleName: "app",
plugins: [ plugins: [
browserifyPlugin(vueify), browserifyPlugin(vueify),
browserifyPlugin(envify), browserifyPlugin(envify),
browserifyPlugin(minpropsify), browserifyPlugin(minpropsify),
nodeResolvePlugin({ nodeResolvePlugin({
jsnext: true, // Default: false jsnext: true, // Default: false
main: true, // Default: true main: true, // Default: true
browser: true, // Default: false browser: true, // Default: false
preferBuiltins: false, preferBuiltins: false,
extensions: [ '.js', '.vue' ] extensions: [".js", ".vue"]
}), }),
commonjsPlugin({ commonjsPlugin({
include: [], include: [],
extensions: [ '.js', '.vue' ] extensions: [".js", ".vue"]
}) })
], ],
dest: path.join(__dirname, '../build/bundles/vue.js') dest: path.join(__dirname, "../build/bundles/vue.js")
}; };

5
benchmark/vdom/.eslintrc Normal file
View File

@ -0,0 +1,5 @@
{
"env": {
"browser": true
}
}

View File

@ -1,24 +1,24 @@
module.exports = function(app) { module.exports = function(app) {
var Suite = window.Benchmark.Suite; var Suite = window.Benchmark.Suite;
var names = [ var names = ["dom", "dom-innerHTML", "marko-vdom", "react"];
'dom',
'dom-innerHTML',
'marko-vdom',
'react'
];
var htmlFiles = ['todomvc', 'marko-docs', 'tabs']; var htmlFiles = ["todomvc", "marko-docs", "tabs"];
function loadScripts() { function loadScripts() {
window.createBenchmarks = {}; window.createBenchmarks = {};
var scripts = []; var scripts = [];
names.forEach(function(name) { names.forEach(function(name) {
htmlFiles.forEach(function(htmlFile) { htmlFiles.forEach(function(htmlFile) {
scripts.push('./codegen-create/benchmark-' + htmlFile + '-' + name + '.js'); scripts.push(
"./codegen-create/benchmark-" +
htmlFile +
"-" +
name +
".js"
);
}); });
}); });
@ -26,18 +26,17 @@ module.exports = function(app) {
} }
function runForHtmlFile(htmlFile) { function runForHtmlFile(htmlFile) {
return loadScripts(htmlFile) return loadScripts(htmlFile).then(function() {
.then(function() { var suite = new Suite("create-" + htmlFile);
var suite = new Suite('create-' + htmlFile);
names.forEach(function(name) { names.forEach(function(name) {
suite.add(name, function() { suite.add(name, function() {
return window.createBenchmarks[htmlFile + '-' + name](); return window.createBenchmarks[htmlFile + "-" + name]();
});
}); });
return app.runSuite(suite);
}); });
return app.runSuite(suite);
});
} }
var loadScriptsPromise = loadScripts(); var loadScriptsPromise = loadScripts();

View File

@ -2,16 +2,15 @@ module.exports = function(app) {
var Suite = window.Benchmark.Suite; var Suite = window.Benchmark.Suite;
var MarkoVDOM = app.vdom; var MarkoVDOM = app.vdom;
var suite = new Suite('walk'); var suite = new Suite("walk");
var todomvcDOM = document.getElementById('todoapp'); var todomvcDOM = document.getElementById("todoapp");
var todomvcDOMVirtual = MarkoVDOM.virtualize(todomvcDOM); var todomvcDOMVirtual = MarkoVDOM.virtualize(todomvcDOM);
function toHTML(node) { function toHTML(node) {
// NOTE: We don't use XMLSerializer because we need to sort the attributes to correctly compare output HTML strings // NOTE: We don't use XMLSerializer because we need to sort the attributes to correctly compare output HTML strings
// BAD: return (new XMLSerializer()).serializeToString(node); // BAD: return (new XMLSerializer()).serializeToString(node);
var html = ''; var html = "";
function serializeHelper(node, indent) { function serializeHelper(node, indent) {
if (node.nodeType === 1) { if (node.nodeType === 1) {
serializeElHelper(node, indent); serializeElHelper(node, indent);
@ -20,7 +19,7 @@ module.exports = function(app) {
} else if (node.nodeType === 8) { } else if (node.nodeType === 8) {
serializeCommentHelper(node, indent); serializeCommentHelper(node, indent);
} else { } else {
console.log('Invalid node:', node); console.log("Invalid node:", node);
html += indent + `INVALID NODE TYPE ${node.nodeType}\n`; html += indent + `INVALID NODE TYPE ${node.nodeType}\n`;
// throw new Error('Unexpected node type'); // throw new Error('Unexpected node type');
} }
@ -29,66 +28,68 @@ module.exports = function(app) {
function serializeElHelper(el, indent) { function serializeElHelper(el, indent) {
var tagName = el.nodeName; var tagName = el.nodeName;
if (el.namespaceURI === 'http://www.w3.org/2000/svg') { if (el.namespaceURI === "http://www.w3.org/2000/svg") {
tagName = 'svg:' + tagName; tagName = "svg:" + tagName;
} else if (el.namespaceURI === 'http://www.w3.org/1998/Math/MathML') { } else if (
tagName = 'math:' + tagName; el.namespaceURI === "http://www.w3.org/1998/Math/MathML"
) {
tagName = "math:" + tagName;
} }
html += indent + '<' + tagName; html += indent + "<" + tagName;
var attributes = el.attributes; var attributes = el.attributes;
var attributesArray = []; var attributesArray = [];
for (var i=0; i<attributes.length; i++) { for (var i = 0; i < attributes.length; i++) {
var attr = attributes[i]; var attr = attributes[i];
var attrName = attr.name; var attrName = attr.name;
if (attr.namespaceURI) { if (attr.namespaceURI) {
attrName = attr.namespaceURI + ':' + attrName; attrName = attr.namespaceURI + ":" + attrName;
} }
attributesArray.push(' ' + attrName + '="' + attr.value + '"'); attributesArray.push(" " + attrName + '="' + attr.value + '"');
} }
attributesArray.sort(); attributesArray.sort();
html += attributesArray.join(''); html += attributesArray.join("");
html += '>\n'; html += ">\n";
if (tagName.toUpperCase() === 'TEXTAREA') { if (tagName.toUpperCase() === "TEXTAREA") {
html += indent + ' VALUE: ' + JSON.stringify(el.value) + '\n'; html += indent + " VALUE: " + JSON.stringify(el.value) + "\n";
} else { } else {
var curChild = el.firstChild; var curChild = el.firstChild;
while(curChild) { while (curChild) {
serializeHelper(curChild, indent + ' '); serializeHelper(curChild, indent + " ");
curChild = curChild.nextSibling; curChild = curChild.nextSibling;
} }
} }
} }
function serializeTextHelper(node, indent) { function serializeTextHelper(node, indent) {
html += indent + JSON.stringify(node.nodeValue) + '\n'; html += indent + JSON.stringify(node.nodeValue) + "\n";
} }
function serializeCommentHelper(node, indent) { function serializeCommentHelper(node, indent) {
html += indent + '<!--' + JSON.stringify(node.nodeValue) + '-->\n'; html += indent + "<!--" + JSON.stringify(node.nodeValue) + "-->\n";
} }
serializeHelper(node, ''); serializeHelper(node, "");
return html; return html;
} }
// add tests // add tests
suite.add('real DOM', function() { suite.add("real DOM", function() {
return toHTML(todomvcDOM); return toHTML(todomvcDOM);
}); });
suite.add('marko-vdom', function() { suite.add("marko-vdom", function() {
return toHTML(todomvcDOMVirtual); return toHTML(todomvcDOMVirtual);
}); });
return function() { return function() {
return app.runSuite(suite); return app.runSuite(suite);
}; };
}; };

View File

@ -6,13 +6,15 @@
}, },
{ {
"type": "js", "type": "js",
"url": "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.js" "url":
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.js"
}, },
{ {
"type": "js", "type": "js",
"url": "https://cdnjs.cloudflare.com/ajax/libs/benchmark/2.1.2/benchmark.js" "url":
"https://cdnjs.cloudflare.com/ajax/libs/benchmark/2.1.2/benchmark.js"
}, },
"benchmark/benchmark.js", "benchmark/benchmark.js",
"require-run: ./client.js" "require-run: ./client.js"
] ]
} }

View File

@ -1,17 +1,17 @@
var HTMLElement = require('../../runtime/vdom/HTMLElement'); var HTMLElement = require("../../runtime/vdom/HTMLElement");
var Text = require('../../runtime/vdom/Text'); var Text = require("../../runtime/vdom/Text");
var Comment = require('../../runtime/vdom/Comment'); var Comment = require("../../runtime/vdom/Comment");
var DocumentFragment = require('../../runtime/vdom/DocumentFragment'); var DocumentFragment = require("../../runtime/vdom/DocumentFragment");
var resultsEl = document.getElementById('results'); var resultsEl = document.getElementById("results");
var running = false; var running = false;
var benchmarks = {}; var benchmarks = {};
function loadScript(path) { function loadScript(path) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var script = document.createElement('script'); var script = document.createElement("script");
script.src = path; script.src = path;
script.onload = function () { script.onload = function() {
resolve(); resolve();
}; };
@ -24,9 +24,11 @@ function loadScript(path) {
} }
function loadScripts(paths) { function loadScripts(paths) {
return Promise.all(paths.map(function(path) { return Promise.all(
return loadScript(path); paths.map(function(path) {
})); return loadScript(path);
})
);
} }
function runSuite(suite) { function runSuite(suite) {
@ -38,33 +40,36 @@ function runSuite(suite) {
running = true; running = true;
suite suite
.on('start', function(event) { .on("start", function() {
resultsEl.innerHTML += 'Running "' + suite.name + '"...\n'; resultsEl.innerHTML += 'Running "' + suite.name + '"...\n';
}) })
.on('cycle', function(event) { .on("cycle", function(event) {
resultsEl.innerHTML += String(event.target) + '\n'; resultsEl.innerHTML += String(event.target) + "\n";
}) })
.on('complete', function() { .on("complete", function() {
resultsEl.innerHTML += 'Fastest is ' + this.filter('fastest').map('name') + '\n\n--------------\n\n'; resultsEl.innerHTML +=
"Fastest is " +
this.filter("fastest").map("name") +
"\n\n--------------\n\n";
running = false; running = false;
suite.off('start cycle complete'); suite.off("start cycle complete");
resolve(); resolve();
}) })
.on('error', function(e) { .on("error", function(e) {
running = false; running = false;
suite.off('start cycle complete error'); suite.off("start cycle complete error");
reject(e.target.error); reject(e.target.error);
}) })
// run async // run async
.run({ 'async': true }); .run({ async: true });
}); });
} }
var vdom = window.MarkoVDOM = { var vdom = (window.MarkoVDOM = {
virtualize: require('../../runtime/vdom/virtualize'), virtualize: require("../../runtime/vdom/virtualize"),
createElement: function(tagName, attrs, childCount, constId) { createElement: function(tagName, attrs, childCount, constId) {
return new HTMLElement(tagName, attrs, childCount, constId); return new HTMLElement(tagName, attrs, childCount, constId);
@ -78,7 +83,7 @@ var vdom = window.MarkoVDOM = {
createDocumentFragment: function() { createDocumentFragment: function() {
return new DocumentFragment(); return new DocumentFragment();
} }
}; });
var app = { var app = {
loadScript, loadScript,
@ -91,19 +96,19 @@ function registerBenchmark(name, func) {
benchmarks[name] = func(app); benchmarks[name] = func(app);
} }
registerBenchmark('create', require('./benchmark-create')); registerBenchmark("create", require("./benchmark-create"));
registerBenchmark('walk', require('./benchmark-walk')); registerBenchmark("walk", require("./benchmark-walk"));
document.body.addEventListener('click', function(event) { document.body.addEventListener("click", function(event) {
if (running) { if (running) {
return; return;
} }
var target = event.target; var target = event.target;
var benchmarkName = target.getAttribute('data-benchmark'); var benchmarkName = target.getAttribute("data-benchmark");
if (benchmarkName) { if (benchmarkName) {
var oldButtonLabel = target.innerHTML; var oldButtonLabel = target.innerHTML;
target.innerHTML = oldButtonLabel + ' - running...'; target.innerHTML = oldButtonLabel + " - running...";
resultsEl.innerHTML = ''; resultsEl.innerHTML = "";
var benchmarkFunc = benchmarks[benchmarkName]; var benchmarkFunc = benchmarks[benchmarkName];
@ -111,7 +116,7 @@ document.body.addEventListener('click', function(event) {
.then(function() { .then(function() {
target.innerHTML = oldButtonLabel; target.innerHTML = oldButtonLabel;
resultsEl.innerHTML += '\nDONE!'; resultsEl.innerHTML += "\nDONE!";
}) })
.catch(function(e) { .catch(function(e) {
target.innerHTML = oldButtonLabel; target.innerHTML = oldButtonLabel;
@ -119,4 +124,4 @@ document.body.addEventListener('click', function(event) {
resultsEl.innerHTML = e.toString(); resultsEl.innerHTML = e.toString();
}); });
} }
}); });

View File

@ -1,4 +1,4 @@
module.exports = function(node, html) { module.exports = function() {
return ` return `
var fragment = range.createContextualFragment(html); var fragment = range.createContextualFragment(html);
return fragment.childNodes[0];`; return fragment.childNodes[0];`;
@ -10,4 +10,4 @@ var range = document.createRange();
range.selectNode(document.body); range.selectNode(document.body);
var html = ${JSON.stringify(html)}; var html = ${JSON.stringify(html)};
`; `;
}; };

View File

@ -1,29 +1,33 @@
module.exports = function(node) { module.exports = function(node) {
var nextId = 0; var nextId = 0;
var code = ''; var code = "";
function codegenEl(node, level) { function codegenEl(node, level) {
var varName = level === 0 ? 'root' : `node${nextId++}`; var varName = level === 0 ? "root" : `node${nextId++}`;
code += `var ${varName} = document.createElement(${JSON.stringify(node.nodeName)})\n`; code += `var ${varName} = document.createElement(${JSON.stringify(
node.nodeName
)})\n`;
var attributes = node.attributes; var attributes = node.attributes;
for (var i=0; i<attributes.length; i++) { for (var i = 0; i < attributes.length; i++) {
var attr = attributes[i]; var attr = attributes[i];
code += `${varName}.setAttribute(${JSON.stringify(attr.name)}, ${JSON.stringify(attr.value)})\n`; code += `${varName}.setAttribute(${JSON.stringify(
attr.name
)}, ${JSON.stringify(attr.value)})\n`;
} }
var curChild = node.firstChild; var curChild = node.firstChild;
while(curChild) { while (curChild) {
if (curChild.nodeType === 1) { if (curChild.nodeType === 1) {
var childVarName = codegenEl(curChild, level + 1); var childVarName = codegenEl(curChild, level + 1);
code += `${varName}.appendChild(${childVarName})\n`; code += `${varName}.appendChild(${childVarName})\n`;
} else if (curChild.nodeType === 3) { } else if (curChild.nodeType === 3) {
code += `${varName}.appendChild(document.createTextNode(${JSON.stringify(curChild.nodeValue)}))\n`; code += `${varName}.appendChild(document.createTextNode(${JSON.stringify(
curChild.nodeValue
)}))\n`;
} }
curChild = curChild.nextSibling; curChild = curChild.nextSibling;
@ -34,7 +38,7 @@ module.exports = function(node) {
codegenEl(node, 0); codegenEl(node, 0);
code += '\nreturn root;\n'; code += "\nreturn root;\n";
return code; return code;
}; };

View File

@ -1,40 +1,42 @@
function indentStr(level) { function indentStr(level) {
var str = ''; var str = "";
for (var i=0; i<level; i++) { for (var i = 0; i < level; i++) {
str += ' '; str += " ";
} }
return str; return str;
} }
module.exports = function(node) { module.exports = function(node) {
var code = ''; var code = "";
function codegenEl(node, level) { function codegenEl(node, level) {
if (level === 0) { if (level === 0) {
code += 'createElement'; code += "createElement";
} else { } else {
code += '.e'; code += ".e";
} }
var attrsMap = {}; var attrsMap = {};
var attributes = node.attributes; var attributes = node.attributes;
for (var i=0; i<attributes.length; i++) { for (var i = 0; i < attributes.length; i++) {
var attr = attributes[i]; var attr = attributes[i];
attrsMap[attr.name] = attr.value; attrsMap[attr.name] = attr.value;
} }
code += `(${JSON.stringify(node.nodeName)}, ${JSON.stringify(attrsMap)}, ${node.childNodes.length})\n`; code += `(${JSON.stringify(node.nodeName)}, ${JSON.stringify(
attrsMap
)}, ${node.childNodes.length})\n`;
// codegenAttrs(node.attributes, level + 1); // codegenAttrs(node.attributes, level + 1);
var curChild = node.firstChild; var curChild = node.firstChild;
while(curChild) { while (curChild) {
codegen(curChild, level + 1); codegen(curChild, level + 1);
curChild = curChild.nextSibling; curChild = curChild.nextSibling;
} }
} }
function codegenText(node, level) { function codegenText(node) {
code += `.t(${JSON.stringify(node.nodeValue)})\n`; code += `.t(${JSON.stringify(node.nodeValue)})\n`;
} }
@ -50,14 +52,12 @@ module.exports = function(node) {
codegen(node, 0); codegen(node, 0);
return 'return ' + code + '\n'; return "return " + code + "\n";
}; };
module.exports.generateInitCode = function(node, html) { module.exports.generateInitCode = function() {
return ` return `
var MarkoVDOM = window.MarkoVDOM; var MarkoVDOM = window.MarkoVDOM;
var createElement = MarkoVDOM.createElement; var createElement = MarkoVDOM.createElement;
`; `;
}; };

View File

@ -1,7 +1,7 @@
function indentStr(level) { function indentStr(level) {
var str = ''; var str = "";
for (var i=0; i<level; i++) { for (var i = 0; i < level; i++) {
str += ' '; str += " ";
} }
return str; return str;
@ -9,54 +9,55 @@ function indentStr(level) {
module.exports = function(node) { module.exports = function(node) {
function codegenChildNodes(node, level) { function codegenChildNodes(node, level) {
var curChild = node.firstChild; var curChild = node.firstChild;
if (!curChild) { if (!curChild) {
return 'null'; return "null";
} }
var code = '[\n'; var code = "[\n";
var childLevel = level + 2; var childLevel = level + 2;
while(curChild) { while (curChild) {
code += indentStr(childLevel) + codegen(curChild, childLevel); code += indentStr(childLevel) + codegen(curChild, childLevel);
curChild = curChild.nextSibling; curChild = curChild.nextSibling;
if (curChild) { if (curChild) {
code += ',\n'; code += ",\n";
} else { } else {
code += '\n'; code += "\n";
} }
} }
code += indentStr(level + 1) + ']'; code += indentStr(level + 1) + "]";
return code; return code;
} }
function codegenEl(node, level) { function codegenEl(node, level) {
var attributesMap = null; var attributesMap = null;
var attributes = node.attributes; var attributes = node.attributes;
if (attributes.length) { if (attributes.length) {
attributesMap = {}; attributesMap = {};
for (var i=0; i<attributes.length; i++) { for (var i = 0; i < attributes.length; i++) {
var attr = attributes[i]; var attr = attributes[i];
var attrName = attr.name; var attrName = attr.name;
var attrValue = attr.value; var attrValue = attr.value;
if (attrName === 'class') { if (attrName === "class") {
attrName = 'className'; attrName = "className";
} }
attributesMap[attrName] = attrValue; attributesMap[attrName] = attrValue;
} }
} }
return `React.createElement(${JSON.stringify(
return `React.createElement(${JSON.stringify(node.nodeName)}, ${JSON.stringify(attributesMap)}, ${codegenChildNodes(node, level)})`; node.nodeName
)}, ${JSON.stringify(attributesMap)}, ${codegenChildNodes(
node,
level
)})`;
} }
function codegen(node, level) { function codegen(node, level) {
@ -67,5 +68,5 @@ module.exports = function(node) {
} }
} }
return 'return ' + codegen(node, 0) + '\n'; return "return " + codegen(node, 0) + "\n";
}; };

View File

@ -1,13 +1,14 @@
var jsdom = require("jsdom").jsdom; var jsdom = require("jsdom").jsdom;
var fs = require('fs'); var fs = require("fs");
var path = require('path'); var path = require("path");
function generateCode(name, htmlFile, rootNode, html) { function generateCode(name, htmlFile, rootNode, html) {
var generator = require('./codegen-' + name); var generator = require("./codegen-" + name);
var code = generator(rootNode, html); var code = generator(rootNode, html);
var wrappedCode = `window.createBenchmarks[${JSON.stringify(htmlFile+'-'+name)}]=function() {\n${code}\n}`; var wrappedCode = `window.createBenchmarks[${JSON.stringify(
htmlFile + "-" + name
)}]=function() {\n${code}\n}`;
var generateInitCode = generator.generateInitCode; var generateInitCode = generator.generateInitCode;
@ -18,24 +19,24 @@ ${generateInitCode(rootNode, html)}
${wrappedCode} ${wrappedCode}
}())`; }())`;
} }
fs.writeFileSync(path.join(__dirname, `benchmark-${htmlFile}-${name}.js`), wrappedCode, { encoding: 'utf8' }); fs.writeFileSync(
path.join(__dirname, `benchmark-${htmlFile}-${name}.js`),
wrappedCode,
{ encoding: "utf8" }
);
} }
var methods = [ var methods = ["dom", "dom-innerHTML", "marko-vdom", "react"];
'dom',
'dom-innerHTML',
'marko-vdom',
'react'
];
var htmlFiles = fs.readdirSync(__dirname) var htmlFiles = fs.readdirSync(__dirname).filter(function(name) {
.filter(function(name) { return name.startsWith("html-");
return name.startsWith('html-'); });
});
htmlFiles.forEach(function(htmlFile) { htmlFiles.forEach(function(htmlFile) {
var name = htmlFile.substring('html-'.length).slice(0, 0-'.html'.length); var name = htmlFile.substring("html-".length).slice(0, 0 - ".html".length);
var html = fs.readFileSync(path.join(__dirname, htmlFile), { encoding: 'utf8' }); var html = fs.readFileSync(path.join(__dirname, htmlFile), {
encoding: "utf8"
});
var doc = jsdom(html); var doc = jsdom(html);

View File

@ -1,32 +1,32 @@
require('../patch-module'); require("../patch-module");
require('marko/node-require'); require("marko/node-require");
require('marko/express'); require("marko/express");
var isProduction = process.env.NODE_ENV === 'production'; var isProduction = process.env.NODE_ENV === "production";
require('lasso').configure({ require("lasso").configure({
outputDir: __dirname + '/static', outputDir: __dirname + "/static",
bundlingEnabled: isProduction, bundlingEnabled: isProduction,
fingerprintsEnabled: isProduction, fingerprintsEnabled: isProduction,
minify: isProduction minify: isProduction
}); });
var express = require('express'); var express = require("express");
var app = express(); var app = express();
var serveStatic = require('serve-static'); var serveStatic = require("serve-static");
require('./codegen-create/run'); require("./codegen-create/run");
var template = require('./index.marko'); var template = require("./index.marko");
app.use('/codegen-create', serveStatic(__dirname + '/codegen-create')); app.use("/codegen-create", serveStatic(__dirname + "/codegen-create"));
app.use(require('lasso/middleware').serveStatic()); app.use(require("lasso/middleware").serveStatic());
app.get('/', function(req, res) { app.get("/", function(req, res) {
res.marko(template); res.marko(template);
}); });
@ -35,5 +35,5 @@ app.listen(8080, function(err) {
throw err; throw err;
} }
console.log('Server ready:\nhttp://localhost:8080'); console.log("Server ready:\nhttp://localhost:8080");
}); });

View File

@ -1,36 +1,42 @@
var fs = require('fs'); /* eslint-disable no-console */
var nodePath = require('path');
var fs = require("fs");
var nodePath = require("path");
var cwd = process.cwd(); var cwd = process.cwd();
var resolveFrom = require('resolve-from'); var resolveFrom = require("resolve-from");
// Try to use the Marko compiler installed with the project // Try to use the Marko compiler installed with the project
var markoCompilerPath; var markoCompilerPath;
const markocPkgVersion = require('../package.json').version; const markocPkgVersion = require("../package.json").version;
var markoPkgVersion; var markoPkgVersion;
try { try {
var markoPkgPath = resolveFrom(process.cwd(), 'marko/package.json'); var markoPkgPath = resolveFrom(process.cwd(), "marko/package.json");
markoPkgVersion = require(markoPkgPath).version; markoPkgVersion = require(markoPkgPath).version;
} catch(e) {} } catch (e) {
/* ignore error */
}
try { try {
markoCompilerPath = resolveFrom(process.cwd(), 'marko/compiler'); markoCompilerPath = resolveFrom(process.cwd(), "marko/compiler");
} catch(e) {} } catch (e) {
/* ignore error */
}
var markoCompiler; var markoCompiler;
if (markoCompilerPath) { if (markoCompilerPath) {
markoCompiler = require(markoCompilerPath); markoCompiler = require(markoCompilerPath);
} else { } else {
markoCompiler = require('../compiler'); markoCompiler = require("../compiler");
} }
var Minimatch = require('minimatch').Minimatch; var Minimatch = require("minimatch").Minimatch;
var appModulePath = require('app-module-path'); var appModulePath = require("app-module-path");
require('raptor-polyfill/string/startsWith'); require("raptor-polyfill/string/startsWith");
require('raptor-polyfill/string/endsWith'); require("raptor-polyfill/string/endsWith");
markoCompiler.defaultOptions.checkUpToDate = false; markoCompiler.defaultOptions.checkUpToDate = false;
@ -42,58 +48,65 @@ var mmOptions = {
function relPath(path) { function relPath(path) {
if (path.startsWith(cwd)) { if (path.startsWith(cwd)) {
return path.substring(cwd.length+1); return path.substring(cwd.length + 1);
} }
} }
var args = require('argly').createParser({ var args = require("argly")
'--help': { .createParser({
type: 'boolean', "--help": {
description: 'Show this help message' type: "boolean",
description: "Show this help message"
}, },
'--files --file -f *': { "--files --file -f *": {
type: 'string[]', type: "string[]",
description: 'A set of directories or files to compile' description: "A set of directories or files to compile"
}, },
'--ignore -i': { "--ignore -i": {
type: 'string[]', type: "string[]",
description: 'An ignore rule (default: --ignore "/node_modules" ".*")' description:
'An ignore rule (default: --ignore "/node_modules" ".*")'
}, },
'--clean -c': { "--clean -c": {
type: 'boolean', type: "boolean",
description: 'Clean all of the *.marko.js files' description: "Clean all of the *.marko.js files"
}, },
'--force': { "--force": {
type: 'boolean', type: "boolean",
description: 'Force template recompilation even if unchanged' description: "Force template recompilation even if unchanged"
}, },
'--paths -p': { "--paths -p": {
type: 'string[]', type: "string[]",
description: 'Additional directories to add to the Node.js module search path' description:
"Additional directories to add to the Node.js module search path"
}, },
'--vdom -V': { "--vdom -V": {
type: 'boolean', type: "boolean",
description: 'VDOM output' description: "VDOM output"
}, },
'--version -v': { "--version -v": {
type: 'boolean', type: "boolean",
description: 'Print markoc and marko compiler versions to the console' description:
"Print markoc and marko compiler versions to the console"
} }
}) })
.usage('Usage: $0 <pattern> [options]') .usage("Usage: $0 <pattern> [options]")
.example('Compile a single template', '$0 template.marko') .example("Compile a single template", "$0 template.marko")
.example('Compile all templates in the current directory', '$0 .') .example("Compile all templates in the current directory", "$0 .")
.example('Compile multiple templates', '$0 template.marko src/ foo/') .example("Compile multiple templates", "$0 template.marko src/ foo/")
.example('Delete all *.marko.js files in the current directory', '$0 . --clean') .example(
"Delete all *.marko.js files in the current directory",
"$0 . --clean"
)
.validate(function(result) { .validate(function(result) {
if (result.help) { if (result.help) {
this.printUsage(); this.printUsage();
process.exit(0); process.exit(0);
} else if (result.version) { } else if (result.version) {
console.log('markoc@' + markocPkgVersion); console.log("markoc@" + markocPkgVersion);
if (markoPkgVersion) { if (markoPkgVersion) {
console.log('marko@' + markoPkgVersion); console.log("marko@" + markoPkgVersion);
} }
process.exit(0); process.exit(0);
@ -114,20 +127,19 @@ var args = require('argly').createParser({
}) })
.parse(); .parse();
var output = 'html'; var output = "html";
var isForBrowser = false; var isForBrowser = false;
if (args.vdom) { if (args.vdom) {
output = 'vdom'; output = "vdom";
isForBrowser = true; isForBrowser = true;
} }
var compileOptions = { var compileOptions = {
output: output, output: output,
browser: isForBrowser, browser: isForBrowser,
compilerType: 'markoc', compilerType: "markoc",
compilerVersion: markoPkgVersion || markocPkgVersion compilerVersion: markoPkgVersion || markocPkgVersion
}; };
@ -146,30 +158,28 @@ if (paths && paths.length) {
var ignoreRules = args.ignore; var ignoreRules = args.ignore;
if (!ignoreRules) { if (!ignoreRules) {
ignoreRules = ['/node_modules', '.*']; ignoreRules = ["/node_modules", ".*"];
} }
ignoreRules = ignoreRules.filter(function (s) { ignoreRules = ignoreRules.filter(function(s) {
s = s.trim(); s = s.trim();
return s && !s.match(/^#/); return s && !s.match(/^#/);
}); });
ignoreRules = ignoreRules.map(function (pattern) { ignoreRules = ignoreRules.map(function(pattern) {
return new Minimatch(pattern, mmOptions); return new Minimatch(pattern, mmOptions);
}); });
function isIgnored(path, dir, stat) { function isIgnored(path, dir, stat) {
if (path.startsWith(dir)) { if (path.startsWith(dir)) {
path = path.substring(dir.length); path = path.substring(dir.length);
} }
path = path.replace(/\\/g, '/'); path = path.replace(/\\/g, "/");
var ignore = false; var ignore = false;
var ignoreRulesLength = ignoreRules.length; var ignoreRulesLength = ignoreRules.length;
for (var i=0; i<ignoreRulesLength; i++) { for (var i = 0; i < ignoreRulesLength; i++) {
var rule = ignoreRules[i]; var rule = ignoreRules[i];
var match = rule.match(path); var match = rule.match(path);
@ -177,14 +187,15 @@ function isIgnored(path, dir, stat) {
if (!match && stat && stat.isDirectory()) { if (!match && stat && stat.isDirectory()) {
try { try {
stat = fs.statSync(path); stat = fs.statSync(path);
} catch(e) {} } catch (e) {
/* ignore error */
}
if (stat && stat.isDirectory()) { if (stat && stat.isDirectory()) {
match = rule.match(path + '/'); match = rule.match(path + "/");
} }
} }
if (match) { if (match) {
if (rule.negate) { if (rule.negate) {
ignore = false; ignore = false;
@ -199,7 +210,7 @@ function isIgnored(path, dir, stat) {
function walk(files, options, done) { function walk(files, options, done) {
if (!files || files.length === 0) { if (!files || files.length === 0) {
done('No files provided'); done("No files provided");
} }
var pending = 0; var pending = 0;
@ -227,7 +238,6 @@ function walk(files, options, done) {
} else { } else {
done(null); done(null);
} }
} }
} }
}; };
@ -266,7 +276,7 @@ function walk(files, options, done) {
}); });
} }
for (var i=0; i<files.length; i++) { for (var i = 0; i < files.length; i++) {
var file = nodePath.resolve(cwd, files[i]); var file = nodePath.resolve(cwd, files[i]);
var stat = fs.statSync(file); var stat = fs.statSync(file);
@ -288,32 +298,34 @@ if (args.clean) {
file: function(file, context) { file: function(file, context) {
var basename = nodePath.basename(file); var basename = nodePath.basename(file);
if (basename.endsWith('.marko.js') || basename.endsWith('.marko.html') || basename.endsWith('.marko.xml.js')) { if (
basename.endsWith(".marko.js") ||
basename.endsWith(".marko.html") ||
basename.endsWith(".marko.xml.js")
) {
context.beginAsync(); context.beginAsync();
fs.unlink(file, function(err) { fs.unlink(file, function(err) {
if (err) { if (err) {
return context.endAsync(err); return context.endAsync(err);
} }
deleteCount++; deleteCount++;
console.log('Deleted: ' + file); console.log("Deleted: " + file);
context.endAsync(); context.endAsync();
}); });
} }
} }
}, },
function(err) { function() {
if (deleteCount === 0) { if (deleteCount === 0) {
console.log('No *.marko.js files were found. Already clean.'); console.log("No *.marko.js files were found. Already clean.");
} else { } else {
console.log('Deleted ' + deleteCount + ' file(s)'); console.log("Deleted " + deleteCount + " file(s)");
} }
}
}); );
} else { } else {
var found = {}; var found = {};
var compileCount = 0; var compileCount = 0;
var failed;
var failed = []; var failed = [];
var compile = function(path, context) { var compile = function(path, context) {
@ -322,37 +334,51 @@ if (args.clean) {
} }
found[path] = true; found[path] = true;
var outPath = path + '.js'; var outPath = path + ".js";
console.log('Compiling:\n Input: ' + relPath(path) + '\n Output: ' + relPath(outPath) + '\n'); console.log(
"Compiling:\n Input: " +
relPath(path) +
"\n Output: " +
relPath(outPath) +
"\n"
);
context.beginAsync(); context.beginAsync();
markoCompiler.compileFile(path, compileOptions, function(err, src) { markoCompiler.compileFile(path, compileOptions, function(err, src) {
if (err) { if (err) {
failed.push('Failed to compile "' + relPath(path) + '". Error: ' + (err.stack || err)); failed.push(
'Failed to compile "' +
relPath(path) +
'". Error: ' +
(err.stack || err)
);
context.endAsync(err); context.endAsync(err);
return; return;
} }
context.beginAsync(); context.beginAsync();
fs.writeFile(outPath, src, {encoding: 'utf8'}, function(err, src) { fs.writeFile(outPath, src, { encoding: "utf8" }, function(err) {
if (err) { if (err) {
failed.push('Failed to write "' + path + '". Error: ' + (err.stack || err)); failed.push(
'Failed to write "' +
path +
'". Error: ' +
(err.stack || err)
);
context.endAsync(err); context.endAsync(err);
return; return;
} }
compileCount++; compileCount++;
context.endAsync(); context.endAsync();
}); });
context.endAsync(); context.endAsync();
}); });
}; };
if (args.files && args.files.length) { if (args.files && args.files.length) {
walk( walk(
args.files, args.files,
@ -360,7 +386,11 @@ if (args.clean) {
file: function(file, context) { file: function(file, context) {
var basename = nodePath.basename(file); var basename = nodePath.basename(file);
if (basename.endsWith('.marko') || basename.endsWith('.marko.html') || basename.endsWith('.marko.xml')) { if (
basename.endsWith(".marko") ||
basename.endsWith(".marko.html") ||
basename.endsWith(".marko.xml")
) {
compile(file, context); compile(file, context);
} }
} }
@ -368,7 +398,10 @@ if (args.clean) {
function(err) { function(err) {
if (err) { if (err) {
if (failed.length) { if (failed.length) {
console.error('The following errors occurred:\n- ' + failed.join('\n- ')); console.error(
"The following errors occurred:\n- " +
failed.join("\n- ")
);
} else { } else {
console.error(err); console.error(err);
} }
@ -377,11 +410,11 @@ if (args.clean) {
} }
if (compileCount === 0) { if (compileCount === 0) {
console.log('No templates found'); console.log("No templates found");
} else { } else {
console.log('Compiled ' + compileCount + ' templates(s)'); console.log("Compiled " + compileCount + " templates(s)");
} }
}
}); );
} }
} }

View File

@ -1,7 +1,7 @@
var isDebug = require('./env').isDebug; var isDebug = require("./env").isDebug;
if (isDebug) { if (isDebug) {
module.exports = require('./src/browser-refresh'); module.exports = require("./src/browser-refresh");
} else { } else {
module.exports = require('./dist/browser-refresh'); module.exports = require("./dist/browser-refresh");
} }

View File

@ -1,7 +1,7 @@
var isDebug = require('./env').isDebug; var isDebug = require("./env").isDebug;
if (isDebug) { if (isDebug) {
module.exports = require('./src/compiler'); module.exports = require("./src/compiler");
} else { } else {
module.exports = require('./dist/compiler'); module.exports = require("./dist/compiler");
} }

View File

@ -1,7 +1,7 @@
var isDebug = require('./env').isDebug; var isDebug = require("./env").isDebug;
if (isDebug) { if (isDebug) {
module.exports = require('./src/components'); module.exports = require("./src/components");
} else { } else {
module.exports = require('./dist/components'); module.exports = require("./dist/components");
} }

View File

@ -127,7 +127,7 @@ button.example-button on-click('increment') — Click me!
Cant make up your mind or just want to paste in that code snippet from Cant make up your mind or just want to paste in that code snippet from
StackOverflow? HTML syntax can be used within in the concise syntax. Youll come StackOverflow? HTML syntax can be used within in the concise syntax. Youll come
back and make it consistent…*one day*. back and make it consistent…_one day_.
#### 5. Import JavaScript modules #### 5. Import JavaScript modules
@ -174,8 +174,8 @@ imported:
#### 7. Use JavaScript to set CSS classes and styles #### 7. Use JavaScript to set CSS classes and styles
Setting CSS classes and styles is made easy using JavaScript! Marko will happily Setting CSS classes and styles is made easy using JavaScript! Marko will happily
accept simple strings, JavaScript objects and arrays (*falsy values will be accept simple strings, JavaScript objects and arrays (_falsy values will be
ignored).* ignored)._
```marko ```marko
$ const fontColor = input.color || 'blue'; $ const fontColor = input.color || 'blue';
@ -243,23 +243,29 @@ components](http://markojs.com/docs/server-side-rendering/) rendered on the
server when the page loads in the browser): server when the page loads in the browser):
```js ```js
require('marko/node-require').install(); // require .marko files! require("marko/node-require").install(); // require .marko files!
const http = require('http'); const http = require("http");
const template = require('./template'); const template = require("./template");
http.createServer().on('request', (req, res) => { http
template.render({ .createServer()
name: 'Frank', .on("request", (req, res) => {
count: 30, template.render(
colors: ['red', 'green', 'blue'] {
}, res); name: "Frank",
}).listen(8080); count: 30,
colors: ["red", "green", "blue"]
},
res
);
})
.listen(8080);
``` ```
#### Bonus: Friendly compile-time errors #### Bonus: Friendly compile-time errors
We all make mistakes *every now and then*. Typo in your custom tag? Forgot an We all make mistakes _every now and then_. Typo in your custom tag? Forgot an
ending tag? No worries! Marko will give you a friendly error message and point ending tag? No worries! Marko will give you a friendly error message and point
you right to the problematic code. you right to the problematic code.
@ -276,7 +282,7 @@ Unrecognized tag: fancy-buttn — More details: https://github.com/marko-js/mark
Coming soon: auto correction and autonomous coding Coming soon: auto correction and autonomous coding
***** ---
*Cover image credit: _Cover image credit:
*[Wikipedia](https://en.wikipedia.org/wiki/List_of_rock_formations#/media/File:Amanhecer_no_Hercules_--.jpg) _[Wikipedia](https://en.wikipedia.org/wiki/List_of_rock_formations#/media/File:Amanhecer_no_Hercules_--.jpg)

View File

@ -12,15 +12,14 @@ With Marko, the DOM output of a UI component is based on input properties and a
Marko makes it easy to to co-locate your component's class and styles with the HTML view that they correspond to. The following are the key part of any UI component: Marko makes it easy to to co-locate your component's class and styles with the HTML view that they correspond to. The following are the key part of any UI component:
- __View__ - The HTML template for your UI component. Receives input properties and states and renders to either HTML (server-side) or virtual DOM nodes (browser-side) * **View** - The HTML template for your UI component. Receives input properties and states and renders to either HTML (server-side) or virtual DOM nodes (browser-side)
- __Client-side behavior__ - Implemented as a JavaScript class with methods and properties to provide initialization, event handling (including DOM events, custom events and lifecycle events) and state management * **Client-side behavior** - Implemented as a JavaScript class with methods and properties to provide initialization, event handling (including DOM events, custom events and lifecycle events) and state management
- __Styling__ - Cascading StyleSheet with support for CSS preprocessors such as Less or Sass * **Styling** - Cascading StyleSheet with support for CSS preprocessors such as Less or Sass
## Server-side rendering ## Server-side rendering
A UI component can be rendered on the server or in the browser, but in either case, the UI component instance will be mounted to a DOM node in the browser automatically. If a UI component tree is rendered on the server then Marko will automatically recreate the UI component tree in the browser with no extra code required. For more details, please see [Server-side rendering](/docs/server-side-rendering/). A UI component can be rendered on the server or in the browser, but in either case, the UI component instance will be mounted to a DOM node in the browser automatically. If a UI component tree is rendered on the server then Marko will automatically recreate the UI component tree in the browser with no extra code required. For more details, please see [Server-side rendering](/docs/server-side-rendering/).
## Single-file components ## Single-file components
Marko allows you to define a `class` for a component right in the `.marko` view and call methods of that class with `on-*` attributes: Marko allows you to define a `class` for a component right in the `.marko` view and call methods of that class with `on-*` attributes:
@ -45,7 +44,7 @@ class {
### Styles ### Styles
Adding styles to your view is also made easy. These styles won't be output in a `<style>` tag as inline styles usually are, but will result in the style being externalized so it isn't duplicated should a component be used more than once on the page. Adding styles to your view is also made easy. These styles won't be output in a `<style>` tag as inline styles usually are, but will result in the style being externalized so it isn't duplicated should a component be used more than once on the page.
```marko ```marko
style { style {
@ -61,6 +60,7 @@ style {
``` ```
If you use a css preprocessor, you can add the extension right on `style`: If you use a css preprocessor, you can add the extension right on `style`:
```marko ```marko
style.less { style.less {
button.primary { button.primary {
@ -71,16 +71,15 @@ style.less {
> **Note:** The code in the style section is processed in a context that is separate from the rest of the template so so you won't be able to use JavaScript variables inside the style section. If you need variables in your CSS then you will need to use a CSS pre-processor that supports variables. > **Note:** The code in the style section is processed in a context that is separate from the rest of the template so so you won't be able to use JavaScript variables inside the style section. If you need variables in your CSS then you will need to use a CSS pre-processor that supports variables.
## Multi-file components ## Multi-file components
You might prefer to keep your component's class and style definitions in separate files from the view definition - the classical separation of HTML, CSS and JavaScript. Marko makes this possible with a simple filename based convention. You might prefer to keep your component's class and style definitions in separate files from the view definition - the classical separation of HTML, CSS and JavaScript. Marko makes this possible with a simple filename based convention.
> **ProTip:** If your motivation to move the component's class and styles out to a separate file is that the code is getting too large, consider splitting the component into smaller, more manageable components. > **ProTip:** If your motivation to move the component's class and styles out to a separate file is that the code is getting too large, consider splitting the component into smaller, more manageable components.
### Supporting files ### Supporting files
Marko automatically discovers supporting files in the same directory as a Marko view. For example if you have a view named `counter.marko`, Marko will automatically look for `counter.component.js` and `counter.style.css`. Marko automatically discovers supporting files in the same directory as a Marko view. For example if you have a view named `counter.marko`, Marko will automatically look for `counter.component.js` and `counter.style.css`.
``` ```
counter.marko counter.marko
@ -88,7 +87,7 @@ counter.component.js
counter.style.css counter.style.css
``` ```
Marko also handles views named `index.marko` specially, it will look for `component.js` and `style.css` in addition to `index.component.js` and `index.style.css`. This allows easily grouping component files into a directory: Marko also handles views named `index.marko` specially, it will look for `component.js` and `style.css` in addition to `index.component.js` and `index.style.css`. This allows easily grouping component files into a directory:
``` ```
counter/ counter/
@ -101,18 +100,19 @@ In your `component.js` file, export the component's class:
```js ```js
module.exports = class { module.exports = class {
onCreate() { onCreate() {
this.state = { this.state = {
count:0 count: 0
}; };
} }
increment() { increment() {
this.state.count++; this.state.count++;
} }
} };
``` ```
In your `index.marko` file, you can reference methods from the class using `on-*` attributes: In your `index.marko` file, you can reference methods from the class using `on-*` attributes:
```marko ```marko
<div> <div>
<h2>The current count is ${state.count}</h2> <h2>The current count is ${state.count}</h2>
@ -121,46 +121,46 @@ In your `index.marko` file, you can reference methods from the class using `on-*
``` ```
And in your `style.css`, define the style: And in your `style.css`, define the style:
```css ```css
button.primary { button.primary {
background-color:#09c; background-color: #09c;
} }
``` ```
> **ProTip:** In addition to looking for `[name].style.css`, Marko actually looks for `[name].style.*` so it will also pick up any css preprocessor you're using (less, stylus, scss, etc.). > **ProTip:** In addition to looking for `[name].style.css`, Marko actually looks for `[name].style.*` so it will also pick up any css preprocessor you're using (less, stylus, scss, etc.).
### Components with plain objects ### Components with plain objects
If you're targeting a browser that does not support classes, a plain object may also be exported: If you're targeting a browser that does not support classes, a plain object may also be exported:
```js ```js
module.exports = { module.exports = {
onCreate: function() { onCreate: function() {
this.state = { this.state = {
count:0 count: 0
}; };
}, },
increment: function() { increment: function() {
this.state.count++; this.state.count++;
} }
} };
``` ```
## Split components ## Split components
Split components allow you to optimize for the case where a component is rendered on the server, but doesn't need to be re-rendered in the browser. Because the component doesn't need to be rendered in the browser, the template does not need to be sent to the browser. This can reduce your page weight by a few hundred bytes in some cases. Split components allow you to optimize for the case where a component is rendered on the server, but doesn't need to be re-rendered in the browser. Because the component doesn't need to be rendered in the browser, the template does not need to be sent to the browser. This can reduce your page weight by a few hundred bytes in some cases.
> **Note:** If a split component is the child of a stateful component, the full rendering logic will still be sent down because the parent component may pass new input to the split component, requiring it to re-render. > **Note:** If a split component is the child of a stateful component, the full rendering logic will still be sent down because the parent component may pass new input to the split component, requiring it to re-render.
Additionally if _all_ components rendered on a page are split components, Marko's VDOM and rendering runtime is not necessary and therefore not sent to the browser, which can further reduce page weight by a few kilobytes. Additionally if _all_ components rendered on a page are split components, Marko's VDOM and rendering runtime is not necessary and therefore not sent to the browser, which can further reduce page weight by a few kilobytes.
> **ProTip:** Don't over-optimize. If your component really doesn't need re-rendering, go ahead and split, but don't forgo stateful re-rendering when it would make your code more maintainable. > **ProTip:** Don't over-optimize. If your component really doesn't need re-rendering, go ahead and split, but don't forgo stateful re-rendering when it would make your code more maintainable.
### Usage ### Usage
Marko discovers split components in a similar way to how it discovers an external component class: Marko discovers split components in a similar way to how it discovers an external component class:
for example if you have a view named `button.marko`, it will automatically look for `button.component-browser.js`. If your view is named `index.marko`, it will look for `component-browser.js` in addition to `index.component-browser.js`. for example if you have a view named `button.marko`, it will automatically look for `button.component-browser.js`. If your view is named `index.marko`, it will look for `component-browser.js` in addition to `index.component-browser.js`.
``` ```
counter/ counter/
@ -168,10 +168,12 @@ counter/
component-browser.js component-browser.js
``` ```
A split component might also need to do some set up as part of the initial render. In this case, the component may define a second component class to use the `onCreate`, `onInput`, and `onRender` [lifecycle methods](#lifecycle-events). This class can be exported from `component.js` or defined right in the template as with single-file components. A split component might also need to do some set up as part of the initial render. In this case, the component may define a second component class to use the `onCreate`, `onInput`, and `onRender` [lifecycle methods](#lifecycle-events). This class can be exported from `component.js` or defined right in the template as with single-file components.
### Example ### Example
_index.marko_ _index.marko_
```marko ```marko
class { class {
onCreate() { onCreate() {
@ -183,12 +185,13 @@ class {
``` ```
_component-browser.js_ _component-browser.js_
```js ```js
module.exports = { module.exports = {
shout() { shout() {
alert('My favorite number is ' + this.number + '!'); alert("My favorite number is " + this.number + "!");
} }
} };
``` ```
## Event handling ## Event handling
@ -226,10 +229,10 @@ class {
Any string that represents a valid JavaScript identifier is allowed for the event handler method name and it can be a JavaScript expression. The following arguments are passed to the handler method when the event is fired: Any string that represents a valid JavaScript identifier is allowed for the event handler method name and it can be a JavaScript expression. The following arguments are passed to the handler method when the event is fired:
1. `...args` - Any extra arguments bind are _prepended_ to the arguments passed to the component's handler method 1. `...args` - Any extra arguments bind are _prepended_ to the arguments passed to the component's handler method
- For example: `on-click('onButtonClick', arg1, arg2)``onButtonClick(arg1, arg2, event, el)`) * For example: `on-click('onButtonClick', arg1, arg2)``onButtonClick(arg1, arg2, event, el)`)
2. `event` - The native DOM event 2. `event` - The native DOM event
3. `el` - The DOM element that the event listener was attached to 3. `el` - The DOM element that the event listener was attached to
When using the `on-*` or `once-*` attributes to attach event listeners, Marko will use event delegation that is more efficient than using `el.addEventListener()` directly. Please see [Why is Marko Fast? » Event delegation](/docs/why-is-marko-fast/#event-delegation) for more details. When using the `on-*` or `once-*` attributes to attach event listeners, Marko will use event delegation that is more efficient than using `el.addEventListener()` directly. Please see [Why is Marko Fast? » Event delegation](/docs/why-is-marko-fast/#event-delegation) for more details.
@ -256,14 +259,14 @@ class {
Any string that represents a valid JavaScript identifier is allowed for the event handler method name and it can be a JavaScript expression. The following arguments are passed to the handler method when the event is fired: Any string that represents a valid JavaScript identifier is allowed for the event handler method name and it can be a JavaScript expression. The following arguments are passed to the handler method when the event is fired:
1. `...args` - Any extra bind arguments are _prepended_ to the arguments passed to the component's handler method 1. `...args` - Any extra bind arguments are _prepended_ to the arguments passed to the component's handler method
2. `...eventArgs` - The arguments passed to `this.emit()` by the target UI component 2. `...eventArgs` - The arguments passed to `this.emit()` by the target UI component
3. `component` - The component instance that the event listener was attached to 3. `component` - The component instance that the event listener was attached to
The following code illustrates how the UI component for `<counter>` might emit the `change` event: The following code illustrates how the UI component for `<counter>` might emit the `change` event:
_counter/index.marko_ _counter/index.marko_
```marko ```marko
class { class {
onCreate() { onCreate() {
@ -287,12 +290,10 @@ class {
</div> </div>
``` ```
> **ProTip:** Unlike native DOM events, UI component custom events may be emitted with multiple arguments. For example: > **ProTip:** Unlike native DOM events, UI component custom events may be emitted with multiple arguments. For example:
```js ```js
this.emit('foo', 'bar', 'baz'); this.emit("foo", "bar", "baz");
``` ```
## Attributes ## Attributes
@ -370,11 +371,12 @@ The `key` attribute can be used to pair HTML elements or UI components that are
Putting the `:scoped` modifier on an attribute will result in the attribute value being prefixed with a unique ID associated with the current UI component. Thus, `:scoped` attribute modifiers can be used to assign a globally unique attribute value from a value that only needs to be unique to the current UI component. Putting the `:scoped` modifier on an attribute will result in the attribute value being prefixed with a unique ID associated with the current UI component. Thus, `:scoped` attribute modifiers can be used to assign a globally unique attribute value from a value that only needs to be unique to the current UI component.
Certain HTML attributes reference the `id` of other elements on the page. For example, the [HTML `<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) `for` attribute takes an `id` as its value. Many `ARIA` attributes ([`aria-describedby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-describedby_attribute), etc.) also take an `id` as their value. Certain HTML attributes reference the `id` of other elements on the page. For example, the [HTML `<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) `for` attribute takes an `id` as its value. Many `ARIA` attributes ([`aria-describedby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-describedby_attribute), etc.) also take an `id` as their value.
The `:scoped` modifier on an attribute allows you to reference another element as shown in the following examples: The `:scoped` modifier on an attribute allows you to reference another element as shown in the following examples:
**`for:scoped`** **`for:scoped`**
```marko ```marko
<label for:scoped="name">Name</label> <label for:scoped="name">Name</label>
<input id:scoped="name" value="Frank"/> <input id:scoped="name" value="Frank"/>
@ -477,7 +479,7 @@ suffix:
### `this.el` ### `this.el`
The root [HTML element](https://developer.mozilla.org/en-US/docs/Web/API/element) that the component is bound to. If there are multiple roots, this is the first. The root [HTML element](https://developer.mozilla.org/en-US/docs/Web/API/element) that the component is bound to. If there are multiple roots, this is the first.
### `this.els` ### `this.els`
@ -489,9 +491,9 @@ The String ID of the root [HTML element](https://developer.mozilla.org/en-US/doc
### `this.state` ### `this.state`
The current state for the component. Changing `this.state` or any of its direct properties will result in the component being re-rendered. The current state for the component. Changing `this.state` or any of its direct properties will result in the component being re-rendered.
Only properties that exist when `this.state` is first defined will be watched for changes. If you don't need a property initially, you can set it to `null`. Only properties that exist when `this.state` is first defined will be watched for changes. If you don't need a property initially, you can set it to `null`.
```marko ```marko
class { class {
@ -520,7 +522,7 @@ this.state.numbers.push(num);
// mark numbers as dirty, because a `push` // mark numbers as dirty, because a `push`
// won't be automatically detected by Marko // won't be automatically detected by Marko
this.setStateDirty('numbers'); this.setStateDirty("numbers");
``` ```
### `this.input` ### `this.input`
@ -566,18 +568,18 @@ The `state` variable refers to UI component's state object and is the unwatched
### `destroy([options])` ### `destroy([options])`
| option | type | default | description | | option | type | default | description |
| ------- | ---- | ------- | ----------- | | ------------ | --------- | ------- | --------------------------------------------------------------------------------------- |
| `removeNode` | `Boolean` | `true` | `false` will keep the component in the DOM while still unsubscribing all events from it | | `removeNode` | `Boolean` | `true` | `false` will keep the component in the DOM while still unsubscribing all events from it |
| `recursive` | `Boolean` | `true` | `false` will prevent child components from being destroyed | | `recursive` | `Boolean` | `true` | `false` will prevent child components from being destroyed |
Destroys the component by unsubscribing from all listeners made using the `subscribeTo` method and then detaching the component's root element from the DOM. All nested components (discovered by querying the DOM) are also destroyed. Destroys the component by unsubscribing from all listeners made using the `subscribeTo` method and then detaching the component's root element from the DOM. All nested components (discovered by querying the DOM) are also destroyed.
```javascript ```javascript
component.destroy({ component.destroy({
removeNode: true, //true by default removeNode: true, //true by default
recursive: true //true by default recursive: true //true by default
}) });
``` ```
### `forceUpdate()` ### `forceUpdate()`
@ -589,48 +591,48 @@ Queue the component to re-render and skip all checks to see if it actually needs
### `getEl([key, index])` ### `getEl([key, index])`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | ------------- | ------------------------------------------------------------------------------- |
| `key` | `String` | _optional_ the scoped identifier for the element | | `key` | `String` | _optional_ the scoped identifier for the element |
| `index` | `Number` | _optional_ the index of the component, if `key` references a repeated component | | `index` | `Number` | _optional_ the index of the component, if `key` references a repeated component |
| return value | `HTMLElement` | the element matching the key or `this.el` if no key is provided | | return value | `HTMLElement` | the element matching the key or `this.el` if no key is provided |
Returns a nested DOM element by prefixing the provided `key` with the component's ID. For Marko, nested DOM elements should be assigned an ID using the `key` custom attribute. Returns a nested DOM element by prefixing the provided `key` with the component's ID. For Marko, nested DOM elements should be assigned an ID using the `key` custom attribute.
### `getEls(key)` ### `getEls(key)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | -------------------- | ----------------------------------------------------- |
| `key` | `String` | the scoped identifier for the element | | `key` | `String` | the scoped identifier for the element |
| return value | `Array<HTMLElement>` | an array of _repeated_ DOM elements for the given key | | return value | `Array<HTMLElement>` | an array of _repeated_ DOM elements for the given key |
Repeated DOM elements must have a value for the `key` attribute that ends with `[]` (e.g., `key="items[]"`) Repeated DOM elements must have a value for the `key` attribute that ends with `[]` (e.g., `key="items[]"`)
### `getElId([key, index])` ### `getElId([key, index])`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | -------- | ------------------------------------------------------------------------------- |
| `key` | `String` | _optional_ the scoped identifier for the element | | `key` | `String` | _optional_ the scoped identifier for the element |
| `index` | `Number` | _optional_ the index of the component, if `key` references a repeated component | | `index` | `Number` | _optional_ the index of the component, if `key` references a repeated component |
| return value | `String` | the element ID matching the key or `this.el.id` if `key` is undefined | | return value | `String` | the element ID matching the key or `this.el.id` if `key` is undefined |
Similar to `getEl`, but only returns the String ID of the nested DOM element instead of the actual DOM element. Similar to `getEl`, but only returns the String ID of the nested DOM element instead of the actual DOM element.
### `getComponent(key[, index])` ### `getComponent(key[, index])`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key` | `String` | the scoped identifier for the element | | `key` | `String` | the scoped identifier for the element |
| `index` | `Number` | _optional_ the index of the component, if `key` references a repeated component | | `index` | `Number` | _optional_ the index of the component, if `key` references a repeated component |
| return value | `Component` | a reference to a nested `Component` for the given key. If an `index` is provided and the target component is a repeated component (e.g. `key="items[]"`) then the component at the given index will be returned. | | return value | `Component` | a reference to a nested `Component` for the given key. If an `index` is provided and the target component is a repeated component (e.g. `key="items[]"`) then the component at the given index will be returned. |
### `getComponents(key, [, index])` ### `getComponents(key, [, index])`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | ------------------ | ------------------------------------------------------------------------------- |
| `key` | `String` | the scoped identifier for the element | | `key` | `String` | the scoped identifier for the element |
| `index` | `Number` | _optional_ the index of the component, if `key` references a repeated component | | `index` | `Number` | _optional_ the index of the component, if `key` references a repeated component |
| return value | `Array<Component>` | an array of _repeated_ `Component` instances for the given key | | return value | `Array<Component>` | an array of _repeated_ `Component` instances for the given key |
Repeated components must have a value for the `key` attribute that ends with `[]` (e.g., `key="items[]"`) Repeated components must have a value for the `key` attribute that ends with `[]` (e.g., `key="items[]"`)
@ -643,8 +645,8 @@ Returns `true` if a component has been destroyed using
### `replaceState(newState)` ### `replaceState(newState)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ---------- | -------- | ------------------------------------------------ |
| `newState` | `Object` | a new state object to replace the previous state | | `newState` | `Object` | a new state object to replace the previous state |
Replaces the state with an entirely new state. Equivalent to `this.state = newState`. Replaces the state with an entirely new state. Equivalent to `this.state = newState`.
@ -653,46 +655,46 @@ Replaces the state with an entirely new state. Equivalent to `this.state = newSt
### `rerender([input])` ### `rerender([input])`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------- | -------- | -------------------------------------------------- |
| `input` | `Object` | _optional_ new input data to use when re-rendering | | `input` | `Object` | _optional_ new input data to use when re-rendering |
Rerenders the component using its `renderer` and either supplied `input` or internal `input` and `state`. Rerenders the component using its `renderer` and either supplied `input` or internal `input` and `state`.
### `setState(name, value)` ### `setState(name, value)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------- | -------- | ---------------------------------------- |
| `name` | `String` | the name of the state property to update | | `name` | `String` | the name of the state property to update |
| `value` | `Any` | the new value for the state property | | `value` | `Any` | the new value for the state property |
Used to change the value of a single state property. Equivalent to setting `this.state[name] = value` except it will also work for adding new properties to the component state. Used to change the value of a single state property. Equivalent to setting `this.state[name] = value` except it will also work for adding new properties to the component state.
```javascript ```javascript
this.setState('disabled', true); this.setState("disabled", true);
``` ```
### `setState(newState)` ### `setState(newState)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ---------- | -------- | --------------------------------------------------- |
| `newState` | `Object` | a new state object to merge into the previous state | | `newState` | `Object` | a new state object to merge into the previous state |
Used to change the value of multiple state properties. Used to change the value of multiple state properties.
```js ```js
this.setState({ this.setState({
disabled: true, disabled: true,
size: 'large' size: "large"
}); });
``` ```
### `setStateDirty(name[, value])` ### `setStateDirty(name[, value])`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------- | -------- | ----------------------------------------------- |
| `name` | `String` | the name of the state property to mark as dirty | | `name` | `String` | the name of the state property to mark as dirty |
| `value` | `Any` | _optional_ a new value for the state property | | `value` | `Any` | _optional_ a new value for the state property |
Force a state property to be changed even if the value is equal to the old value. This is helpful in cases where a change occurs to a complex object that would not be detected by a shallow compare. Invoking this function completely circumvents all property equality checks (shallow compares) and always rerenders the component. Force a state property to be changed even if the value is equal to the old value. This is helpful in cases where a change occurs to a complex object that would not be detected by a shallow compare. Invoking this function completely circumvents all property equality checks (shallow compares) and always rerenders the component.
@ -706,28 +708,29 @@ _example.js_
```javascript ```javascript
// Because this does not create a new array, the change // Because this does not create a new array, the change
// would not be detected by a shallow property comparison // would not be detected by a shallow property comparison
this.state.colors.push('red'); this.state.colors.push("red");
// Force that particular state property to be considered dirty so // Force that particular state property to be considered dirty so
// that it will trigger the component's view to be updated // that it will trigger the component's view to be updated
this.setStateDirty('colors'); this.setStateDirty("colors");
``` ```
### `subscribeTo(emitter)` ### `subscribeTo(emitter)`
| params | description | | params | description |
| ------- | ----------- | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `emitter` | a node.js [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) or a DOM object that emits events (`window`, `document`, etc.) | | `emitter` | a node.js [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) or a DOM object that emits events (`window`, `document`, etc.) |
| return value | a tracked subscription | | return value | a tracked subscription |
When a component is destroyed, it is necessary to remove any listeners that were attached by the component in order to prevent a memory leak. By using `subscribeTo`, Marko will automatically track and remove any listeners you attach when the component is destroyed. When a component is destroyed, it is necessary to remove any listeners that were attached by the component in order to prevent a memory leak. By using `subscribeTo`, Marko will automatically track and remove any listeners you attach when the component is destroyed.
Marko uses [`listener-tracker`](https://github.com/patrick-steele-idem/listener-tracker) to provide this feature. Marko uses [`listener-tracker`](https://github.com/patrick-steele-idem/listener-tracker) to provide this feature.
_example.js_ _example.js_
```js ```js
this.subscribeTo(window).on('scroll', () => { this.subscribeTo(window).on("scroll", () => {
console.log('The user scrolled the window!'); console.log("The user scrolled the window!");
}); });
``` ```
@ -736,40 +739,40 @@ this.subscribeTo(window).on('scroll', () => {
Immediately, executes any pending updates to the DOM rather than following the normal queued update mechanism for rendering. Immediately, executes any pending updates to the DOM rather than following the normal queued update mechanism for rendering.
```js ```js
this.setState('foo', 'bar'); this.setState("foo", "bar");
this.update(); // Force the DOM to update this.update(); // Force the DOM to update
this.setState('hello', 'world'); this.setState("hello", "world");
this.update(); // Force the DOM to update this.update(); // Force the DOM to update
``` ```
## Event methods ## Event methods
A Marko component inherits from [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter). Below are a few commonly used methods, view the node docs for the full list. A Marko component inherits from [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter). Below are a few commonly used methods, view the node docs for the full list.
### `emit(eventName, ...args)` ### `emit(eventName, ...args)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ----------- | -------- | ----------------------------------------------------- |
| `eventName` | `String` | the name of the event | | `eventName` | `String` | the name of the event |
| `...args` | `Any` | all subsequent parameters are passed to the listeners | | `...args` | `Any` | all subsequent parameters are passed to the listeners |
Emits a UI component custom event. If a UI component attached a listener with the matching `eventName` then the corresponding event listener method will be automatically invoked. Event listeners can be attached using either the [`on-[event](methodName, ...args)` attribute](#declarative-custom-events) attribute or `targetComponent.on()`. Emits a UI component custom event. If a UI component attached a listener with the matching `eventName` then the corresponding event listener method will be automatically invoked. Event listeners can be attached using either the [`on-[event](methodName, ...args)` attribute](#declarative-custom-events) attribute or `targetComponent.on()`.
### `on(eventName, handler)` ### `on(eventName, handler)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ----------- | ---------- | -------------------------------------------- |
| `eventName` | `String` | the name of the event to listen for | | `eventName` | `String` | the name of the event to listen for |
| `handler` | `Function` | the function to call when the event is fired | | `handler` | `Function` | the function to call when the event is fired |
Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.
### `once(eventName, handler)` ### `once(eventName, handler)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ----------- | ---------- | -------------------------------------------- |
| `eventName` | `String` | the name of the event to listen for | | `eventName` | `String` | the name of the event to listen for |
| `handler` | `Function` | the function to call when the event is fired | | `handler` | `Function` | the function to call when the event is fired |
Adds a one time listener function for the event named eventName. The next time eventName is triggered, this listener is removed and then invoked. Adds a one time listener function for the event named eventName. The next time eventName is triggered, this listener is removed and then invoked.
@ -777,12 +780,12 @@ Adds a one time listener function for the event named eventName. The next time e
Marko defines six distinct lifecycle events: Marko defines six distinct lifecycle events:
- `create` * `create`
- `input` * `input`
- `render` * `render`
- `mount` * `mount`
- `update` * `update`
- `destroy` * `destroy`
These events are emitted at specific points over the lifecycle of a component as shown below: These events are emitted at specific points over the lifecycle of a component as shown below:
@ -807,7 +810,7 @@ emit('render')  emit('update')
**Destroy** **Destroy**
```js ```js
emit('destroy') emit("destroy");
``` ```
### Lifecycle event methods ### Lifecycle event methods
@ -825,18 +828,19 @@ class {
} }
``` ```
> **ProTip:** When a lifecycle event occurs in the browser, the corresponding event is emitted on the component instance. A parent component, or other code that has access to the component instance, can listen for these events. For example: > **ProTip:** When a lifecycle event occurs in the browser, the corresponding event is emitted on the component instance. A parent component, or other code that has access to the component instance, can listen for these events. For example:
```js ```js
component.on('input', function(input, out) { component.on("input", function(input, out) {
// The component was destroyed! // The component was destroyed!
}); });
``` ```
### `onCreate(input, out)` ### `onCreate(input, out)`
| params | description | | params | description |
| ------- | ----------- | | ------- | --------------------------------------------------------------- |
| `input` | the input data used to render the component for the first time | | `input` | the input data used to render the component for the first time |
| `out` | the async `out` used to render the component for the first time | | `out` | the async `out` used to render the component for the first time |
The `create` event is emitted (and `onCreate` is called) when the component is first created. The `create` event is emitted (and `onCreate` is called) when the component is first created.
@ -844,6 +848,7 @@ The `create` event is emitted (and `onCreate` is called) when the component is f
`onCreate` is typically used to set the initial state for stateful components: `onCreate` is typically used to set the initial state for stateful components:
_example.marko_ _example.marko_
```marko ```marko
class { class {
onCreate(input) { onCreate(input) {
@ -856,29 +861,30 @@ class {
### `onInput(input, out)` ### `onInput(input, out)`
| params | description | | params | description |
| ------- | ----------- | | ------- | ------------------ |
| `input` | the new input data | | `input` | the new input data |
The `input` event is emitted (and `onInput` is called) when the component receives input: both the initial input and for any subsequent updates to its input. The `input` event is emitted (and `onInput` is called) when the component receives input: both the initial input and for any subsequent updates to its input.
### `onRender(out)` ### `onRender(out)`
| params | description | | params | description |
| ------- | ----------- | | ------ | -------------------------------------- |
| `out` | the async `out` for the current render | | `out` | the async `out` for the current render |
The `render` event is emitted (and `onRender` is called) when the component is about to be rendered (or re-rendered). The `render` event is emitted (and `onRender` is called) when the component is about to be rendered (or re-rendered).
### `onMount()` ### `onMount()`
The `mount` event is emitted (and `onMount` is called) when the component is first mounted to the DOM. For a server-rendered component, this is the first event that is emitted in the browser. The `mount` event is emitted (and `onMount` is called) when the component is first mounted to the DOM. For a server-rendered component, this is the first event that is emitted in the browser.
This is the first point at which `this.el` and `this.els` are defined. `onMount` is commonly used to attach third-party javascript (or `jQuery` if you're still doing that) plugins to the newly-mounted DOM element(s). This is the first point at which `this.el` and `this.els` are defined. `onMount` is commonly used to attach third-party javascript (or `jQuery` if you're still doing that) plugins to the newly-mounted DOM element(s).
For example, attaching a library that monitors whether the component is in the viewport: For example, attaching a library that monitors whether the component is in the viewport:
_example.marko_ _example.marko_
```marko ```marko
import scrollmonitor from 'scrollmonitor'; import scrollmonitor from 'scrollmonitor';
@ -895,11 +901,11 @@ class {
### `onUpdate()` ### `onUpdate()`
The `update` event is emitted (and `onUpdate` is called) when the component is called after a component has re-rendered and the DOM has been updated. If a re-render does not cause the DOM to be updated (nothing changed), this event will not be fired. The `update` event is emitted (and `onUpdate` is called) when the component is called after a component has re-rendered and the DOM has been updated. If a re-render does not cause the DOM to be updated (nothing changed), this event will not be fired.
### `onDestroy()` ### `onDestroy()`
The `destroy` event is emitted (and `onDestroy` is called) when the component is about to be unmounted from the DOM and cleaned up. `onDestroy` should be used to do any additional clean up beyond what Marko handles itself. The `destroy` event is emitted (and `onDestroy` is called) when the component is about to be unmounted from the DOM and cleaned up. `onDestroy` should be used to do any additional clean up beyond what Marko handles itself.
For example, cleaning up from our `scrollmonitor` example in [`onMount`](#codeonmountcode): For example, cleaning up from our `scrollmonitor` example in [`onMount`](#codeonmountcode):

View File

@ -1,14 +1,16 @@
# Concise syntax # Concise syntax
Marko's concise syntax is very similar to the HTML syntax, except it's more... concise. Essentially, you take an HTML tag, remove the angle brackets (`<>`) and use indentation rather than a closing tag: Marko's concise syntax is very similar to the HTML syntax, except it's more... concise. Essentially, you take an HTML tag, remove the angle brackets (`<>`) and use indentation rather than a closing tag:
_input.marko_ _input.marko_
```marko ```marko
div class="thumbnail" div class="thumbnail"
img src="https://example.com/thumb.png" img src="https://example.com/thumb.png"
``` ```
_output.html_ _output.html_
```html ```html
<div class="thumbnail"> <div class="thumbnail">
<img src="https://example.com/thumb.png"/> <img src="https://example.com/thumb.png"/>
@ -20,6 +22,7 @@ _output.html_
Marko provides a shorthand for declaring classes and ids on an element: Marko provides a shorthand for declaring classes and ids on an element:
_input.marko_ _input.marko_
```marko ```marko
div.my-class div.my-class
span#my-id span#my-id
@ -29,6 +32,7 @@ button#submit.primary.large
Yields this HTML: Yields this HTML:
_output.html_ _output.html_
```html ```html
<div class="my-class"></div> <div class="my-class"></div>
<span id="my-id"></span> <span id="my-id"></span>
@ -39,11 +43,12 @@ _output.html_
## Text ## Text
Text in concise mode is denoted by two or more dashes (`--`). Text in concise mode is denoted by two or more dashes (`--`).
If there is text on the same line following `--`, it is single-line text: If there is text on the same line following `--`, it is single-line text:
_single-line-text.marko_ _single-line-text.marko_
```marko ```marko
-- Hello world -- Hello world
``` ```
@ -51,6 +56,7 @@ _single-line-text.marko_
The dashes can also follow an element to give it a single text node as a child The dashes can also follow an element to give it a single text node as a child
_single-line-text.marko_ _single-line-text.marko_
```marko ```marko
div -- Hello world div -- Hello world
``` ```
@ -58,6 +64,7 @@ div -- Hello world
If there is a line break immediately following `--`, everything following the `--` at the current indentation is parsed as multi-line line text. If there is a line break immediately following `--`, everything following the `--` at the current indentation is parsed as multi-line line text.
_multi-line-text.marko_ _multi-line-text.marko_
```marko ```marko
div div
-- --
@ -71,9 +78,10 @@ div
text text
``` ```
A multi-line text block can be ended by the same number of dashes that opened it. This allows it to have siblings: A multi-line text block can be ended by the same number of dashes that opened it. This allows it to have siblings:
_multi-line-text.marko_ _multi-line-text.marko_
```marko ```marko
div div
img src="https://example.com/photo.png" img src="https://example.com/photo.png"
@ -90,6 +98,7 @@ div
There is one "gotcha" that you need to be aware of. The Marko parser starts out in the concise mode. Therefore, given the following template: There is one "gotcha" that you need to be aware of. The Marko parser starts out in the concise mode. Therefore, given the following template:
_input.marko_ _input.marko_
```marko ```marko
Hello World Hello World
Welcome to Marko Welcome to Marko
@ -98,6 +107,7 @@ Welcome to Marko
The output would be the following: The output would be the following:
_output.html_ _output.html_
```html ```html
<Hello World></Hello> <Hello World></Hello>
<Welcome to Marko></Welcome> <Welcome to Marko></Welcome>
@ -106,7 +116,8 @@ _output.html_
Instead, prefix the lines with `--` so they are parsed as text: Instead, prefix the lines with `--` so they are parsed as text:
_input.marko_ _input.marko_
```marko ```marko
-- Hello World -- Hello World
-- Welcome to Marko -- Welcome to Marko
``` ```

View File

@ -39,6 +39,7 @@ And support complex expressions:
### `<for>` ### `<for>`
The `<for>` tag allows iterating over an array of items: The `<for>` tag allows iterating over an array of items:
```marko ```marko
<ul> <ul>
<for(color in colors)> <for(color in colors)>
@ -48,6 +49,7 @@ The `<for>` tag allows iterating over an array of items:
``` ```
It may also be applied as an attribute: It may also be applied as an attribute:
```marko ```marko
<ul> <ul>
<li for(color in colors)>${color}</li> <li for(color in colors)>${color}</li>
@ -57,7 +59,7 @@ It may also be applied as an attribute:
With either of the above templates, and the following value for `colors`: With either of the above templates, and the following value for `colors`:
```js ```js
var colors = ['red', 'green', 'blue']; var colors = ["red", "green", "blue"];
``` ```
The output HTML would be the following: The output HTML would be the following:
@ -167,7 +169,6 @@ The `from`, `to` and `step` values must be numerical expressions. If not specifi
#### Custom Iterator #### Custom Iterator
```marko ```marko
static function reverseIterator(arrayList, callback) { static function reverseIterator(arrayList, callback) {
for(var i=arrayList.length-1; i>=0; i--){ for(var i=arrayList.length-1; i>=0; i--){
@ -218,7 +219,6 @@ $ var n = 0;
### `body-only-if` ### `body-only-if`
If you find that you have a wrapper element that is conditional, but whose body should always be rendered then you can use the `body-only-if` attribute to handle this use case. For example, to only render a wrapping `<a>` tag if there is a valid URL then you could do the following: If you find that you have a wrapper element that is conditional, but whose body should always be rendered then you can use the `body-only-if` attribute to handle this use case. For example, to only render a wrapping `<a>` tag if there is a valid URL then you could do the following:
```marko ```marko
@ -246,9 +246,10 @@ Some body content
The following tags are always written using the [concise syntax](./concise.md), even when using HTML syntax for tags that generate HTML output. The following tags are always written using the [concise syntax](./concise.md), even when using HTML syntax for tags that generate HTML output.
### `import` ### `import`
> **Static:** The code generated by `import` will run once when the template is loaded and be shared by all calls to render. It must be declared as a top level tag and does not have access to `data`, `state`, or other values passed in at render. > **Static:** The code generated by `import` will run once when the template is loaded and be shared by all calls to render. It must be declared as a top level tag and does not have access to `data`, `state`, or other values passed in at render.
The `import` tag is used to access data and functions from external files. It follows the same syntax as the [JavaScript `import` statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import). The `import` tag is used to access data and functions from external files. It follows the same syntax as the [JavaScript `import` statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import).
```marko ```marko
import sum from './utils/sum'; import sum from './utils/sum';
@ -308,9 +309,10 @@ import Hello from './components/hello/index.marko';
#### Layouts with nested attributes #### Layouts with nested attributes
In addition to including external content, you can inject additional content chunks into the external content. This is accomplished by using nested attribute tags which are denoted by the `@` symbol: In addition to including external content, you can inject additional content chunks into the external content. This is accomplished by using nested attribute tags which are denoted by the `@` symbol:
_page.marko_ _page.marko_
```marko ```marko
<include('./layout.marko')> <include('./layout.marko')>
<@body> <@body>
@ -322,6 +324,7 @@ _page.marko_
Then in your layout template you can include the injected content: Then in your layout template you can include the injected content:
_layout.marko_ _layout.marko_
```marko ```marko
<!doctype html> <!doctype html>
<html> <html>
@ -389,6 +392,7 @@ sample template shows how to use macro functions inside expressions:
The `<await>` tag is used to dynamically load in content from a data provider. The data provider can be a `Promise` or a `callback`. Once the provider returns it's results the children are rendered. The `<await>` tag is used to dynamically load in content from a data provider. The data provider can be a `Promise` or a `callback`. Once the provider returns it's results the children are rendered.
await-example.marko await-example.marko
```marko ```marko
$ var personPromise = new Promise((resolve, reject) => { $ var personPromise = new Promise((resolve, reject) => {
setTimeout(function() { setTimeout(function() {
@ -404,27 +408,28 @@ $ var personPromise = new Promise((resolve, reject) => {
``` ```
Advanced implementation: Advanced implementation:
+ `<await>` tag signature
* Basic usage: `<await(results from dataProvider)>...</await>` * `<await>` tag signature
* Optional attributes * Basic usage: `<await(results from dataProvider)>...</await>`
- client-reorder `boolean` * Optional attributes
- arg `expression` * client-reorder `boolean`
- arg-* `string` * arg `expression`
- method `string` * arg-\* `string`
- timeout `integer` * method `string`
- timeout-message `string` * timeout `integer`
- error-message `string` * timeout-message `string`
- placeholder `string` * error-message `string`
- renderTimeout `function` * placeholder `string`
- renderError `function` * renderTimeout `function`
- renderPlaceholder `function` * renderError `function`
- name `string` * renderPlaceholder `function`
- scope `expression` * name `string`
- show-after `string` * scope `expression`
* Optional child tags * show-after `string`
- `<await-placeholder>Loading...</await-placeholder>` * Optional child tags
- `<await-timeout>Request timed out</await-timeout>` * `<await-placeholder>Loading...</await-placeholder>`
- `<await-error>Request errored</await-error>` * `<await-timeout>Request timed out</await-timeout>`
* `<await-error>Request errored</await-error>`
## Comments ## Comments
@ -442,7 +447,7 @@ Example comments:
/* /*
Block comments are also supported Block comments are also supported
*/ */
-- Hello --Hello;
``` ```
If you would like for your HTML comment to show up in the final output then you can use the custom `html-comment` tag. If you would like for your HTML comment to show up in the final output then you can use the custom `html-comment` tag.
@ -450,18 +455,21 @@ If you would like for your HTML comment to show up in the final output then you
### `<html-comment>` ### `<html-comment>`
_input.marko_ _input.marko_
```marko ```marko
<html-comment>This is a comment that *will* be rendered</html-comment> <html-comment>This is a comment that *will* be rendered</html-comment>
<h1>Hello</h1> <h1>Hello</h1>
``` ```
_output.html_ _output.html_
```html ```html
<!--This is a comment that *will* be rendered--> <!--This is a comment that *will* be rendered-->
<h1>Hello</h1> <h1>Hello</h1>
``` ```
Alternatively, the `<marko-compiler-options>` tag may be used to configure comments for the entire template: Alternatively, the `<marko-compiler-options>` tag may be used to configure comments for the entire template:
```marko ```marko
<marko-compiler-options preserve-comments/> <marko-compiler-options preserve-comments/>
``` ```
@ -479,6 +487,7 @@ Whitespace can be preserved using the `preserve-whitespace` attribute:
be preserved. be preserved.
</div> </div>
``` ```
Alternatively, the `<marko-compiler-options>` tag may be used to configure whitespace for the entire template: Alternatively, the `<marko-compiler-options>` tag may be used to configure whitespace for the entire template:
```marko ```marko
@ -488,11 +497,13 @@ Alternatively, the `<marko-compiler-options>` tag may be used to configure white
### `marko-body` ### `marko-body`
The `marko-body` attribute can be used to control how body content is parsed. The following values are supported: The `marko-body` attribute can be used to control how body content is parsed. The following values are supported:
- `html` - Body content will be parsed HTML (the default)
- `static-text` - Body content will be parsed as static text (HTML tags will be ignored). Placeholders will be ignored. * `html` - Body content will be parsed HTML (the default)
- `parsed-text` - Body content will be parsed as text (HTML tags will be ignored). Placeholders will not be ignored. * `static-text` - Body content will be parsed as static text (HTML tags will be ignored). Placeholders will be ignored.
* `parsed-text` - Body content will be parsed as text (HTML tags will be ignored). Placeholders will not be ignored.
_input.marko_ _input.marko_
```marko ```marko
<div marko-body="static-text"> <div marko-body="static-text">
This is just one This is just one
@ -504,6 +515,7 @@ _input.marko_
``` ```
_output.html_ _output.html_
```html ```html
<div> <div>
This is just one This is just one

View File

@ -10,7 +10,7 @@ To get started let's look at template-based tags which allow you to include anot
### Discovering tags ### Discovering tags
When compiling a template Marko will search starting at template's directory, up to the project root for directories named `components/`. It then attempts to load the children of these directories as custom tags. The children can be a Marko template or a directory with an `index.marko` template (and other supporting files). When compiling a template Marko will search starting at template's directory, up to the project root for directories named `components/`. It then attempts to load the children of these directories as custom tags. The children can be a Marko template or a directory with an `index.marko` template (and other supporting files).
```dir ```dir
components/ components/
@ -28,9 +28,9 @@ pages/
When compiling the template at `pages/home/index.marko`, the following tags would be found: When compiling the template at `pages/home/index.marko`, the following tags would be found:
- `<app-header>` * `<app-header>`
- `<app-footer>` * `<app-footer>`
- `<home-banner>` * `<home-banner>`
So now, instead of needing to specify a path: So now, instead of needing to specify a path:
@ -39,19 +39,20 @@ So now, instead of needing to specify a path:
``` ```
You can just use the tag name: You can just use the tag name:
```marko ```marko
<app-header/> <app-header/>
``` ```
## Using tags from npm ## Using tags from npm
Using custom tags from `npm` is easy. Ensure that the package is installed and listed in your `package.json` dependencies: Using custom tags from `npm` is easy. Ensure that the package is installed and listed in your `package.json` dependencies:
``` ```
npm install --save some-third-party-package npm install --save some-third-party-package
``` ```
And that's it. Marko will now discover these tags when compiling your templates and you can simply use them in your templates: And that's it. Marko will now discover these tags when compiling your templates and you can simply use them in your templates:
```marko ```marko
<div> <div>
@ -67,52 +68,51 @@ As an example, given a template at path `/my-project/src/pages/login/template.ma
```json ```json
{ {
"name": "my-package", "name": "my-package",
"dependencies": { "dependencies": {
"foo": "1.0.0" "foo": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"bar": "1.0.0" "bar": "1.0.0"
} }
} }
``` ```
The search path will be the following: The search path will be the following:
1. `/my-project/src/pages/login/marko.json` 1. `/my-project/src/pages/login/marko.json`
2. `/my-project/src/pages/marko.json` 2. `/my-project/src/pages/marko.json`
3. `/my-project/src/marko.json` 3. `/my-project/src/marko.json`
4. `/my-project/marko.json` 4. `/my-project/marko.json`
5. `/my-project/node_modules/foo/marko.json` 5. `/my-project/node_modules/foo/marko.json`
6. `/my-project/node_modules/bar/marko.json` 6. `/my-project/node_modules/bar/marko.json`
### Hiding taglibs ### Hiding taglibs
If you wish to hide particular folder and/or node_module from discovery of marko.json, you can exclude certain directories or packages. This is used primarily for testing. If you wish to hide particular folder and/or node_module from discovery of marko.json, you can exclude certain directories or packages. This is used primarily for testing.
```javascript ```javascript
require('marko/compiler').taglibFinder.excludeDir(dirPath); require("marko/compiler").taglibFinder.excludeDir(dirPath);
// Where 'dirPath' is an absolute path to the folder containing marko.json // Where 'dirPath' is an absolute path to the folder containing marko.json
require('marko/compiler').taglibFinder.excludePackage(packageName); require("marko/compiler").taglibFinder.excludePackage(packageName);
// Where 'packageName' is the name of the node_module containing marko.json // Where 'packageName' is the name of the node_module containing marko.json
``` ```
These statements should be used before any rendering begins in the process. These statements should be used before any rendering begins in the process.
### marko.json syntax ### marko.json syntax
```json ```json
{ {
"tags": { "tags": {
"my-hello": { "my-hello": {
"renderer": "./hello-renderer", "renderer": "./hello-renderer",
"attributes": { "attributes": {
"name": "string" "name": "string"
} }
}
} }
}
} }
``` ```
@ -120,10 +120,10 @@ Marko also supports a short-hand for declaring tags and attributes. The followin
```json ```json
{ {
"<my-hello>": { "<my-hello>": {
"renderer": "./hello-renderer", "renderer": "./hello-renderer",
"@name": "string" "@name": "string"
} }
} }
``` ```
@ -133,18 +133,18 @@ Tags can be defined by adding `"<tag_name>": <tag_def>` properties to your `mark
```json ```json
{ {
"<my-hello>": { "<my-hello>": {
"renderer": "./hello-renderer", "renderer": "./hello-renderer",
"@name": "string" "@name": "string"
}, },
"<my-foo>": { "<my-foo>": {
"renderer": "./foo-renderer", "renderer": "./foo-renderer",
"@*": "string" "@*": "string"
}, },
"<my-bar>": "./path/to/my-bar/marko-tag.json", "<my-bar>": "./path/to/my-bar/marko-tag.json",
"<my-baz>": { "<my-baz>": {
"template": "./baz-template.marko" "template": "./baz-template.marko"
}, }
} }
``` ```
@ -186,11 +186,11 @@ Above, we tell Marko this attribute name contains a pattern so that it will allo
### Custom directory scanning ### Custom directory scanning
You can configure the `tags-dir` value in your `marko.json` to configure the name of the directory that marko scans in for custom tags. As described above, by default it uses the name `components/`. You can override this at a directory level and give a path to another directory to scan: You can configure the `tags-dir` value in your `marko.json` to configure the name of the directory that marko scans in for custom tags. As described above, by default it uses the name `components/`. You can override this at a directory level and give a path to another directory to scan:
```json ```json
{ {
"tags-dir": "./ui-components" "tags-dir": "./ui-components"
} }
``` ```
@ -198,7 +198,7 @@ You can configure the `tags-dir` value in your `marko.json` to configure the nam
```json ```json
{ {
"tags-dir": ["./ui-components", "./components"] "tags-dir": ["./ui-components", "./components"]
} }
``` ```

View File

@ -1,40 +1,39 @@
# Editor Plugins # Editor Plugins
## Atom ## Atom
[Documentation](https://atom.io/packages/language-marko) [Documentation](https://atom.io/packages/language-marko)
- Syntax highlighting * Syntax highlighting
- Tag matching * Tag matching
- Tag and attribute autocompletion * Tag and attribute autocompletion
- Code snippets * Code snippets
- Hyperclick (clickable tags and attributes) * Hyperclick (clickable tags and attributes)
- Prettyprinting ([marko-prettyprint](https://github.com/marko-js/marko-prettyprint) is used internally) * Prettyprinting ([marko-prettyprint](https://github.com/marko-js/marko-prettyprint) is used internally)
## Visual Studio Code ## Visual Studio Code
[Documentation](https://marketplace.visualstudio.com/items?itemName=pcanella.marko) [Documentation](https://marketplace.visualstudio.com/items?itemName=pcanella.marko)
- Syntax highlighting * Syntax highlighting
## Sublime ## Sublime
[Documentation](https://github.com/merwan7/sublime-marko) [Documentation](https://github.com/merwan7/sublime-marko)
- Syntax highlighting * Syntax highlighting
## WebStorm ## WebStorm
[Documentation](https://github.com/marko-js/marko-tmbundle) [Documentation](https://github.com/marko-js/marko-tmbundle)
- Syntax highlighting * Syntax highlighting
## TextMate ## TextMate
[Documentation](https://github.com/marko-js/marko-tmbundle) [Documentation](https://github.com/marko-js/marko-tmbundle)
- Syntax highlighting * Syntax highlighting
## CodeMirror ## CodeMirror

View File

@ -1,5 +1,4 @@
Express + Marko # Express + Marko
=====================
See the [marko-express](https://github.com/marko-js-samples/marko-express) sample See the [marko-express](https://github.com/marko-js-samples/marko-express) sample
project for a working example. project for a working example.
@ -13,32 +12,31 @@ npm install marko --save
## Skip the view engine ## Skip the view engine
The built in view engine for express may be asynchronous, but it doesn't support streaming (check out [Rediscovering Progressive HTML Rendering](http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/) to see why this is so important). So instead we'll [bypass the view engine](https://strongloop.com/strongblog/bypassing-express-view-rendering-for-speed-and-modularity/). The built in view engine for express may be asynchronous, but it doesn't support streaming (check out [Rediscovering Progressive HTML Rendering](http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/) to see why this is so important). So instead we'll [bypass the view engine](https://strongloop.com/strongblog/bypassing-express-view-rendering-for-speed-and-modularity/).
## Usage ## Usage
Marko provides a submodule (`marko/express`) to add a `res.marko` method to the express response object. This function works much like `res.render`, but doesn't impose the restrictions of the express view engine and allows you to take full advantage of Marko's streaming and modular approach to templates. Marko provides a submodule (`marko/express`) to add a `res.marko` method to the express response object. This function works much like `res.render`, but doesn't impose the restrictions of the express view engine and allows you to take full advantage of Marko's streaming and modular approach to templates.
By using `res.marko` you'll automatically have access to `req`, `res`, `app`, `app.locals`, and `res.locals` from within your Marko template and custom tags. These values are added to `out.global`. By using `res.marko` you'll automatically have access to `req`, `res`, `app`, `app.locals`, and `res.locals` from within your Marko template and custom tags. These values are added to `out.global`.
```javascript ```javascript
require("marko/node-require"); // Allow Node.js to require and load `.marko` files
require('marko/node-require'); // Allow Node.js to require and load `.marko` files var express = require("express");
var markoExpress = require("marko/express");
var express = require('express'); var template = require("./template");
var markoExpress = require('marko/express');
var template = require('./template');
var app = express(); var app = express();
app.use(markoExpress()); //enable res.marko(template, data) app.use(markoExpress()); //enable res.marko(template, data)
app.get('/', function(req, res) { app.get("/", function(req, res) {
res.marko(template, { res.marko(template, {
name: 'Frank', name: "Frank",
count: 30, count: 30,
colors: ['red', 'green', 'blue'] colors: ["red", "green", "blue"]
}); });
}); });
app.listen(8080); app.listen(8080);

View File

@ -14,24 +14,24 @@ npm install marko --save
## Usage ## Usage
```js ```js
const fastify = require('fastify')(); const fastify = require("fastify")();
fastify.register(require('point-of-view'), { fastify.register(require("point-of-view"), {
engine: { engine: {
marko: require('marko') marko: require("marko")
} }
}); });
fastify.get('/', (req, reply) => { fastify.get("/", (req, reply) => {
reply.view('/index.marko', { reply.view("/index.marko", {
name: 'Frank', name: "Frank",
count: 30, count: 30,
colors: ['red', 'green', 'blue'] colors: ["red", "green", "blue"]
}); });
}); });
fastify.listen(8080, err => { fastify.listen(8080, err => {
if (err) throw err; if (err) throw err;
console.log(`Server listening on ${fastify.server.address().port}`); console.log(`Server listening on ${fastify.server.address().port}`);
}); });
``` ```

View File

@ -7,6 +7,7 @@ The easiest way to get started with Marko is to use the [Try Online](https://mar
Marko makes it easy to represent your UI using a syntax that is like HTML: Marko makes it easy to represent your UI using a syntax that is like HTML:
_hello.marko_ _hello.marko_
```marko ```marko
<h1>Hello World</h1> <h1>Hello World</h1>
``` ```
@ -14,6 +15,7 @@ _hello.marko_
In fact, Marko is so much like HTML, that you can use it as a replacement for a templating language like handlebars, mustache, or pug: In fact, Marko is so much like HTML, that you can use it as a replacement for a templating language like handlebars, mustache, or pug:
_template.marko_ _template.marko_
```marko ```marko
<!doctype html> <!doctype html>
<html> <html>
@ -26,7 +28,7 @@ _template.marko_
</html> </html>
``` ```
However, Marko is much more than a templating language. It's a UI library that allows you to break your application into components that are self-contained and describe how the application view changes over time and in response to user actions. However, Marko is much more than a templating language. It's a UI library that allows you to break your application into components that are self-contained and describe how the application view changes over time and in response to user actions.
In the browser, when the data representing your UI changes, Marko will automatically and efficiently update the DOM to reflect the changes. In the browser, when the data representing your UI changes, Marko will automatically and efficiently update the DOM to reflect the changes.
@ -35,6 +37,7 @@ In the browser, when the data representing your UI changes, Marko will automatic
Let's say we have a `<button>` that we want to assign some behavior to when it is clicked: Let's say we have a `<button>` that we want to assign some behavior to when it is clicked:
_button.marko_ _button.marko_
```marko ```marko
<button>Click me!</button> <button>Click me!</button>
``` ```
@ -42,6 +45,7 @@ _button.marko_
Marko makes this really easy, allowing you to define a `class` for a component right in the `.marko` view and call methods of that class with `on-` attributes: Marko makes this really easy, allowing you to define a `class` for a component right in the `.marko` view and call methods of that class with `on-` attributes:
_button.marko_ _button.marko_
```marko ```marko
class { class {
sayHi() { sayHi() {
@ -54,9 +58,10 @@ class {
### Adding state ### Adding state
Alerting when a button is clicked is great, but what about updating your UI in response to an action? Marko's stateful components make this easy. All you need to do is set `this.state` from inside your component's class. This makes a new `state` variable available to your view. When a value in `this.state` is changed, the view will automatically re-render and only update the part of the DOM that changed. Alerting when a button is clicked is great, but what about updating your UI in response to an action? Marko's stateful components make this easy. All you need to do is set `this.state` from inside your component's class. This makes a new `state` variable available to your view. When a value in `this.state` is changed, the view will automatically re-render and only update the part of the DOM that changed.
_counter.marko_ _counter.marko_
```marko ```marko
class { class {
onCreate() { onCreate() {

View File

@ -13,13 +13,13 @@ npm install marko --save
## Usage ## Usage
```javascript ```javascript
'use strict'; "use strict";
require('marko/node-require'); require("marko/node-require");
const Hapi = require('hapi'); const Hapi = require("hapi");
const indexTemplate = require('./index'); const indexTemplate = require("./index");
const server = new Hapi.Server(); const server = new Hapi.Server();
const port = 8080; const port = 8080;
@ -27,22 +27,24 @@ const port = 8080;
server.connection({ port }); server.connection({ port });
server.route({ server.route({
method: 'GET', method: "GET",
path: '/', path: "/",
handler (request, reply) { handler(request, reply) {
return reply(indexTemplate.stream({ return reply(
name: 'Frank', indexTemplate.stream({
count: 30, name: "Frank",
colors: ['red', 'green', 'blue'] count: 30,
})).type('text/html'); colors: ["red", "green", "blue"]
} })
).type("text/html");
}
}); });
server.start((err) => { server.start(err => {
if (err) { if (err) {
throw err; throw err;
} }
console.log(`Server running on port: ${port}`); console.log(`Server running on port: ${port}`);
}); });
``` ```

View File

@ -12,23 +12,26 @@ npm install marko --save
## Usage ## Usage
```js ```js
require('marko/node-require').install(); require("marko/node-require").install();
const http = require('http'); const http = require("http");
const server = require('http').createServer(); const server = require("http").createServer();
const port = 8080; const port = 8080;
const indexTemplate = require('./index.marko'); const indexTemplate = require("./index.marko");
server.on('request', (req, res) => { server.on("request", (req, res) => {
indexTemplate.render({ indexTemplate.render(
name: 'Frank', {
count: 30, name: "Frank",
colors: ['red', 'green', 'blue'] count: 30,
}, res); colors: ["red", "green", "blue"]
},
res
);
}); });
server.listen(port, () => { server.listen(port, () => {
console.log(`Successfully started server on port ${port}`); console.log(`Successfully started server on port ${port}`);
}); });
``` ```

View File

@ -10,14 +10,14 @@ project for a working example.
## Usage ## Usage
```javascript ```javascript
require('marko/node-require'); require("marko/node-require");
const Huncwot = require('huncwot'); const Huncwot = require("huncwot");
const app = new Huncwot(); const app = new Huncwot();
const template = require('./index.marko'); const template = require("./index.marko");
app.get('/', request => template.stream({ name: 'Frank' })) app.get("/", request => template.stream({ name: "Frank" }));
app.listen(3000); app.listen(3000);
``` ```

View File

@ -2,7 +2,7 @@
## Trying out Marko ## Trying out Marko
If you just want to play around with Marko in the browser, head on over to our [Try Online](https://markojs.com/try-online) feature. You'll be able to develop a Marko application right in your browser. If you just want to play around with Marko in the browser, head on over to our [Try Online](https://markojs.com/try-online) feature. You'll be able to develop a Marko application right in your browser.
## Creating new apps ## Creating new apps
@ -37,6 +37,7 @@ yarn add marko
Let's say we have a simple view that we want to render in the browser: `hello.marko` Let's say we have a simple view that we want to render in the browser: `hello.marko`
_hello.marko_ _hello.marko_
```marko ```marko
<h1>Hello ${input.name}</h1> <h1>Hello ${input.name}</h1>
``` ```
@ -44,16 +45,17 @@ _hello.marko_
First, let's create a `client.js` that requires the view and renders it to the body: First, let's create a `client.js` that requires the view and renders it to the body:
_client.js_ _client.js_
```js
var helloComponent = require('./hello');
helloComponent.renderSync({ name:'Marko' }) ```js
.appendTo(document.body); var helloComponent = require("./hello");
helloComponent.renderSync({ name: "Marko" }).appendTo(document.body);
``` ```
We will also create a barebones HTML page to host our application: We will also create a barebones HTML page to host our application:
_index.html_ _index.html_
``` ```
<!doctype html> <!doctype html>
<html> <html>
@ -66,7 +68,7 @@ _index.html_
</html> </html>
``` ```
Now, we need to bundle these files for use in the browser. We can use a tool called [`lasso`](https://github.com/lasso-js/lasso) to do that for us, so let's get it (and the marko plugin) installed: Now, we need to bundle these files for use in the browser. We can use a tool called [`lasso`](https://github.com/lasso-js/lasso) to do that for us, so let's get it (and the marko plugin) installed:
``` ```
npm install --global lasso-cli npm install --global lasso-cli
@ -79,7 +81,7 @@ Now we can build our bundle for the browser:
lasso --main client.js --plugins lasso-marko --inject-into index.html lasso --main client.js --plugins lasso-marko --inject-into index.html
``` ```
This builds a `client.js` file to the newly created `static/` directory and injects the required `<script>` tags into our HTML page to load our application in the browser. If we had css in the view then `<link>` tags would have also been added. This builds a `client.js` file to the newly created `static/` directory and injects the required `<script>` tags into our HTML page to load our application in the browser. If we had css in the view then `<link>` tags would have also been added.
Load up that page in your browser and you should see `Hello Marko` staring back at you. Load up that page in your browser and you should see `Hello Marko` staring back at you.
@ -90,6 +92,7 @@ Load up that page in your browser and you should see `Hello Marko` staring back
Marko provides a custom Node.js require extension that allows you to `require` Marko views exactly like a standard JavaScript module. Take the following example `server.js`: Marko provides a custom Node.js require extension that allows you to `require` Marko views exactly like a standard JavaScript module. Take the following example `server.js`:
_hello.marko_ _hello.marko_
```marko ```marko
<div> <div>
Hello ${input.name}! Hello ${input.name}!
@ -97,23 +100,23 @@ _hello.marko_
``` ```
_server.js_ _server.js_
```js ```js
// The following line installs the Node.js require extension // The following line installs the Node.js require extension
// for `.marko` files. This should be called once near the start // for `.marko` files. This should be called once near the start
// of your application before requiring any `*.marko` files. // of your application before requiring any `*.marko` files.
require('marko/node-require'); require("marko/node-require");
var fs = require('fs'); var fs = require("fs");
// Load a Marko view by requiring a .marko file: // Load a Marko view by requiring a .marko file:
var hello = require('./hello'); var hello = require("./hello");
var out = fs.createWriteStream('hello.html', { encoding: 'utf8' }); var out = fs.createWriteStream("hello.html", { encoding: "utf8" });
hello.render({ name: 'Frank' }, out); hello.render({ name: "Frank" }, out);
``` ```
Using the Node.js require extension is completely optional. If you prefer to not use the Node.js require extension then you will need to precompile all of the marko templates using [Marko CLI](https://github.com/marko-js/marko-cli): Using the Node.js require extension is completely optional. If you prefer to not use the Node.js require extension then you will need to precompile all of the marko templates using [Marko CLI](https://github.com/marko-js/marko-cli):
```bash ```bash
marko compile hello.marko marko compile hello.marko
``` ```
@ -121,9 +124,10 @@ marko compile hello.marko
This will produce a `hello.marko.js` file next to the original template. The generated `.js` file will be what gets loaded by the Node.js runtime. It is important to leave off the `.marko` extension when requiring a Marko template so that the `.js` will be resolved correctly. This will produce a `hello.marko.js` file next to the original template. The generated `.js` file will be what gets loaded by the Node.js runtime. It is important to leave off the `.marko` extension when requiring a Marko template so that the `.js` will be resolved correctly.
If you wish to only use the require extension in development, you can conditionally require it. If you wish to only use the require extension in development, you can conditionally require it.
```js ```js
if (!process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
require('marko/node-require'); require("marko/node-require");
} }
``` ```
@ -132,26 +136,30 @@ if (!process.env.NODE_ENV) {
Let's update `server.js` to serve the view from an http server: Let's update `server.js` to serve the view from an http server:
_server.js_ _server.js_
```js ```js
// Allow requiring `.marko` files // Allow requiring `.marko` files
require('marko/node-require'); require("marko/node-require");
var http = require('http'); var http = require("http");
var hello = require('./hello'); var hello = require("./hello");
var port = 8080; var port = 8080;
http.createServer((req, res) => { http
.createServer((req, res) => {
// let the browser know html is coming // let the browser know html is coming
res.setHeader('content-type', 'text/html'); res.setHeader("content-type", "text/html");
// render the output to the `res` output stream // render the output to the `res` output stream
hello.render({ name:'Marko' }, res); hello.render({ name: "Marko" }, res);
}).listen(port); })
.listen(port);
``` ```
And give `hello.marko` some content: And give `hello.marko` some content:
_hello.marko_ _hello.marko_
```marko ```marko
<h1>Hello ${input.name}</h1> <h1>Hello ${input.name}</h1>
``` ```
@ -160,9 +168,9 @@ Start the server (`node server.js`) and open your browser to [http://localhost:8
#### Initializing server-rendered components #### Initializing server-rendered components
Marko automatically injects a list of components that need to be mounted in the browser, right before the closing `</body>` tag (as such, it required that you include a `<body>` in your rendered output). Marko automatically injects a list of components that need to be mounted in the browser, right before the closing `</body>` tag (as such, it required that you include a `<body>` in your rendered output).
However, you still need to bundle the CSS & JavaScript for your page and include the proper `link`, `style`, and `script` tags. Luckily, the `lasso` taglib will do all the heavy lifting for you. However, you still need to bundle the CSS & JavaScript for your page and include the proper `link`, `style`, and `script` tags. Luckily, the `lasso` taglib will do all the heavy lifting for you.
First install `lasso` and `lasso-marko`: First install `lasso` and `lasso-marko`:
@ -173,6 +181,7 @@ npm install --save lasso lasso-marko
Next, in your page or layout view, add the `lasso-head` and `lasso-body` tags: Next, in your page or layout view, add the `lasso-head` and `lasso-body` tags:
_layout.marko_ _layout.marko_
```marko ```marko
<!doctype> <!doctype>
<html> <html>
@ -190,6 +199,7 @@ _layout.marko_
Finally, configure your server to serve the static files that `lasso` generates: Finally, configure your server to serve the static files that `lasso` generates:
_server.js_ _server.js_
```js ```js
app.use(require('lasso/middleware').serveStatic()); app.use(require("lasso/middleware").serveStatic());
``` ```

View File

@ -11,20 +11,20 @@ project for a fully-working example.
## Usage ## Usage
```javascript ```javascript
require('marko/node-require'); require("marko/node-require");
const Koa = require('koa'); const Koa = require("koa");
const app = new Koa(); const app = new Koa();
const template = require('./index.marko'); const template = require("./index.marko");
app.use((ctx, next) => { app.use((ctx, next) => {
ctx.type = 'html'; ctx.type = "html";
ctx.body = template.stream({ ctx.body = template.stream({
name: 'Frank', name: "Frank",
count: 30, count: 30,
colors: ['red', 'green', 'blue'] colors: ["red", "green", "blue"]
}); });
}); });
app.listen(8080); app.listen(8080);
@ -33,27 +33,27 @@ app.listen(8080);
You may also easily add `gzip` streaming support without additional dependencies: You may also easily add `gzip` streaming support without additional dependencies:
```javascript ```javascript
require('marko/node-require'); require("marko/node-require");
const { createGzip } = require('zlib'); const { createGzip } = require("zlib");
const Koa = require('koa'); const Koa = require("koa");
const app = new Koa(); const app = new Koa();
const template = require('./index.marko'); const template = require("./index.marko");
app.use((ctx, next) => { app.use((ctx, next) => {
ctx.type = 'html'; ctx.type = "html";
ctx.body = template.stream({ ctx.body = template.stream({
name: 'Frank', name: "Frank",
count: 30, count: 30,
colors: ['red', 'green', 'blue'] colors: ["red", "green", "blue"]
}); });
ctx.vary('Accept-Encoding'); ctx.vary("Accept-Encoding");
if (ctx.acceptsEncodings('gzip')) { if (ctx.acceptsEncodings("gzip")) {
ctx.set('Content-Encoding', 'gzip'); ctx.set("Content-Encoding", "gzip");
ctx.body = ctx.body.pipe(createGzip()); ctx.body = ctx.body.pipe(createGzip());
} }
}); });
app.listen(8080); app.listen(8080);

View File

@ -56,12 +56,10 @@ After installing, the lasso custom tags can be used in your templates:
The `browser.json` provides a simple way for declaring _top-level_ page dependencies. For example: The `browser.json` provides a simple way for declaring _top-level_ page dependencies. For example:
_browser.json_ _browser.json_
```json ```json
{ {
"dependencies": [ "dependencies": ["./style.css", "require-run: ./client.js"]
"./style.css",
"require-run: ./client.js"
]
} }
``` ```
@ -72,10 +70,11 @@ Lasso.js will automatically bundle up transitive dependencies by building and wa
Marko templates can be imported and rendered by any JavaScript module. The code below shows how to render a top-level UI component and have it be mounted to the DOM as a child `document.body`: Marko templates can be imported and rendered by any JavaScript module. The code below shows how to render a top-level UI component and have it be mounted to the DOM as a child `document.body`:
_client.js_ _client.js_
```js ```js
require('./components/app/index.marko') require("./components/app/index.marko")
.renderSync({}) .renderSync({})
.appendTo(document.body); .appendTo(document.body);
``` ```
When Lasso.js bundles up the code above it will automatically bundle up the required `./components/app/index.marko` file. When Lasso.js bundles up the code above it will automatically bundle up the required `./components/app/index.marko` file.
@ -84,8 +83,8 @@ When Lasso.js bundles up the code above it will automatically bundle up the requ
If you are rendering the initial UI on the server then it is necessary to make sure that all UI components are bundled and sent to the browser so that UI components can be mounted in the browser. For example: If you are rendering the initial UI on the server then it is necessary to make sure that all UI components are bundled and sent to the browser so that UI components can be mounted in the browser. For example:
_about-me/index.marko_ _about-me/index.marko_
```marko ```marko
<lasso-page package-path="./browser.json" /> <lasso-page package-path="./browser.json" />
@ -111,12 +110,10 @@ _about-me/index.marko_
Typically, adding the top-level UI component as a page dependency is all that is required: Typically, adding the top-level UI component as a page dependency is all that is required:
_about-me/browser.json_ _about-me/browser.json_
```json ```json
{ {
"dependencies": [ "dependencies": ["./style.css", "require: ./components/app/index.marko"]
"./style.css",
"require: ./components/app/index.marko"
]
} }
``` ```

View File

@ -47,13 +47,14 @@ style.less {
You can easily `require`/`import` a single file component and interact with it using the exported JavaScript API: You can easily `require`/`import` a single file component and interact with it using the exported JavaScript API:
```js ```js
var clickCount = require('./src/components/click-count'); var clickCount = require("./src/components/click-count");
var component = clickCount.renderSync({ var component = clickCount
value: 10 .renderSync({
}) value: 10
.appendTo(document.body) })
.getComponent(); .appendTo(document.body)
.getComponent();
component.increment(); component.increment();
``` ```
@ -68,7 +69,7 @@ Of course, a single file component can also be embedded in another template as a
### Virtual DOM support ([#366](https://github.com/marko-js/marko/issues/366)) ### Virtual DOM support ([#366](https://github.com/marko-js/marko/issues/366))
Because Marko renders raw HTML strings to a stream on the server, Marko has always been faster than other libraries by an [order of magnitude](https://github.com/patrick-steele-idem/marko-vs-react) when rendering on the server. However although Marko has been _pretty_ fast in the browser, it was a little behind some of our competitors. This was mainly because the output HTML string needed to be parsed into a DOM in order to do DOM diffing/patching. Because Marko renders raw HTML strings to a stream on the server, Marko has always been faster than other libraries by an [order of magnitude](https://github.com/patrick-steele-idem/marko-vs-react) when rendering on the server. However although Marko has been _pretty_ fast in the browser, it was a little behind some of our competitors. This was mainly because the output HTML string needed to be parsed into a DOM in order to do DOM diffing/patching.
That's changed. Marko now supports multiple compilation outputs. Templates compiled for the server will continue to render to an HTML stream/string and templates compiled for the browser will now render to a fast and lightweight virtual DOM tree. The code samples below show how the two different compilation outputs compare: That's changed. Marko now supports multiple compilation outputs. Templates compiled for the server will continue to render to an HTML stream/string and templates compiled for the browser will now render to a fast and lightweight virtual DOM tree. The code samples below show how the two different compilation outputs compare:
@ -82,9 +83,7 @@ function render(data, out) {
out.w("<ul>"); out.w("<ul>");
marko_forEach(colors, function(color) { marko_forEach(colors, function(color) {
out.w("<li class=\"color\">" + out.w('<li class="color">' + marko_escapeXml(color) + "</li>");
marko_escapeXml(color) +
"</li>");
}); });
out.w("</ul>"); out.w("</ul>");
@ -98,10 +97,11 @@ _Compiled for VDOM output (browser-side):_
```javascript ```javascript
var marko_attrs0 = { var marko_attrs0 = {
"class": "color" class: "color"
}, },
marko_node0 = marko_createElement("DIV", null, 1, marko_const_nextId()) marko_node0 = marko_createElement("DIV", null, 1, marko_const_nextId()).t(
.t("No colors!"); "No colors!"
);
function render(data, out) { function render(data, out) {
var colors = data.colors; var colors = data.colors;
@ -110,8 +110,7 @@ function render(data, out) {
out.be("UL"); out.be("UL");
marko_forEach(colors, function(color) { marko_forEach(colors, function(color) {
out.e("LI", marko_attrs0, 1) out.e("LI", marko_attrs0, 1).t(marko_str(color));
.t(marko_str(color));
}); });
out.ee(); out.ee();
@ -123,28 +122,26 @@ function render(data, out) {
The VDOM output allows optimizations that were previously not possible: The VDOM output allows optimizations that were previously not possible:
- Static subtrees are pulled into variables that are only initialized once and reused for every render * Static subtrees are pulled into variables that are only initialized once and reused for every render
- Static attributes that are on dynamic elements are pulled out to static variables * Static attributes that are on dynamic elements are pulled out to static variables
- Diffing is skipped when comparing static subtrees * Diffing is skipped when comparing static subtrees
- Diffing is skipped when comparing static attributes * Diffing is skipped when comparing static attributes
Our benchmarks show a significant improvement in rendering time and we are consistently outperforming React/Preact/Inferno, Vue and other UI libraries. Our benchmarks show a significant improvement in rendering time and we are consistently outperforming React/Preact/Inferno, Vue and other UI libraries.
### Merge in Marko Widgets ([#390](https://github.com/marko-js/marko/issues/390)) ### Merge in Marko Widgets ([#390](https://github.com/marko-js/marko/issues/390))
A big part of this release is a shift in focus from Marko being merely a templating language to a complete UI library. As such, we are providing first-class support for components. A big part of this release is a shift in focus from Marko being merely a templating language to a complete UI library. As such, we are providing first-class support for components.
You will no longer need to install `marko-widgets` as an external library, and there is more cohesion between the templates and components/components. You will no longer need to install `marko-widgets` as an external library, and there is more cohesion between the templates and components/components.
### Improved component lifecycle methods ([#396](https://github.com/marko-js/marko/issues/396)) ### Improved component lifecycle methods ([#396](https://github.com/marko-js/marko/issues/396))
* `getInitialState()``onInput(input)`
- `getInitialState()``onInput(input)` * `getComponentConfig()``onInput(input)`
- `getComponentConfig()``onInput(input)` * `init(config)``onMount()`
- `init(config)``onMount()` * `getTemplateData(input, state)` ➔ (no longer needed)
- `getTemplateData(input, state)` ➔ (no longer needed) * `getInitialProps(input)` ➔ (no longer needed)
- `getInitialProps(input)` ➔ (no longer needed)
```js ```js
class { class {
@ -190,7 +187,7 @@ class {
// Helper methods: // Helper methods:
reset() { reset() {
this.state.count = this.initialCount; this.state.count = this.initialCount;
} }
increment() { increment() {
@ -213,7 +210,7 @@ class {
return { return {
count: input.count || 0 count: input.count || 0
}; };
} }
increment() { increment() {
this.setState('count', this.state.count+1); this.setState('count', this.state.count+1);
} }
@ -228,7 +225,7 @@ class {
this.state = { this.state = {
count: input.count || 0 count: input.count || 0
}; };
} }
increment() { increment() {
this.state.count++; this.state.count++;
} }
@ -249,7 +246,7 @@ In addition, the default state can now be declared:
this.state = { this.state = {
count: input.count count: input.count
}; };
} }
increment() { increment() {
this.state.count++; this.state.count++;
@ -263,29 +260,29 @@ In Marko v3, UI components exported an API that included DOM insertion methods w
```js ```js
// Append to an existing DOM node: // Append to an existing DOM node:
require('./template.marko') require("./template.marko")
.renderSync({ name: 'Frank '}) .renderSync({ name: "Frank " })
.appendTo(document.body); .appendTo(document.body);
// Replace an existing DOM node: // Replace an existing DOM node:
require('./template.marko') require("./template.marko")
.renderSync({ name: 'Frank '}) .renderSync({ name: "Frank " })
.replace(document.getElementById('foo')); .replace(document.getElementById("foo"));
``` ```
### Less Boilerplate ### Less Boilerplate
- Removed: `w-bind` * Removed: `w-bind`
- Removed: `w-extend` * Removed: `w-extend`
- Removed: `require('marko-widgets').defineComponent(...)` * Removed: `require('marko-widgets').defineComponent(...)`
- Removed: `require('marko-widgets').defineWidget(...)` * Removed: `require('marko-widgets').defineWidget(...)`
- Removed: `require('marko-widgets').defineRenderer(...)` * Removed: `require('marko-widgets').defineRenderer(...)`
- Removed: `w-body` (use `<include()>` instead) * Removed: `w-body` (use `<include()>` instead)
- `w-on*="handleSomeEvent"` --> `on*('handleSomeEvent')` * `w-on*="handleSomeEvent"` --> `on*('handleSomeEvent')`
- `w-id` --> `key` * `w-id` --> `key`
- `w-for` --> `for-key` * `w-for` --> `for-key`
- `w-preserve` --> `no-update` * `w-preserve` --> `no-update`
- `class="foo" w-preserve-attrs="class"` --> `class:no-update="foo"` * `class="foo" w-preserve-attrs="class"` --> `class:no-update="foo"`
Some of these things are described in more detail later in this document. Some of these things are described in more detail later in this document.
@ -317,8 +314,7 @@ $ {
} }
``` ```
JavaScript blocks can be embedded anywhere by putting `$ ` at the start of the line (ignoring whitespace) and it works the same for both the concise syntax and the non-concise syntax: JavaScript blocks can be embedded anywhere by putting `$` at the start of the line (ignoring whitespace) and it works the same for both the concise syntax and the non-concise syntax:
```marko ```marko
<div.hello> <div.hello>
@ -384,9 +380,9 @@ static {
### Template variables ### Template variables
- `data` --> `input` - References the input object (should be treated as immutable) * `data` --> `input` - References the input object (should be treated as immutable)
- Introduced `state` - References the components raw state object (for components only) * Introduced `state` - References the components raw state object (for components only)
- Introduced `component` - References the component instance (for components only) * Introduced `component` - References the component instance (for components only)
## Other Improvements ## Other Improvements
@ -399,6 +395,7 @@ In Marko v4, only the attributes rendered by Marko are ever modified by Marko. A
### Allow multiple top-level DOM elements to be bound ([#393](https://github.com/marko-js/marko/issues/393)) ### Allow multiple top-level DOM elements to be bound ([#393](https://github.com/marko-js/marko/issues/393))
**Old:** **Old:**
```marko ```marko
<div w-bind> <div w-bind>
<h1>The current count is ${data.count}</h1> <h1>The current count is ${data.count}</h1>
@ -407,6 +404,7 @@ In Marko v4, only the attributes rendered by Marko are ever modified by Marko. A
``` ```
**New:** **New:**
```marko ```marko
<h1>The current count is ${input.count}</h1> <h1>The current count is ${input.count}</h1>
<button onClick('incrementCount')>Increment Count</button> <button onClick('incrementCount')>Increment Count</button>
@ -417,6 +415,7 @@ In Marko v4, only the attributes rendered by Marko are ever modified by Marko. A
**Old:** **Old:**
`index.js` `index.js`
```js ```js
module.exports = require('marko-widgets').defineComponent({ module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'), template: require('./template.marko'),
@ -434,7 +433,6 @@ module.exports = require('marko-widgets').defineComponent({
**New:** **New:**
`component.js` `component.js`
```js ```js
@ -456,6 +454,7 @@ module.exports = {
### Allow event handler attribute to bind additional arguments ([#401](https://github.com/marko-js/marko/issues/401)) ### Allow event handler attribute to bind additional arguments ([#401](https://github.com/marko-js/marko/issues/401))
**Old:** **Old:**
```marko ```marko
<ul for(color in colors)> <ul for(color in colors)>
<li w-onClick="handleColorClick" data-color=color>${color}</li> <li w-onClick="handleColorClick" data-color=color>${color}</li>
@ -469,6 +468,7 @@ handleColorClick(event, el) {
``` ```
**New:** **New:**
```marko ```marko
class { class {
handleColorClick(color, event, el) { handleColorClick(color, event, el) {
@ -497,6 +497,7 @@ Marko v4 introduces ES6 style imports for importing other JavaScript modules:
``` ```
**New:** **New:**
```marko ```marko
import helpers from "./helpers" import helpers from "./helpers"
<div>Total: ${helpers.formatCurrency(data.total)}</div> <div>Total: ${helpers.formatCurrency(data.total)}</div>
@ -509,19 +510,22 @@ import { formatCurrency } from "./helpers"
<div>Total: ${formatCurrency(data.total)}</div> <div>Total: ${formatCurrency(data.total)}</div>
``` ```
### Allow dynamic custom tags/components to be used with `<include>` ([#139](https://github.com/marko-js/marko/issues/139)) ### Allow dynamic custom tags/components to be used with `<include>` ([#139](https://github.com/marko-js/marko/issues/139))
**Old:** **Old:**
```marko ```marko
<invoke data.myComponent.renderer({name: 'Frank'}, out)/> <invoke data.myComponent.renderer({name: 'Frank'}, out)/>
``` ```
**New:** **New:**
```marko ```marko
<include(input.myComponent) name='Frank' /> <include(input.myComponent) name='Frank' />
``` ```
or or
```marko ```marko
<include(input.myComponent, {name: 'Frank'}) /> <include(input.myComponent, {name: 'Frank'}) />
``` ```
@ -597,7 +601,6 @@ components/hello/
}, },
... ...
} }
``` ```
`template.marko` `template.marko`
@ -638,11 +641,13 @@ $ var age = calculateAge(state.birthday);
### Make output of render `Promise`-compatible ([#251](https://github.com/marko-js/marko/issues/251)) ### Make output of render `Promise`-compatible ([#251](https://github.com/marko-js/marko/issues/251))
**Old:** **Old:**
```js ```js
template.render({}, function(err, html, out) {}); template.render({}, function(err, html, out) {});
``` ```
**New:** **New:**
```js ```js
template.render({}) template.render({})
.then(function(result){}) .then(function(result){})
@ -658,6 +663,7 @@ NOTE: callback/events still work as well
### Make `<await-reorderer/>` optional ([#410](https://github.com/marko-js/marko/issues/410)) ### Make `<await-reorderer/>` optional ([#410](https://github.com/marko-js/marko/issues/410))
**Old:** **Old:**
```marko ```marko
<html> <html>
... ...
@ -669,6 +675,7 @@ NOTE: callback/events still work as well
``` ```
**New:** **New:**
```marko ```marko
<html> <html>
... ...
@ -677,21 +684,24 @@ NOTE: callback/events still work as well
</body> </body>
</html> </html>
``` ```
*Automatically inserted before `</body>`*
_Automatically inserted before `</body>`_
### Allow multiple extensions when installing the Node.js require hook ([#407](https://github.com/marko-js/marko/issues/407)) ### Allow multiple extensions when installing the Node.js require hook ([#407](https://github.com/marko-js/marko/issues/407))
**Old:** **Old:**
```js ```js
require('marko/node-require').install({ require("marko/node-require").install({
extension: '.marko' extension: ".marko"
}); });
``` ```
**New:** **New:**
```js ```js
require('marko/node-require').install({ require("marko/node-require").install({
extensions: ['.marko', '.marko.xml', '.html'] extensions: [".marko", ".marko.xml", ".html"]
}); });
``` ```
@ -702,12 +712,14 @@ Hot reload any extensions that were registered via `require('marko/node-require'
### Allow spaces around attributes ([#403](https://github.com/marko-js/marko/issues/403)) ### Allow spaces around attributes ([#403](https://github.com/marko-js/marko/issues/403))
**Old:** **Old:**
```marko ```marko
var className="foo" var className="foo"
<div class=className/> <div class=className/>
``` ```
**New:** **New:**
```marko ```marko
$ var className = "foo" $ var className = "foo"
<div class = className/> <div class = className/>
@ -718,30 +730,34 @@ $ var className = "foo"
### Allow compile-time transformers to be registered at the template level ([#408](https://github.com/marko-js/marko/issues/408)) ### Allow compile-time transformers to be registered at the template level ([#408](https://github.com/marko-js/marko/issues/408))
`marko.json` `marko.json`
```json ```json
{ {
"transformer": "./my-transformer.js" "transformer": "./my-transformer.js"
} }
``` ```
`my-transformer.js` `my-transformer.js`
```js ```js
module.exports = function transform(rootNode, context) { module.exports = function transform(rootNode, context) {
// ... // ...
}; };
``` ```
[see commit](https://github.com/marko-js/marko/commit/a35e6bdbc3fe6e7f4e92fb377c435e29ab3d6e33)
[see commit](https://github.com/marko-js/marko/commit/a35e6bdbc3fe6e7f4e92fb377c435e29ab3d6e33)
### Allow regular expression for an HTML attribute value ([#386](https://github.com/marko-js/marko/issues/386)) ### Allow regular expression for an HTML attribute value ([#386](https://github.com/marko-js/marko/issues/386))
**Old:** **Old:**
```marko ```marko
<!-- escaped backslash (\) since strings are parsed as JS values --> <!-- escaped backslash (\) since strings are parsed as JS values -->
<input type="text" pattern="\\w{2,20}" /> <input type="text" pattern="\\w{2,20}" />
``` ```
**New:** **New:**
```marko ```marko
<!-- just use a regex --> <!-- just use a regex -->
<input type="text" pattern=/\w{2,20}/ /> <input type="text" pattern=/\w{2,20}/ />
@ -749,13 +765,14 @@ module.exports = function transform(rootNode, context) {
## Deprecations ## Deprecations
A huge effort is being made to make this release as painless as possible and keep backwards compatibility wherever possible. It should be possible to continue to use custom tags that were developed against v3 with the v4 release as long as there are no dependencies on features deprecated in Marko v3 that have now been removed in Marko v4 (see [Breaking Changes](#breaking-changes) below). A huge effort is being made to make this release as painless as possible and keep backwards compatibility wherever possible. It should be possible to continue to use custom tags that were developed against v3 with the v4 release as long as there are no dependencies on features deprecated in Marko v3 that have now been removed in Marko v4 (see [Breaking Changes](#breaking-changes) below).
Additionally, [`marko-migrate`](https://github.com/marko-js/marko-migrate) will be updated to handle many of the deprecations described below. Additionally, [`marko-migrate`](https://github.com/marko-js/marko-migrate) will be updated to handle many of the deprecations described below.
### Deprecate `<script marko-init>` and replace with `static` section ([#397](https://github.com/marko-js/marko/issues/397)) ### Deprecate `<script marko-init>` and replace with `static` section ([#397](https://github.com/marko-js/marko/issues/397))
**Old:** **Old:**
```marko ```marko
<script marko-init> <script marko-init>
var format = require('format'); var format = require('format');
@ -765,6 +782,7 @@ Additionally, [`marko-migrate`](https://github.com/marko-js/marko-migrate) will
``` ```
**New:** **New:**
```marko ```marko
static var format=require('format') static var format=require('format')
$ var name='World' $ var name='World'
@ -778,6 +796,7 @@ Use embedded JavaScript blocks instead
### Deprecate `w-bind` ([#394](https://github.com/marko-js/marko/issues/394), [#395](https://github.com/marko-js/marko/issues/395)) ### Deprecate `w-bind` ([#394](https://github.com/marko-js/marko/issues/394), [#395](https://github.com/marko-js/marko/issues/395))
**Old:** **Old:**
```marko ```marko
<div w-bind> <div w-bind>
... ...
@ -797,6 +816,7 @@ Use embedded JavaScript blocks instead
### Deprecate `widget-types` ([#514](https://github.com/marko-js/marko/issues/514)) ### Deprecate `widget-types` ([#514](https://github.com/marko-js/marko/issues/514))
**Old:** **Old:**
```marko ```marko
<widget-types default="./component" mobile="./component-mobile"/> <widget-types default="./component" mobile="./component-mobile"/>
@ -818,6 +838,7 @@ The `w-id` attribute was used to obtain references using `this.getEl(refId)`. `w
``` ```
**New:** **New:**
```marko ```marko
<input type="text" key="nameInput" /> <input type="text" key="nameInput" />
``` ```
@ -832,6 +853,7 @@ Similarly, `w-for` has been been replaced with `for-key`:
``` ```
**New:** **New:**
```marko ```marko
<label for-key="nameInput">Name</label> <label for-key="nameInput">Name</label>
<input type="text" key="nameInput" /> <input type="text" key="nameInput" />
@ -842,19 +864,25 @@ Similarly, `w-for` has been been replaced with `for-key`:
### Deprecate `w-on*` in favor of `on*()` ([#420](https://github.com/marko-js/marko/issues/420)) ### Deprecate `w-on*` in favor of `on*()` ([#420](https://github.com/marko-js/marko/issues/420))
**Old:** **Old:**
```marko ```marko
<button w-on-click="handleClick">click me</button> <button w-on-click="handleClick">click me</button>
``` ```
or or
```marko ```marko
<button w-onClick="handleClick">click me</button> <button w-onClick="handleClick">click me</button>
``` ```
**New:** **New:**
```marko ```marko
<button on-click('handleClick')>click me</button> <button on-click('handleClick')>click me</button>
``` ```
or or
```marko ```marko
<button onClick('handleClick')>click me</button> <button onClick('handleClick')>click me</button>
``` ```
@ -862,6 +890,7 @@ or
### Deprecate `<init-widgets/>` ([#409](https://github.com/marko-js/marko/issues/409)) ### Deprecate `<init-widgets/>` ([#409](https://github.com/marko-js/marko/issues/409))
**Old:** **Old:**
```marko ```marko
<html> <html>
... ...
@ -923,6 +952,7 @@ Or, with an argument value:
### Deprecate `w-preserve*` and replace with `no-update*` ([#419](https://github.com/marko-js/marko/issues/419)) ### Deprecate `w-preserve*` and replace with `no-update*` ([#419](https://github.com/marko-js/marko/issues/419))
**Old:** **Old:**
```marko ```marko
<div w-preserve> <div w-preserve>
... ...
@ -930,6 +960,7 @@ Or, with an argument value:
``` ```
**New:** **New:**
```marko ```marko
<div no-update> <div no-update>
... ...
@ -939,6 +970,7 @@ Or, with an argument value:
### Deprecate `w-preserve-attrs` and replace with `:no-update` ([#422](https://github.com/marko-js/marko/issues/422)) ### Deprecate `w-preserve-attrs` and replace with `:no-update` ([#422](https://github.com/marko-js/marko/issues/422))
**Old:** **Old:**
```marko ```marko
<div style="color:#09c" w-preserve-attrs="style"> <div style="color:#09c" w-preserve-attrs="style">
... ...
@ -946,6 +978,7 @@ Or, with an argument value:
``` ```
**New:** **New:**
```marko ```marko
<div style:no-update="color:#09c"> <div style:no-update="color:#09c">
... ...
@ -957,17 +990,21 @@ Or, with an argument value:
> `w-extend` is now deprecated > `w-extend` is now deprecated
**Old:** **Old:**
```marko ```marko
<div w-bind> <div w-bind>
<some-component w-onEvent="handleEvent"/> <some-component w-onEvent="handleEvent"/>
</div> </div>
``` ```
or or
```marko ```marko
<some-component w-extend w-onEvent="handleEvent"/> <some-component w-extend w-onEvent="handleEvent"/>
``` ```
**New:** **New:**
```marko ```marko
<some-component onEvent('handleEvent')/> <some-component onEvent('handleEvent')/>
``` ```
@ -983,9 +1020,10 @@ In order to move forward it was necessary to introduce a few (minor) breaking ch
### Consistent rendering API ([#389](https://github.com/marko-js/marko/issues/389)) ### Consistent rendering API ([#389](https://github.com/marko-js/marko/issues/389))
**Old:** **Old:**
```js ```js
var template = require('./template.marko'); var template = require("./template.marko");
var component = require('./my-component'); var component = require("./my-component");
var data = {}; var data = {};
template.render(data); // returns `out` template.render(data); // returns `out`
@ -998,9 +1036,10 @@ component.renderSync(data); // throws an error, not a method.
``` ```
**New:** **New:**
```js ```js
var template = require('./template.marko'); var template = require("./template.marko");
var component = require('./my-component'); var component = require("./my-component");
var data = {}; var data = {};
template.render(data); // returns `out` template.render(data); // returns `out`
@ -1038,11 +1077,13 @@ Given a template like this:
`include-target.marko` looks like: `include-target.marko` looks like:
**Old:** **Old:**
```marko ```marko
-- Hello ${data['first-name']} -- Hello ${data['first-name']}
``` ```
**New:** **New:**
```marko ```marko
-- Hello ${input.firstName} -- Hello ${input.firstName}
``` ```
@ -1052,6 +1093,7 @@ Given a template like this:
> Already deprecated in v3 > Already deprecated in v3
**Old:** **Old:**
```marko ```marko
<async-fragment var="foo" data-provider=data.provider> <async-fragment var="foo" data-provider=data.provider>
${foo} ${foo}
@ -1059,6 +1101,7 @@ Given a template like this:
``` ```
**New:** **New:**
```marko ```marko
<await(foo from data.provider)> <await(foo from data.provider)>
${foo} ${foo}
@ -1070,7 +1113,7 @@ Given a template like this:
> Already deprecated in v3 > Already deprecated in v3
| Old | New | | Old | New |
|------------------------------|-----------------------| | ---------------------------- | --------------------- |
| `asyncFragmentFinish` | `await:finish` | | `asyncFragmentFinish` | `await:finish` |
| `asyncFragmentBegin` | `await:begin` | | `asyncFragmentBegin` | `await:begin` |
| `asyncFragmentBeforeRender` | `await:beforeRender` | | `asyncFragmentBeforeRender` | `await:beforeRender` |

View File

@ -42,10 +42,10 @@ class Counter extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { count: 0 } this.state = { count: 0 };
function doIncrement(delta) { function doIncrement(delta) {
this.setState((prevState) => ({ this.setState(prevState => ({
count: prevState.count + delta count: prevState.count + delta
})); }));
} }
@ -55,25 +55,19 @@ class Counter extends React.Component {
} }
render() { render() {
var count = this.state.count; var count = this.state.count;
var countClassName = 'count'; var countClassName = "count";
if (count > 0) { if (count > 0) {
countClassName += ' positive'; countClassName += " positive";
} else if (count < 0) { } else if (count < 0) {
countClassName += ' negative'; countClassName += " negative";
} }
return ( return (
<div className="click-count"> <div className="click-count">
<div className={countClassName}> <div className={countClassName}>{count}</div>
{count} <button onClick={this.decrement}>-1</button>
</div> <button onClick={this.increment}>+1</button>
<button onClick={this.decrement}>
-1
</button>
<button onClick={this.increment}>
+1
</button>
</div> </div>
); );
} }
@ -145,113 +139,113 @@ At a high level here are some differences:
#### Differences in rendering #### Differences in rendering
* **Improved performance:** Marko renders to a virtual DOM in the browser and * **Improved performance:** Marko renders to a virtual DOM in the browser and
directly to an HTML stream on the server (Marko supports multiple compilation directly to an HTML stream on the server (Marko supports multiple compilation
targets). targets).
* **Improved performance:** Marko supports asynchronous rendering with [early * **Improved performance:** Marko supports asynchronous rendering with [early
flushing of flushing of
HTML](http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/) HTML](http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/)
for improvements in actual and perceived page load times. for improvements in actual and perceived page load times.
* **Improved performance: **React requires an additional client-side re-render if * **Improved performance: **React requires an additional client-side re-render if
a page is initially rendered on the server while Marko does not. a page is initially rendered on the server while Marko does not.
* **Improved ease of use: **Marko automatically serializes UI component state and * **Improved ease of use: **Marko automatically serializes UI component state and
input down to the browser so that the browser can pick up right where the server input down to the browser so that the browser can pick up right where the server
left off. left off.
* **Improved ease of use: **Marko is suitable for rendering an entire HTML page on * **Improved ease of use: **Marko is suitable for rendering an entire HTML page on
the server with support for tags such as `<doctype>` and `<html>` the server with support for tags such as `<doctype>` and `<html>`
#### Differences in syntax #### Differences in syntax
* **Improved ease of use: **Marko uses the * **Improved ease of use: **Marko uses the
[HTML-JS](http://markojs.com/docs/syntax/) syntax and the [HTML-JS](http://markojs.com/docs/syntax/) syntax and the
[JSX](https://facebook.github.io/react/docs/jsx-in-depth.html) syntax is offered [JSX](https://facebook.github.io/react/docs/jsx-in-depth.html) syntax is offered
for React. for React.
* **Improved ease of use: **Marko supports both a concise syntax and a familiar * **Improved ease of use: **Marko supports both a concise syntax and a familiar
HTML syntax. HTML syntax.
* **Improved ease of use: **JSX requires strict XML while Marko aligns with less * **Improved ease of use: **JSX requires strict XML while Marko aligns with less
strict HTML that web developers are used to. strict HTML that web developers are used to.
* **Improved ease of use: **With Marko, *all* HTML attribute values are parsed as * **Improved ease of use: **With Marko, _all_ HTML attribute values are parsed as
JavaScript expressions. JavaScript expressions.
* **Improved ease of use: **Marko supports simple directives for conditionals, * **Improved ease of use: **Marko supports simple directives for conditionals,
looping, etc. looping, etc.
* **JSX limitation: **JSX is “just JavaScript” but requires expressions that * **JSX limitation: **JSX is “just JavaScript” but requires expressions that
preclude the usage of JavaScript statements such as in certain places. preclude the usage of JavaScript statements such as in certain places.
#### Differences in compilation #### Differences in compilation
* **Improved performance: **Marko supports multiple compilation outputs (Marko * **Improved performance: **Marko supports multiple compilation outputs (Marko
VDOM and HTML streaming are currently supported). VDOM and HTML streaming are currently supported).
* **Improved ease of use: **Marko compiles UI components to JavaScript modules * **Improved ease of use: **Marko compiles UI components to JavaScript modules
that export a rendering API. that export a rendering API.
* **Expanded capabilities: **Marko supports a robust API for controlling how * **Expanded capabilities: **Marko supports a robust API for controlling how
custom tags and custom attributes get compiled and it supports compile-time custom tags and custom attributes get compiled and it supports compile-time
transforms based on a friendly Abstract Syntax Tree (AST). transforms based on a friendly Abstract Syntax Tree (AST).
* **Improved performance: **JSX is just syntactic sugar that translates elements * **Improved performance: **JSX is just syntactic sugar that translates elements
to `createElement()` function calls while the Marko compiler has full control over how things are to `createElement()` function calls while the Marko compiler has full control over how things are
compiled and optimized. compiled and optimized.
* **Improved ease of use: **React requires all UI components to be explicitly * **Improved ease of use: **React requires all UI components to be explicitly
imported before they can be used as custom tags while Marko supports both imported before they can be used as custom tags while Marko supports both
explicit importing and implicit importing. explicit importing and implicit importing.
* **Improved performance: **Marko has a modular runtime and the compiler generates * **Improved performance: **Marko has a modular runtime and the compiler generates
code that only imports the parts of the Marko runtime that are needed for much code that only imports the parts of the Marko runtime that are needed for much
smaller builds. smaller builds.
* **Improved ease of use: **Marko supports optional compile-time checks to ensure * **Improved ease of use: **Marko supports optional compile-time checks to ensure
that only allowed attributes are passed to custom tags. (React `PropTypes` only provide that only allowed attributes are passed to custom tags. (React `PropTypes` only provide
validation at render-time) validation at render-time)
* **Improved ease of use: **Marko validates *all* tag names at compile-time. * **Improved ease of use: **Marko validates _all_ tag names at compile-time.
* **Improved ease of use: **Marko provides its own compiler that integrates with * **Improved ease of use: **Marko provides its own compiler that integrates with
Node.js and JavaScript module bundlers while React JSX requires babel and custom Node.js and JavaScript module bundlers while React JSX requires babel and custom
babel transforms. babel transforms.
#### Differences in UI components #### Differences in UI components
* **Reduced boilerplate: **No explicit extending of JavaScript classes in Marko * **Reduced boilerplate: **No explicit extending of JavaScript classes in Marko
(in contrast to `class Counter extends React.Component` in React). (in contrast to `class Counter extends React.Component` in React).
* **Improved ease of use: **Modifications to UI component state are synchronous * **Improved ease of use: **Modifications to UI component state are synchronous
with Marko while [the rules for React are more with Marko while [the rules for React are more
complicated](https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous). complicated](https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous).
* **Improved ease of use: **Marko watches UI component state objects to allow * **Improved ease of use: **Marko watches UI component state objects to allow
state to be modified directly (e.g., `this.state.count++`). state to be modified directly (e.g., `this.state.count++`).
* **Improved ease of use: **Marko supports single-file UI components combining * **Improved ease of use: **Marko supports single-file UI components combining
JavaScript behavior, CSS styling (with support for CSS preprocessors) and HTML JavaScript behavior, CSS styling (with support for CSS preprocessors) and HTML
markup. (React requires using one of the many [CSS in JS markup. (React requires using one of the many [CSS in JS
solutions](https://github.com/MicheleBertoli/css-in-js) if you want styles in solutions](https://github.com/MicheleBertoli/css-in-js) if you want styles in
the same file as your component and there is no standard in the community) the same file as your component and there is no standard in the community)
* **Improved maintainability: **Marko supports a seamless transition from a * **Improved maintainability: **Marko supports a seamless transition from a
single-file UI component to a multi-file UI component. single-file UI component to a multi-file UI component.
* **Improved performance:** Marko assumes UI components are pure by default and * **Improved performance:** Marko assumes UI components are pure by default and
skips re-rendering when input properties and state are unchanged (React requires skips re-rendering when input properties and state are unchanged (React requires
extending extending
[React.PureComponent](https://facebook.github.io/react/docs/react-api.html#react.purecomponent)). [React.PureComponent](https://facebook.github.io/react/docs/react-api.html#react.purecomponent)).
#### Differences in event systems #### Differences in event systems
* **Reduced complexity: **React utilizes [synthetic * **Reduced complexity: **React utilizes [synthetic
events](https://facebook.github.io/react/docs/events.html) while Marko utilizes events](https://facebook.github.io/react/docs/events.html) while Marko utilizes
real DOM events. real DOM events.
* **Improved ease of use: **Custom events are emitted using the [EventEmitter * **Improved ease of use: **Custom events are emitted using the [EventEmitter
API](https://nodejs.org/api/events.html) in Marko (e.g., `this.emit('myCustomEvent', arg1, arg2)`). API](https://nodejs.org/api/events.html) in Marko (e.g., `this.emit('myCustomEvent', arg1, arg2)`).
* **Improved ease of use: **Marko has a consistent approach for listening to both * **Improved ease of use: **Marko has a consistent approach for listening to both
native DOM events and custom events. native DOM events and custom events.
* **Improved ease of use: **React requires passing around `Function` references for custom * **Improved ease of use: **React requires passing around `Function` references for custom
events while Marko automatically delegates emitted custom events to event events while Marko automatically delegates emitted custom events to event
handler methods on components. handler methods on components.
* **Improved ease of use: **Marko provides a simple mechanism for binding * **Improved ease of use: **Marko provides a simple mechanism for binding
additional arguments to event handler methods and `this` will be the component additional arguments to event handler methods and `this` will be the component
instance. instance.
#### Differences in compatibility #### Differences in compatibility
* **Marko limitation: **Marko has no support for native mobile similar to React * **Marko limitation: **Marko has no support for native mobile similar to React
Native (although with Marko VDOM rendering, this is possible). Native (although with Marko VDOM rendering, this is possible).
* **Marko limitation: **Marko requires a JavaScript module bundler (such as * **Marko limitation: **Marko requires a JavaScript module bundler (such as
[Lasso](http://markojs.com/docs/lasso/), [Lasso](http://markojs.com/docs/lasso/),
[Webpack](http://markojs.com/docs/webpack/), [Webpack](http://markojs.com/docs/webpack/),
[Rollup](http://markojs.com/docs/rollup/) or [Rollup](http://markojs.com/docs/rollup/) or
[Browserify](http://markojs.com/docs/browserify/)) to be used in the browser [Browserify](http://markojs.com/docs/browserify/)) to be used in the browser
since Marko UI components compile down to JavaScript modules. (we consider using since Marko UI components compile down to JavaScript modules. (we consider using
a JavaScript module bundler a best practice) a JavaScript module bundler a best practice)
***** ---
In the sections below we will take a closer look at some of the differences In the sections below we will take a closer look at some of the differences
between Marko and React. between Marko and React.
@ -287,7 +281,7 @@ In React JSX, all attribute values are parsed as string values unless `{}` is us
#### Marko #### Marko
With Marko, *all* attribute values are parsed as JavaScript expressions. The With Marko, _all_ attribute values are parsed as JavaScript expressions. The
following Marko code is equivalent to the React JSX code above: following Marko code is equivalent to the React JSX code above:
```marko ```marko
@ -309,10 +303,10 @@ React JSX starts with JavaScript and allows XML elements to be inlined as shown
below: below:
```jsx ```jsx
import { formatDate } from './util'; import { formatDate } from "./util";
function formatName(person) { function formatName(person) {
return person.firstName + ' ' + person.lastName.charAt(0) + '.'; return person.firstName + " " + person.lastName.charAt(0) + ".";
} }
export default function HelloMessage(props) { export default function HelloMessage(props) {
@ -320,10 +314,8 @@ export default function HelloMessage(props) {
return ( return (
<div> <div>
Hello {formatName(person)}! Hello {formatName(person)}!
<span> <span>You were born on {formatDate(person.birthday)}.</span>
You were born on {formatDate(person.birthday)}.
</span>
</div> </div>
); );
} }
@ -356,7 +348,7 @@ $ var person = input.person;
Lines prefixed with `$` are directly added to the compiled JavaScript output inside Lines prefixed with `$` are directly added to the compiled JavaScript output inside
the compiled `render()` function (for JavaScript code that should run for every render). the compiled `render()` function (for JavaScript code that should run for every render).
Lines prefixed with `static` are directly added to the compiled JavaScript output Lines prefixed with `static` are directly added to the compiled JavaScript output
outside the `render()` function (for code that should only run *once* when the template is outside the `render()` function (for code that should only run _once_ when the template is
loaded). loaded).
### Syntax: HTML support ### Syntax: HTML support
@ -370,7 +362,7 @@ documentation](https://facebook.github.io/react/docs/introducing-jsx.html#specif
> Since JSX is closer to JavaScript than HTML, React DOM uses `camelCase` property naming > Since JSX is closer to JavaScript than HTML, React DOM uses `camelCase` property naming
> convention instead of HTML attribute names. > convention instead of HTML attribute names.
> For example, `class` becomes `className` in JSX, and `tabindex` becomes `tabIndex`. > For example, `class` becomes `className` in JSX, and `tabindex` becomes `tabIndex`.
As a result of this caveat for React, [tools for converting HTML to JSX As a result of this caveat for React, [tools for converting HTML to JSX
exist](http://magic.reactjs.net/htmltojsx.htm). exist](http://magic.reactjs.net/htmltojsx.htm).
@ -454,16 +446,19 @@ template:
```jsx ```jsx
function renderColors(colors) { function renderColors(colors) {
return ( return (
<ul> <ul>
{colors.map((color) => ( {colors.map(color => (
<li className="color" style={{ <li
backgroundColor: color className="color"
}}> style={{
{color} backgroundColor: color
</li> }}
))} >
</ul> {color}
); </li>
))}
</ul>
);
} }
``` ```
@ -619,8 +614,8 @@ Marko compiles component to JavaScript modules that export an API for rendering
the component as shown below: the component as shown below:
```js ```js
require('./components/greeting') require("./components/greeting")
.renderSync({ name: 'Frank' }) .renderSync({ name: "Frank" })
.appendTo(document.body); .appendTo(document.body);
``` ```
@ -628,8 +623,7 @@ The same UI component can be rendered to a stream such as a writable HTTP
response stream: response stream:
```js ```js
require('./components/hello') require("./components/hello").render({ name: "John" }, res);
.render({ name: 'John' }, res);
``` ```
> The users of a Marko UI component do not need to know that the component was > The users of a Marko UI component do not need to know that the component was
@ -638,12 +632,11 @@ require('./components/hello')
Contrast this with React as an example: Contrast this with React as an example:
```jsx ```jsx
import ReactDOM from "react-dom";
import ReactDOM from 'react-dom';
ReactDOM.render( ReactDOM.render(
<HelloMessage name="John" />, <HelloMessage name="John" />,
document.getElementById('container') document.getElementById("container")
); );
``` ```
@ -651,10 +644,9 @@ On top of that, React requires that a different module be imported to render the
exact same UI component on the server: exact same UI component on the server:
```jsx ```jsx
import ReactDOMServer from 'react-dom/server'; import ReactDOMServer from "react-dom/server";
var html = ReactDOMServer.renderToString( var html = ReactDOMServer.renderToString(<HelloMessage name="John" />);
<HelloMessage name="John" />);
``` ```
### Custom tags ### Custom tags
@ -664,8 +656,8 @@ var html = ReactDOMServer.renderToString(
With React, all custom tags for UI components must be explicitly imported: With React, all custom tags for UI components must be explicitly imported:
```jsx ```jsx
import Hello from './components/Hello'; import Hello from "./components/Hello";
import GoodBye from './components/GoodBye'; import GoodBye from "./components/GoodBye";
export default function HelloGoodBye(props) { export default function HelloGoodBye(props) {
return ( return (
@ -682,7 +674,7 @@ export default function HelloGoodBye(props) {
Marko supports a mechanism for [automatically discovering custom Marko supports a mechanism for [automatically discovering custom
tags](http://markojs.com/docs/custom-tags/#discovering-tags) for UI components tags](http://markojs.com/docs/custom-tags/#discovering-tags) for UI components
based on the project directory structure. Marko walks up the directory tree to based on the project directory structure. Marko walks up the directory tree to
discover all directories and it will also automatically discover custom tags discover all directories and it will also automatically discover custom tags
exported by installed packages. This approach negates the need for explicitly exported by installed packages. This approach negates the need for explicitly
importing a custom tag to reduce the amount of code needed in a Marko template. importing a custom tag to reduce the amount of code needed in a Marko template.
For example given the following directory structure: For example given the following directory structure:
@ -695,7 +687,7 @@ For example given the following directory structure:
└── index.marko └── index.marko
``` ```
The `<hello>` tag and the `<good-bye>` tag nested below the `components/` The `<hello>` tag and the `<good-bye>` tag nested below the `components/`
directory will automatically be made available to the `index.marko` at the root: directory will automatically be made available to the `index.marko` at the root:
```marko ```marko
@ -751,6 +743,7 @@ function render(input, out) {
``` ```
#### Compiled for the browser: #### Compiled for the browser:
```marko ```marko
var marko_template = require("marko/vdom").t(__filename); var marko_template = require("marko/vdom").t(__filename);
@ -769,12 +762,13 @@ tags and it also provides support for compile-time transforms. While Babel
allows code transformations of JavaScript, the Marko compiler provides support allows code transformations of JavaScript, the Marko compiler provides support
for resolving custom tags declaratively and the Marko AST provides for very for resolving custom tags declaratively and the Marko AST provides for very
powerful and simple transformations as shown in the following code for rendering powerful and simple transformations as shown in the following code for rendering
Markdown to HTML at *compile-time*: Markdown to HTML at _compile-time_:
**components/markdown/code-generator.js:** **components/markdown/code-generator.js:**
```js ```js
import marked from 'marked'; import marked from "marked";
import {removeIndentation} from './util'; import { removeIndentation } from "./util";
export default function generateCode(el, codegen) { export default function generateCode(el, codegen) {
var bodyText = removeIndentation(el.bodyText); var bodyText = removeIndentation(el.bodyText);
@ -827,7 +821,7 @@ quickly jump to referenced files and methods, and [Pretty
printing](https://github.com/marko-js/atom-language-marko#prettyprint) to keep printing](https://github.com/marko-js/atom-language-marko#prettyprint) to keep
your code readable. your code readable.
***** ---
### Why Marko? ### Why Marko?
@ -837,20 +831,20 @@ Here are just a few reasons you should consider using
* Marko requires much less boilerplate. * Marko requires much less boilerplate.
* Marko has much better performance based on our benchmarks. * Marko has much better performance based on our benchmarks.
* Marko offers a clean and powerful syntax that aligns with HTML while also * Marko offers a clean and powerful syntax that aligns with HTML while also
allowing the full power of JavaScript. allowing the full power of JavaScript.
* Marko has much less complexity and a very small runtime. * Marko has much less complexity and a very small runtime.
* Marko has a much lower page weight for faster page loads. * Marko has a much lower page weight for faster page loads.
* Marko has strong integrations with Node.js. * Marko has strong integrations with Node.js.
* Marko allows for extremely powerful IDE and editor plugins (see the [Marko * Marko allows for extremely powerful IDE and editor plugins (see the [Marko
plugin for Atom](https://github.com/marko-js/atom-language-marko) as an plugin for Atom](https://github.com/marko-js/atom-language-marko) as an
example). example).
* Marko has a powerful compiler that allows new features to be added without * Marko has a powerful compiler that allows new features to be added without
introducing bloat. introducing bloat.
* eBay relies heavily on Marko and it is being used to build ebay.com (including * eBay relies heavily on Marko and it is being used to build ebay.com (including
the mobile web). the mobile web).
* Marko has a strong and growing community on * Marko has a strong and growing community on
[GitHub](https://github.com/marko-js/marko) and in [GitHub](https://github.com/marko-js/marko) and in
[Gitter](https://gitter.im/marko-js/marko). [Gitter](https://gitter.im/marko-js/marko).
Interested in learning more about Marko? If so, you can get additional Interested in learning more about Marko? If so, you can get additional
information on the [Marko website](http://markojs.com/). Join the conversation information on the [Marko website](http://markojs.com/). Join the conversation

View File

@ -12,11 +12,12 @@ npm install marko --save
## Usage ## Usage
The partial code snippet below shows how a Marko UI component can be connected The partial code snippet below shows how a Marko UI component can be connected
to a Redux store using the `store.subscribe()` method and the Marko `forceUpdate()` to a Redux store using the `store.subscribe()` method and the Marko `forceUpdate()`
method: method:
**counter.marko** **counter.marko**
```javascript ```javascript
import store from './store'; import store from './store';
@ -41,25 +42,27 @@ class {
``` ```
**reducer.js** **reducer.js**
```js ```js
module.exports = function (state, action) { module.exports = function(state, action) {
state = state || { state = state || {
value: 0 value: 0
}; };
// Additional reducer logic here... // Additional reducer logic here...
return state; return state;
}; };
``` ```
In `counter.marko`, the imported store module exports a Redux store created In `counter.marko`, the imported store module exports a Redux store created
using the following code: using the following code:
**store.js** **store.js**
```javascript ```javascript
var redux = require('redux'); var redux = require("redux");
var counter = require('./reducer'); var counter = require("./reducer");
module.exports = redux.createStore(counter); module.exports = redux.createStore(counter);
``` ```

View File

@ -2,11 +2,11 @@
The files in this directory will be automatically generated by jsdoc. The files in this directory will be automatically generated by jsdoc.
- AST Nodes * AST Nodes
- Runtime API * Runtime API
- Compiler API * Compiler API
- CompileContext API * CompileContext API
- Builder API * Builder API
- CodeGenerator API * CodeGenerator API
- AsyncWriter API * AsyncWriter API
- AsyncVDOMBuilder API * AsyncVDOMBuilder API

View File

@ -3,18 +3,20 @@
To render a Marko view, you need to `require` it. To render a Marko view, you need to `require` it.
_example.js_ _example.js_
```js ```js
var fancyButton = require('./components/fancy-button'); var fancyButton = require("./components/fancy-button");
``` ```
> **Note:** If you are targeting node.js, you will need to enable the [require extension](./installing.md#require-marko-views) in order to require `.marko` files or you will need to precompile all of your templates using [Marko DevTools](https://github.com/marko-js/marko-devtools). If you are targeting the browser, you will need to use a bundler like [`lasso`](./lasso.md), [`webpack`](./webpack.md), [`browserify`](./browserify.md) or [`rollup`](./rollup.md). > **Note:** If you are targeting node.js, you will need to enable the [require extension](./installing.md#require-marko-views) in order to require `.marko` files or you will need to precompile all of your templates using [Marko DevTools](https://github.com/marko-js/marko-devtools). If you are targeting the browser, you will need to use a bundler like [`lasso`](./lasso.md), [`webpack`](./webpack.md), [`browserify`](./browserify.md) or [`rollup`](./rollup.md).
Once you have a view, you can pass input data and render it: Once you have a view, you can pass input data and render it:
_example.js_ _example.js_
```js ```js
var button = require('./components/fancy-button'); var button = require("./components/fancy-button");
var html = button.renderToString({ label:'Click me!' }); var html = button.renderToString({ label: "Click me!" });
console.log(html); console.log(html);
``` ```
@ -22,6 +24,7 @@ console.log(html);
The input data becomes available as `input` within a view, so if `fancy-button.marko` looked like this: The input data becomes available as `input` within a view, so if `fancy-button.marko` looked like this:
_./components/fancy-button.marko_ _./components/fancy-button.marko_
```marko ```marko
<button>${input.label}</button> <button>${input.label}</button>
``` ```
@ -34,21 +37,21 @@ The output HTML would be:
## Rendering methods ## Rendering methods
We used the `renderToString` method above to render the view, but there are a number of different method signatures that can be used to render. We used the `renderToString` method above to render the view, but there are a number of different method signatures that can be used to render.
Many of these methods return a [`RenderResult`](#renderresult) which is an object with helper methods for working with the rendered output. Many of these methods return a [`RenderResult`](#renderresult) which is an object with helper methods for working with the rendered output.
### `renderSync(input)` ### `renderSync(input)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | ------------------------------- | -------------------------------------- |
| `input` | `Object` | the input data used to render the view | | `input` | `Object` | the input data used to render the view |
| return value | [`RenderResult`](#renderresult) | The result of the render | | return value | [`RenderResult`](#renderresult) | The result of the render |
Using `renderSync` forces the render to complete synchronously. If a tag attempts to run asynchronously, an error will be thrown. Using `renderSync` forces the render to complete synchronously. If a tag attempts to run asynchronously, an error will be thrown.
```js ```js
var view = require('./view'); // Import `./view.marko` var view = require("./view"); // Import `./view.marko`
var result = view.renderSync({}); var result = view.renderSync({});
result.appendTo(document.body); result.appendTo(document.body);
@ -56,94 +59,93 @@ result.appendTo(document.body);
### `render(input)` ### `render(input)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | -------------------------------- | -------------------------------------- |
| `input` | `Object` | the input data used to render the view | | `input` | `Object` | the input data used to render the view |
| return value | `AsyncStream`/`AsyncVDOMBuilder` | the async `out` render target | | return value | `AsyncStream`/`AsyncVDOMBuilder` | the async `out` render target |
The `render` method returns an async `out` which is used to generate HTML on the server or a virtual DOM in the browser. In either case, the async `out` has a `then` method that follows the Promises/A+ spec, so it can be used as if it were a Promise. This promise resolves to a [`RenderResult`](#renderresult). The `render` method returns an async `out` which is used to generate HTML on the server or a virtual DOM in the browser. In either case, the async `out` has a `then` method that follows the Promises/A+ spec, so it can be used as if it were a Promise. This promise resolves to a [`RenderResult`](#renderresult).
```js ```js
var view = require('./view'); // Import `./view.marko` var view = require("./view"); // Import `./view.marko`
var resultPromise = view.render({}); var resultPromise = view.render({});
resultPromise.then((result) => { resultPromise.then(result => {
result.appendTo(document.body); result.appendTo(document.body);
}); });
``` ```
### `render(input, callback)` ### `render(input, callback)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | -------------- | -------------------------------- | ---------------------------------------------- |
| `input` | `Object` | the input data used to render the view | | `input` | `Object` | the input data used to render the view |
| `callback` | `Function` | a function to call when the render is complete | | `callback` | `Function` | a function to call when the render is complete |
| callback value | [`RenderResult`](#renderresult) | The result of the render | | callback value | [`RenderResult`](#renderresult) | The result of the render |
| return value | `AsyncStream`/`AsyncVDOMBuilder` | the async `out` render target | | return value | `AsyncStream`/`AsyncVDOMBuilder` | the async `out` render target |
```js ```js
var view = require('./view'); // Import `./view.marko` var view = require("./view"); // Import `./view.marko`
view.render({}, (err, result) => { view.render({}, (err, result) => {
result.appendTo(document.body); result.appendTo(document.body);
}); });
``` ```
### `render(input, stream)` ### `render(input, stream)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | -------------------------------- | -------------------------------------- |
| `input` | `Object` | the input data used to render the view | | `input` | `Object` | the input data used to render the view |
| `stream` | `WritableStream` | a writeable stream | | `stream` | `WritableStream` | a writeable stream |
| return value | `AsyncStream`/`AsyncVDOMBuilder` | the async `out` render target | | return value | `AsyncStream`/`AsyncVDOMBuilder` | the async `out` render target |
The HTML output is written to the passed `stream`. The HTML output is written to the passed `stream`.
```js ```js
var http = require('http'); var http = require("http");
var view = require('./view'); // Import `./view.marko` var view = require("./view"); // Import `./view.marko`
http.createServer((req, res) => { http.createServer((req, res) => {
res.setHeader('content-type', 'text/html'); res.setHeader("content-type", "text/html");
view.render({}, res); view.render({}, res);
}); });
``` ```
### `render(input, out)` ### `render(input, out)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | -------------------------------- | -------------------------------------- |
| `input` | `Object` | the input data used to render the view | | `input` | `Object` | the input data used to render the view |
| `out` | `AsyncStream`/`AsyncVDOMBuilder` | The async `out` to render to | | `out` | `AsyncStream`/`AsyncVDOMBuilder` | The async `out` to render to |
| return value | `AsyncStream`/`AsyncVDOMBuilder` | The `out` that was passed | | return value | `AsyncStream`/`AsyncVDOMBuilder` | The `out` that was passed |
The `render` method also allows passing an existing async `out`. If you do this, `render` will not automatically end the async `out` (this allows rendering a view in the middle of another view). If the async `out` won't be ended by other means, you are responsible for ending it. The `render` method also allows passing an existing async `out`. If you do this, `render` will not automatically end the async `out` (this allows rendering a view in the middle of another view). If the async `out` won't be ended by other means, you are responsible for ending it.
```js ```js
var view = require('./view'); // Import `./view.marko` var view = require("./view"); // Import `./view.marko`
var out = view.createOut(); var out = view.createOut();
view.render({}, out); view.render({}, out);
out.on('finish', () => { out.on("finish", () => {
console.log(out.getOutput()); console.log(out.getOutput());
}); });
out.end(); out.end();
``` ```
### `renderToString(input)` ### `renderToString(input)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | ------------ | -------- | -------------------------------------- |
| `input` | `Object` | the input data used to render the view | | `input` | `Object` | the input data used to render the view |
| return value | `String` | The HTML string produced by the render | | return value | `String` | The HTML string produced by the render |
Returns an HTML string and forces the render to complete synchronously. If a tag attempts to run asynchronously, an error will be thrown. Returns an HTML string and forces the render to complete synchronously. If a tag attempts to run asynchronously, an error will be thrown.
```js ```js
var view = require('./view'); // Import `./view.marko` var view = require("./view"); // Import `./view.marko`
var html = view.renderToString({}); var html = view.renderToString({});
document.body.innerHTML = html; document.body.innerHTML = html;
@ -151,30 +153,30 @@ document.body.innerHTML = html;
### `renderToString(input, callback)` ### `renderToString(input, callback)`
| params | type | description | | params | type | description |
| ------- | ---- | ----------- | | -------------- | ----------- | -------------------------------------- |
| `input` | `Object` | the input data used to render the view | | `input` | `Object` | the input data used to render the view |
| callback value | `String` | The HTML string produced by the render | | callback value | `String` | The HTML string produced by the render |
| return value | `undefined` | N/A | | return value | `undefined` | N/A |
An HTML string is passed to the callback. An HTML string is passed to the callback.
```js ```js
var view = require('./view'); // Import `./view.marko` var view = require("./view"); // Import `./view.marko`
view.renderToString({}, (err, html) => { view.renderToString({}, (err, html) => {
document.body.innerHTML = html; document.body.innerHTML = html;
}); });
``` ```
### `stream(input)` ### `stream(input)`
The `stream` method returns a node.js style stream of the output HTML. This method is available on the server, but is not available by default in the browser. If you need to use streams in the browser, you may `require('marko/stream')` as part of your client-side bundle. The `stream` method returns a node.js style stream of the output HTML. This method is available on the server, but is not available by default in the browser. If you need to use streams in the browser, you may `require('marko/stream')` as part of your client-side bundle.
```js ```js
var fs = require('fs'); var fs = require("fs");
var view = require('./view'); // Import `./view.marko` var view = require("./view"); // Import `./view.marko`
var writeStream = fs.createWriteStream('output.html'); var writeStream = fs.createWriteStream("output.html");
view.stream({}).pipe(writeStream); view.stream({}).pipe(writeStream);
``` ```
@ -182,25 +184,35 @@ view.stream({}).pipe(writeStream);
## RenderResult ## RenderResult
### `getComponent()` ### `getComponent()`
### `getComponents(selector)` ### `getComponents(selector)`
### `afterInsert(doc)` ### `afterInsert(doc)`
### `getNode(doc)` ### `getNode(doc)`
### `getOutput()` ### `getOutput()`
### `appendTo(targetEl)` ### `appendTo(targetEl)`
### `insertAfter(targetEl)` ### `insertAfter(targetEl)`
### `insertBefore(targetEl)` ### `insertBefore(targetEl)`
### `prependTo(targetEl)` ### `prependTo(targetEl)`
### `replace(targetEl)` ### `replace(targetEl)`
### `replaceChildrenOf(targetEl)` ### `replaceChildrenOf(targetEl)`
## Global data ## Global data
If you need to make data available globally to all views that are rendered as the result of a call to one of the above render methods, you can pass the data as a `$global` property on the input data object. This data will be removed from `input` and merged into the `out.global` property. If you need to make data available globally to all views that are rendered as the result of a call to one of the above render methods, you can pass the data as a `$global` property on the input data object. This data will be removed from `input` and merged into the `out.global` property.
```js ```js
view.render({ view.render({
$global: { $global: {
flags: ['mobile'] flags: ["mobile"]
} }
}); });
``` ```

View File

@ -22,33 +22,33 @@ The following is the minimal recommend configuration to use Rollup with Marko:
_rollup.config.js_ _rollup.config.js_
```js ```js
import commonjsPlugin from 'rollup-plugin-commonjs'; import commonjsPlugin from "rollup-plugin-commonjs";
import browserifyPlugin from 'rollup-plugin-browserify-transform'; import browserifyPlugin from "rollup-plugin-browserify-transform";
import nodeResolvePlugin from 'rollup-plugin-node-resolve'; import nodeResolvePlugin from "rollup-plugin-node-resolve";
import markoify from 'markoify'; import markoify from "markoify";
import envify from 'envify'; import envify from "envify";
import path from 'path'; import path from "path";
export default { export default {
entry: path.join(__dirname, 'client.js'), entry: path.join(__dirname, "client.js"),
format: 'iife', format: "iife",
moduleName: 'app', moduleName: "app",
plugins: [ plugins: [
browserifyPlugin(markoify), browserifyPlugin(markoify),
browserifyPlugin(envify), browserifyPlugin(envify),
nodeResolvePlugin({ nodeResolvePlugin({
jsnext: true, // Default: false jsnext: true, // Default: false
main: true, // Default: true main: true, // Default: true
browser: true, // Default: false browser: true, // Default: false
preferBuiltins: false, preferBuiltins: false,
extensions: [ '.js', '.marko' ] extensions: [".js", ".marko"]
}), }),
commonjsPlugin({ commonjsPlugin({
include: [ 'node_modules/**', '**/*.marko', '**/*.js'], include: ["node_modules/**", "**/*.marko", "**/*.js"],
extensions: [ '.js', '.marko' ] extensions: [".js", ".marko"]
}) })
], ],
dest: path.join(__dirname, './dist/bundle.js') dest: path.join(__dirname, "./dist/bundle.js")
}; };
``` ```

View File

@ -3,30 +3,31 @@
Marko allows any Marko template/UI component to be rendered on the server or in the browser. A page can be rendered to a `Writable` stream such as an HTTP response stream as shown below: Marko allows any Marko template/UI component to be rendered on the server or in the browser. A page can be rendered to a `Writable` stream such as an HTTP response stream as shown below:
```js ```js
var template = require('./template'); // Import ./template.marko var template = require("./template"); // Import ./template.marko
module.exports = function(req, res) { module.exports = function(req, res) {
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader("Content-Type", "text/html; charset=utf-8");
template.render({ name: 'Frank' }, res); template.render({ name: "Frank" }, res);
}; };
``` ```
Marko can also provide you with a `Readable` stream. Marko can also provide you with a `Readable` stream.
```js ```js
var template = require('./template'); // Import ./template.marko var template = require("./template"); // Import ./template.marko
module.exports = function(req) { module.exports = function(req) {
// Return a Readable stream for someone to do something with: // Return a Readable stream for someone to do something with:
return template.stream({ name: 'Frank' }); return template.stream({ name: "Frank" });
}; };
``` ```
> **ProTip:** Marko also provides server-side framework integrations: > **ProTip:** Marko also provides server-side framework integrations:
> - [express](/docs/express.md) >
> - [hapi](/docs/hapi.md) > * [express](/docs/express.md)
> - [koa](/docs/koa.md) > * [hapi](/docs/hapi.md)
> - [huncwot](/docs/huncwot.md) > * [koa](/docs/koa.md)
> * [huncwot](/docs/huncwot.md)
## UI Bootstrapping ## UI Bootstrapping
@ -65,35 +66,37 @@ _routes/index/template.marko_
``` ```
> **ProTip:** We have provided some sample apps to help you get started with Marko + Lasso > **ProTip:** We have provided some sample apps to help you get started with Marko + Lasso
> - [marko-lasso](https://github.com/marko-js-samples/marko-lasso) >
> - [ui-components-playground](https://github.com/marko-js-samples/ui-components-playground) > * [marko-lasso](https://github.com/marko-js-samples/marko-lasso)
> * [ui-components-playground](https://github.com/marko-js-samples/ui-components-playground)
### Bootstrapping: Non-Lasso ### Bootstrapping: Non-Lasso
If a JavaScript module bundler other than Lasso is being used then you will need to add some client-side code to bootstrap your application in the browser by doing the following: If a JavaScript module bundler other than Lasso is being used then you will need to add some client-side code to bootstrap your application in the browser by doing the following:
1. Load/import/require all of the UI components that were rendered on the server (loading the top-level UI component is typically sufficient) 1. Load/import/require all of the UI components that were rendered on the server (loading the top-level UI component is typically sufficient)
2. Call `require('marko/components').init()` 2. Call `require('marko/components').init()`
For example, if `client.js` is the entry point for your client-side application: For example, if `client.js` is the entry point for your client-side application:
_routes/index/client.js_ _routes/index/client.js_
```js ```js
// Load the top-level UI component: // Load the top-level UI component:
require('./components/app/index'); require("./components/app/index");
// Now that all of the JavaScript modules for the UI component have been // Now that all of the JavaScript modules for the UI component have been
// loaded and registered we can tell marko to bootstrap/initialize the app // loaded and registered we can tell marko to bootstrap/initialize the app
// Initialize and mount all of the server-rendered UI components: // Initialize and mount all of the server-rendered UI components:
require('marko/components').init(); require("marko/components").init();
``` ```
> **ProTip:** We have provided some sample apps to help you get started: > **ProTip:** We have provided some sample apps to help you get started:
> - [marko-webpack](https://github.com/marko-js-samples/marko-webpack) >
> - [marko-browserify](https://github.com/marko-js-samples/marko-browserify) > * [marko-webpack](https://github.com/marko-js-samples/marko-webpack)
> - [marko-rollup](https://github.com/marko-js-samples/marko-rollup) > * [marko-browserify](https://github.com/marko-js-samples/marko-browserify)
> * [marko-rollup](https://github.com/marko-js-samples/marko-rollup)
# Serialization # Serialization
@ -119,23 +122,26 @@ class {
There are some caveats associated with rendering a page on the server: There are some caveats associated with rendering a page on the server:
- The UI component data for top-level UI components must be serializable: * The UI component data for top-level UI components must be serializable:
- Only simple objects, numbers, strings, booleans, arrays and `Date` objects are serializable * Only simple objects, numbers, strings, booleans, arrays and `Date` objects are serializable
- Functions are not serializable * Functions are not serializable
- Care should be taken to avoid having Marko serialize too much data * Care should be taken to avoid having Marko serialize too much data
- None of the data in `out.global` is serialized by default, but this can be changed as shown below * None of the data in `out.global` is serialized by default, but this can be changed as shown below
## Serializing globals ## Serializing globals
If there are specific properties on the `out.global` object that need to be serialized then they must be whitelisted when the top-level page is rendered on the server. For example, to have the `out.global.apiKey` and the `out.global.locale` properties serialized you would do the following: If there are specific properties on the `out.global` object that need to be serialized then they must be whitelisted when the top-level page is rendered on the server. For example, to have the `out.global.apiKey` and the `out.global.locale` properties serialized you would do the following:
```js ```js
template.render({ template.render(
$global: { {
serializedGlobals: { $global: {
apiKey: true, serializedGlobals: {
locale: true apiKey: true,
} locale: true
} }
}, res); }
},
res
);
``` ```

View File

@ -1,58 +1,52 @@
[{ [
"title": "Introduction", {
"docs": [ "title": "Introduction",
"Installing", "docs": [
"Getting started", "Installing",
"Rendering", "Getting started",
"Syntax", "Rendering",
"Core tags", "Syntax",
"Custom tags", "Core tags",
"Components", "Custom tags",
"Server-side rendering" "Components",
] "Server-side rendering"
},{ ]
"title": "Bundler Integrations", },
"docs": [ {
"Lasso", "title": "Bundler Integrations",
"Webpack", "docs": ["Lasso", "Webpack", "Browserify", "Rollup"]
"Browserify", },
"Rollup" {
] "title": "Server Integrations",
},{ "docs": [
"title": "Server Integrations", "Express",
"docs": [ "Koa",
"Express", "Hapi",
"Koa", "HTTP",
"Hapi", {
"HTTP", "title": "Other",
{ "docs": ["Fastify", "Huncwot"]
"title": "Other", }
"docs": [ ]
"Fastify", },
"Huncwot" {
] "title": "Framework Integrations",
} "docs": ["Redux"]
] },
},{ {
"title": "Framework Integrations", "title": "Tooling",
"docs": [ "docs": ["Editor plugins"]
"Redux" },
] {
},{ "title": "Tutorials",
"title": "Tooling", "docs": ["Color Picker"]
"docs": [ },
"Editor plugins" {
] "title": "Articles",
},{ "docs": [
"title": "Tutorials", "Marko vs React",
"docs": [ "Why is Marko Fast",
"Color Picker" "10 Awesome Marko Features"
] ]
}, { }
"title": "Articles", ]
"docs": [
"Marko vs React",
"Why is Marko Fast",
"10 Awesome Marko Features"
]
}]

View File

@ -1,12 +1,12 @@
# Syntax # Syntax
Marko's syntax is based on HTML, so you basically already know it. Marko extends the HTML language to add a few nice features which we'll cover here. Marko's syntax is based on HTML, so you basically already know it. Marko extends the HTML language to add a few nice features which we'll cover here.
> **ProTip:** Marko also supports a [beautiful concise syntax](./concise.md). If you'd prefer to see our documentation using this syntax, just click the `switch syntax` button in the corner of any Marko code sample. > **ProTip:** Marko also supports a [beautiful concise syntax](./concise.md). If you'd prefer to see our documentation using this syntax, just click the `switch syntax` button in the corner of any Marko code sample.
## Text replacement ## Text replacement
When you render a Marko template, you pass input data that is then available within the template as `input`. You can then use `${}` to insert a value into the template: When you render a Marko template, you pass input data that is then available within the template as `input`. You can then use `${}` to insert a value into the template:
```marko ```marko
<div> <div>
@ -22,7 +22,7 @@ You can actually pass any JavaScript expression here and the result of the expre
</div> </div>
``` ```
These values are automatically escaped so you don't accidentally insert malicious code. If you do need to pass unescaped HTML, you can use `$!{}`: These values are automatically escaped so you don't accidentally insert malicious code. If you do need to pass unescaped HTML, you can use `$!{}`:
```marko ```marko
<div> <div>
@ -42,7 +42,7 @@ If necessary, you can escape `$` using a backslash to have it be treated as text
## Root level text ## Root level text
Text at the root of a template (outside any tags) must be prefixed with the [concise syntax's `--`](./concise.md#text) to denote it is text. The parser starts in concise mode and would otherwise try to parse what you meant to be text as a concise tag declaration. Text at the root of a template (outside any tags) must be prefixed with the [concise syntax's `--`](./concise.md#text) to denote it is text. The parser starts in concise mode and would otherwise try to parse what you meant to be text as a concise tag declaration.
```marko ```marko
-- Root level text -- Root level text
@ -75,22 +75,27 @@ _It does not contain any spaces_
```marko ```marko
<tag sum=1+2 difference=3-4/> <tag sum=1+2 difference=3-4/>
``` ```
```marko ```marko
tag sum=1+2 difference=3-4 tag sum=1+2 difference=3-4
``` ```
_Spaces are contained within matching `()`, `[]`, or `{}`_ _Spaces are contained within matching `()`, `[]`, or `{}`_
```marko ```marko
<tag sum=(1 + 2) difference=(3 - 4)/> <tag sum=(1 + 2) difference=(3 - 4)/>
``` ```
```marko ```marko
tag sum=(1 + 2) difference=(3 - 4) tag sum=(1 + 2) difference=(3 - 4)
``` ```
_Or, commas are used to delimit attributes_ _Or, commas are used to delimit attributes_
```marko ```marko
<tag sum=1 + 2, difference=3 - 4/> <tag sum=1 + 2, difference=3 - 4/>
``` ```
```marko ```marko
tag sum=1 + 2, difference=3 - 4 tag sum=1 + 2, difference=3 - 4
``` ```
@ -104,6 +109,7 @@ Whitespace may optionally be used around the equal sign of an attribute:
```marko ```marko
<tag value = 5/> <tag value = 5/>
``` ```
```marko ```marko
tag value = 5 tag value = 5
``` ```
@ -129,14 +135,17 @@ With a value of `false` for `active`, the output would be the following:
``` ```
### Dynamic attributes ### Dynamic attributes
You can use the `...attrs` syntax inside an open tag to merge in the properties of an object as attributes to a tag: You can use the `...attrs` syntax inside an open tag to merge in the properties of an object as attributes to a tag:
_index.js_ _index.js_
```js ```js
template.render({ attrs:{ class:'active', href:'https://ebay.com/' } }); template.render({ attrs: { class: "active", href: "https://ebay.com/" } });
``` ```
_link.marko_ _link.marko_
```marko ```marko
<a ...input.attrs target="_blank">eBay</a> <a ...input.attrs target="_blank">eBay</a>
``` ```
@ -144,6 +153,7 @@ _link.marko_
would output the following HTML: would output the following HTML:
_output.html_ _output.html_
```html ```html
<a class="active" href="https://ebay.com/" target="_blank">eBay</a> <a class="active" href="https://ebay.com/" target="_blank">eBay</a>
``` ```
@ -177,6 +187,7 @@ The `class` attribute also support object expressions or an array expressions (i
In both cases, the output will be the same: In both cases, the output will be the same:
_output.html_ _output.html_
```html ```html
<div class="a c"></div> <div class="a c"></div>
``` ```
@ -186,6 +197,7 @@ _output.html_
Marko provides a shorthand for declaring classes and ids on an element: Marko provides a shorthand for declaring classes and ids on an element:
_source.marko_ _source.marko_
```marko ```marko
<div.my-class/> <div.my-class/>
<span#my-id/> <span#my-id/>
@ -195,6 +207,7 @@ _source.marko_
Yields this HTML: Yields this HTML:
_output.html_ _output.html_
```html ```html
<div class="my-class"></div> <div class="my-class"></div>
<span id="my-id"></span> <span id="my-id"></span>
@ -203,7 +216,7 @@ _output.html_
## Directives ## Directives
Directives are denoted by parenthesis and take an argument instead of a value. Many directives may be used as both tags and attributes. Directives are denoted by parenthesis and take an argument instead of a value. Many directives may be used as both tags and attributes.
```marko ```marko
<if(true)> <if(true)>
@ -226,11 +239,12 @@ Most directives support JavaScript expressions, and some even support multiple a
``` ```
Others allow a custom syntax: Others allow a custom syntax:
```marko ```marko
<for(item in items)/> <for(item in items)/>
``` ```
Directives are used by many of our [Core Tags](./core-tags.md) for control-flow (`<if>`, `<else-if>`, `<for>`, etc.) and other features. You can also use them in your own [Custom Tags](./custom-tags.md). Directives are used by many of our [Core Tags](./core-tags.md) for control-flow (`<if>`, `<else-if>`, `<for>`, etc.) and other features. You can also use them in your own [Custom Tags](./custom-tags.md).
## Inline JavaScript ## Inline JavaScript
@ -249,7 +263,7 @@ $ var name = input.name;
</div> </div>
``` ```
A statement may continue onto subsequent lines if new lines are bounded by `{}`, `[]`, `()`, ``` `` ```, or `/**/`: A statement may continue onto subsequent lines if new lines are bounded by `{}`, `[]`, `()`, ` `` `, or `/**/`:
```marko ```marko
$ var person = { $ var person = {
@ -270,6 +284,7 @@ $ {
``` ```
### Static JavaScript ### Static JavaScript
> **Static:** The JavaScript code that follows `static` will run once when the template is loaded and be shared by all calls to render. It must be declared at the top level and does not have access to values passed in at render. > **Static:** The JavaScript code that follows `static` will run once when the template is loaded and be shared by all calls to render. It must be declared at the top level and does not have access to values passed in at render.
Inline JavaScript will run each time your template is rendered, if you only want to initialize some values once, use the `static` keyword: Inline JavaScript will run each time your template is rendered, if you only want to initialize some values once, use the `static` keyword:

View File

@ -17,6 +17,7 @@ npm install marko-loader --save
Let's say we have a simple view that we want to render in the browser: `hello.marko` Let's say we have a simple view that we want to render in the browser: `hello.marko`
_hello.marko_ _hello.marko_
```marko ```marko
<h1>Hello ${input.name}</h1> <h1>Hello ${input.name}</h1>
``` ```
@ -24,39 +25,42 @@ _hello.marko_
First, let's create a `client.js` that requires the view and renders it to the body: First, let's create a `client.js` that requires the view and renders it to the body:
_client.js_ _client.js_
```js
var myComponent = require('./hello.marko');
myComponent.renderSync({ name:'Marko' }).appendTo(document.body); ```js
var myComponent = require("./hello.marko");
myComponent.renderSync({ name: "Marko" }).appendTo(document.body);
``` ```
Now, let's configure `webpack` to compile the `client.js` file and use `marko-loader` for any `*.marko` files: Now, let's configure `webpack` to compile the `client.js` file and use `marko-loader` for any `*.marko` files:
_webpack.config.js_ _webpack.config.js_
```js ```js
module.exports = { module.exports = {
entry: "./client.js", entry: "./client.js",
output: { output: {
path: __dirname, path: __dirname,
filename: "static/bundle.js" filename: "static/bundle.js"
}, },
resolve: { resolve: {
extensions: ['.js', '.marko'] extensions: [".js", ".marko"]
}, },
module: { module: {
loaders: [ loaders: [
{ {
test: /\.marko$/, test: /\.marko$/,
loader: 'marko-loader' loader: "marko-loader"
} }
] ]
} }
} };
``` ```
Run `webpack` from your terminal and you'll have a new `static/bundle.js` file created. Reference that from an html file and you're good to go. Run `webpack` from your terminal and you'll have a new `static/bundle.js` file created. Reference that from an html file and you're good to go.
_index.html_ _index.html_
```html ```html
<!doctype html> <!doctype html>
<html> <html>
@ -73,6 +77,7 @@ Load up that page in your browser and you should see `Hello Marko` staring back
If you're using inline css with pre-processors, you must configure the appropriate loader. If you're using inline css with pre-processors, you must configure the appropriate loader.
_pretty.marko_ _pretty.marko_
```marko ```marko
style.less { style.less {
.pretty { .pretty {
@ -84,18 +89,20 @@ style.less {
``` ```
_webpack.config.js_ _webpack.config.js_
```js ```js
//... //...
loaders: [ loaders: [
//... //...
{ {
test: /\.less$/, // matches style.less { ... } from our template test: /\.less$/, // matches style.less { ... } from our template
loader: "style-loader!css-loader!less-loader!" loader: "style-loader!css-loader!less-loader!"
}, }
//... //...
] ];
//... //...
``` ```
## Extracting CSS ## Extracting CSS
It is recommended to configure the [`ExtractTextPlugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) so that you get a separate css bundle rather than it being included in the JavaScript bundle. It is recommended to configure the [`ExtractTextPlugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) so that you get a separate css bundle rather than it being included in the JavaScript bundle.
@ -105,37 +112,38 @@ npm install extract-text-webpack-plugin --save
``` ```
_webpack.config.js_ _webpack.config.js_
```js ```js
'use strict'; "use strict";
var ExtractTextPlugin = require("extract-text-webpack-plugin"); var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = { module.exports = {
entry: "./client.js", entry: "./client.js",
output: { output: {
path: __dirname, path: __dirname,
filename: "static/bundle.js" filename: "static/bundle.js"
}, },
resolve: { resolve: {
extensions: ['.js', '.marko'] extensions: [".js", ".marko"]
}, },
module: { module: {
loaders: [ loaders: [
{ {
test: /\.marko$/, test: /\.marko$/,
loader: 'marko-loader' loader: "marko-loader"
}, },
{ {
test: /\.(less|css)$/, test: /\.(less|css)$/,
loader: ExtractTextPlugin.extract({ loader: ExtractTextPlugin.extract({
fallback: "style-loader", fallback: "style-loader",
use: "css-loader!less-loader" use: "css-loader!less-loader"
}) })
} }
]
},
plugins: [
// Write out CSS bundle to its own file:
new ExtractTextPlugin('static/bundle.css', { allChunks: true })
] ]
},
plugins: [
// Write out CSS bundle to its own file:
new ExtractTextPlugin("static/bundle.css", { allChunks: true })
]
}; };
``` ```

View File

@ -27,7 +27,7 @@ has switched to [a new methodology to measure and understand real-world
JavaScript JavaScript
performance](https://v8project.blogspot.com/2016/12/how-v8-measures-real-world-performance.html). performance](https://v8project.blogspot.com/2016/12/how-v8-measures-real-world-performance.html).
Similarly, weve taken a look at how our developers are *actually* writing their Similarly, weve taken a look at how our developers are _actually_ writing their
Marko components and have found patterns that could be further optimized. Marko components and have found patterns that could be further optimized.
Instead of focusing on benchmarks in this article, I want to focus on the Instead of focusing on benchmarks in this article, I want to focus on the
details of optimizations that we have applied to Marko. details of optimizations that we have applied to Marko.
@ -58,13 +58,11 @@ The compiled output is optimized for streaming HTML output on the server:
```js ```js
var marko_template = require("marko/html").t(__filename), var marko_template = require("marko/html").t(__filename),
marko_helpers = require("marko/runtime/html/helpers"), marko_helpers = require("marko/runtime/html/helpers"),
marko_escapeXml = marko_helpers.x; marko_escapeXml = marko_helpers.x;
function render(input, out) { function render(input, out) {
out.w("<div>Hello " + out.w("<div>Hello " + marko_escapeXml(input.name) + "!</div>");
marko_escapeXml(input.name) +
"!</div>");
} }
``` ```
@ -74,7 +72,8 @@ function render(input, out) {
var marko_template = require("marko/vdom").t(__filename); var marko_template = require("marko/vdom").t(__filename);
function render(input, out) { function render(input, out) {
out.e("DIV", null, 3) out
.e("DIV", null, 3)
.t("Hello ") .t("Hello ")
.t(input.name) .t(input.name)
.t("!"); .t("!");
@ -104,12 +103,17 @@ the `styleAttr` helper is shown below:
var marko_styleAttr = require("marko/runtime/vdom/helper-styleAttr"); var marko_styleAttr = require("marko/runtime/vdom/helper-styleAttr");
function render(input, out) { function render(input, out) {
var color = 'red'; var color = "red";
out.e("DIV", { out.e(
"DIV",
{
style: marko_styleAttr({ style: marko_styleAttr({
backgroundColor: color backgroundColor: color
}) })
}, 0, 4); },
0,
4
);
} }
``` ```
@ -121,7 +125,7 @@ virtual DOM tree on the server its a two-step process to render HTML:
* First pass to produce an entire virtual DOM tree in memory * First pass to produce an entire virtual DOM tree in memory
* Second pass to serialize the virtual DOM tree to an HTML string that can then be * Second pass to serialize the virtual DOM tree to an HTML string that can then be
sent over the wire (this requires traversing the entire tree structure) sent over the wire (this requires traversing the entire tree structure)
In contrast, Marko renders directly to an HTML stream in a single pass. There is In contrast, Marko renders directly to an HTML stream in a single pass. There is
no intermediate tree data structure. no intermediate tree data structure.
@ -176,11 +180,12 @@ Marko will produce the following compiled output:
```js ```js
var marko_attrs0 = { var marko_attrs0 = {
"class": "hello" class: "hello"
}; };
function render(input, out) { function render(input, out) {
out.e("DIV", marko_attrs0, 3) out
.e("DIV", marko_attrs0, 3)
.t("Hello ") .t("Hello ")
.t(input.name) .t(input.name)
.t("!"); .t("!");
@ -229,4 +234,4 @@ root, but this approach uses much less memory and reduces the amount of work
that is done during initialization. The extra overhead of delegating events to that is done during initialization. The extra overhead of delegating events to
components will not be noticeable so it is a very beneficial optimization. components will not be noticeable so it is a very beneficial optimization.
*Cover image credit: [Superhero by Gan Khoon Lay from the Noun Project](https://thenounproject.com/search/?q=superhero&i=690775)* _Cover image credit: [Superhero by Gan Khoon Lay from the Noun Project](https://thenounproject.com/search/?q=superhero&i=690775)_

7
env.js
View File

@ -3,12 +3,11 @@ var isDebug = false;
var env = process.env || {}; var env = process.env || {};
if (env.MARKO_DEBUG != null) { if (env.MARKO_DEBUG != null) {
isDebug = env.MARKO_DEBUG !== 'false' && env.MARKO_DEBUG !== '0'; isDebug = env.MARKO_DEBUG !== "false" && env.MARKO_DEBUG !== "0";
} else { } else {
var NODE_ENV = env.NODE_ENV; var NODE_ENV = env.NODE_ENV;
isDebug = NODE_ENV == null || isDebug =
NODE_ENV === 'development' || NODE_ENV == null || NODE_ENV === "development" || NODE_ENV === "dev";
NODE_ENV === 'dev';
} }
exports.isDebug = isDebug; exports.isDebug = isDebug;

View File

@ -1,6 +1,6 @@
var isDebug = require('./env').isDebug; var isDebug = require("./env").isDebug;
var target = isDebug ? 'marko/src/express' : 'marko/dist/express'; var target = isDebug ? "marko/src/express" : "marko/dist/express";
module.exports = module.parent ? module.exports = module.parent
module.parent.require(target) : ? module.parent.require(target)
require(target); : require(target);

View File

@ -1,5 +1,4 @@
Marko Helpers # Marko Helpers
=============
This directory contains helpers that were deprecated in Marko v3. Marko v4 no longer adds these helpers to every compiled template, but you can still import them if needed: This directory contains helpers that were deprecated in Marko v3. Marko v4 no longer adds these helpers to every compiled template, but you can still import them if needed:

View File

@ -1,5 +1,5 @@
var notEmpty = require('./notEmpty'); var notEmpty = require("./notEmpty");
module.exports = function (o) { module.exports = function(o) {
return !notEmpty(o); return !notEmpty(o);
}; };

View File

@ -3,9 +3,9 @@ module.exports = function notEmpty(o) {
return false; return false;
} else if (Array.isArray(o)) { } else if (Array.isArray(o)) {
return !!o.length; return !!o.length;
} else if (o === '') { } else if (o === "") {
return false; return false;
} }
return true; return true;
}; };

View File

@ -1,7 +1,7 @@
var isDebug = require('./env').isDebug; var isDebug = require("./env").isDebug;
if (isDebug) { if (isDebug) {
module.exports = require('./src/hot-reload'); module.exports = require("./src/hot-reload");
} else { } else {
module.exports = require('./dist/hot-reload'); module.exports = require("./dist/hot-reload");
} }

View File

@ -1,7 +1,7 @@
var isDebug = require('./env').isDebug; var isDebug = require("./env").isDebug;
if (isDebug) { if (isDebug) {
module.exports = require('./src/'); module.exports = require("./src/");
} else { } else {
module.exports = require('./dist/'); module.exports = require("./dist/");
} }

View File

@ -1,7 +1,7 @@
var isDebug = require('./env').isDebug; var isDebug = require("./env").isDebug;
if (isDebug) { if (isDebug) {
module.exports = require('./src/components/legacy'); module.exports = require("./src/components/legacy");
} else { } else {
module.exports = require('./dist/components/legacy'); module.exports = require("./dist/components/legacy");
} }

View File

@ -1,7 +1,7 @@
var isDebug = require('./env').isDebug; var isDebug = require("./env").isDebug;
if (isDebug) { if (isDebug) {
module.exports = require('./src/node-require'); module.exports = require("./src/node-require");
} else { } else {
module.exports = require('./dist/node-require'); module.exports = require("./dist/node-require");
} }

View File

@ -1,139 +1,155 @@
{ {
"name": "marko", "name": "marko",
"description": "UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.", "version": "4.9.1",
"keywords": [ "license": "MIT",
"front-end", "description": "UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.",
"templating", "scripts": {
"template", "build": "node scripts/build.js",
"async", "build-src": "node scripts/build.js src",
"streaming", "prepublish": "npm run build-src",
"components", "precommit": "lint-staged",
"ui", "test": "npm run lint -s && npm run mocha -s",
"vdom", "test-ci": "npm run check-format && npm run test-generate-coverage",
"dom", "mocha": "mocha --timeout 10000 --max-old-space-size=768 ./test/*/*.test.js",
"morphdom", "test-coverage": "npm run test-generate-coverage && nyc report --reporter=html && open ./coverage/index.html",
"virtual", "test-generate-coverage": "nyc -asc npm test",
"virtual-dom" "lint": "eslint .",
], "format": "prettier \"**/*.{js,json,css,md}\" --write",
"repository": { "check-format": "prettier \"**/*.{js,json,css,md}\" -l",
"type": "git", "coveralls": "nyc report --reporter=text-lcov | coveralls",
"url": "https://github.com/marko-js/marko.git" "codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov"
}, },
"scripts": { "lint-staged": {
"build": "node scripts/build.js", "*.js": [
"build-src": "node scripts/build.js src", "eslint"
"prepublish": "npm run build-src", ],
"test": "npm run jshint -s && npm run mocha -s", "*.{js,json,css,md}": [
"test-ci": "npm run test-generate-coverage", "prettier --write",
"mocha": "mocha --timeout 10000 --max-old-space-size=768 ./test/*/*.test.js", "git add"
"test-coverage": "npm run test-generate-coverage && nyc report --reporter=html && open ./coverage/index.html", ]
"test-generate-coverage": "nyc -asc npm test", },
"jshint": "jshint src/ *.js", "dependencies": {
"coveralls": "nyc report --reporter=text-lcov | coveralls", "app-module-path": "^2.2.0",
"codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov" "argly": "^1.0.0",
}, "browser-refresh-client": "^1.0.0",
"author": "Patrick Steele-Idem <pnidem@gmail.com>", "char-props": "~0.1.5",
"maintainers": [ "complain": "^1.2.0",
"Patrick Steele-Idem <pnidem@gmail.com>", "deresolve": "^1.1.2",
"Michael Rawlings <ml.rawlings@gmail.com>", "escodegen": "^1.8.1",
"Phillip Gates-Idem <phillip.idem@gmail.com>", "esprima": "^4.0.0",
"Austin Kelleher <a@alk.im>", "estraverse": "^4.2.0",
"Dylan Piercey <pierceydylan@gmail.com>", "events": "^1.0.2",
"Martin Aberer" "events-light": "^1.0.0",
], "he": "^1.1.0",
"dependencies": { "htmljs-parser": "^2.3.2",
"app-module-path": "^2.2.0", "lasso-caching-fs": "^1.0.1",
"argly": "^1.0.0", "lasso-modules-client": "^2.0.4",
"browser-refresh-client": "^1.0.0", "lasso-package-root": "^1.0.1",
"char-props": "~0.1.5", "listener-tracker": "^2.0.0",
"complain": "^1.2.0", "minimatch": "^3.0.2",
"deresolve": "^1.1.2", "object-assign": "^4.1.0",
"escodegen": "^1.8.1", "property-handlers": "^1.0.0",
"esprima": "^4.0.0", "raptor-json": "^1.0.1",
"estraverse": "^4.2.0", "raptor-polyfill": "^1.0.0",
"events": "^1.0.2", "raptor-promises": "^1.0.1",
"events-light": "^1.0.0", "raptor-regexp": "^1.0.0",
"he": "^1.1.0", "raptor-util": "^3.2.0",
"htmljs-parser": "^2.3.2", "resolve-from": "^2.0.0",
"lasso-caching-fs": "^1.0.1", "shorthash": "0.0.2",
"lasso-modules-client": "^2.0.4", "simple-sha1": "^2.1.0",
"lasso-package-root": "^1.0.1", "strip-json-comments": "^2.0.1",
"listener-tracker": "^2.0.0", "try-require": "^1.2.1",
"minimatch": "^3.0.2", "warp10": "^1.0.0"
"object-assign": "^4.1.0", },
"property-handlers": "^1.0.0", "devDependencies": {
"raptor-json": "^1.0.1", "babel-cli": "^6.24.1",
"raptor-polyfill": "^1.0.0", "babel-core": "^6.24.1",
"raptor-promises": "^1.0.1", "babel-plugin-minprops": "^2.0.1",
"raptor-regexp": "^1.0.0", "benchmark": "^2.1.1",
"raptor-util": "^3.2.0", "bluebird": "^3.4.7",
"resolve-from": "^2.0.0", "builtins": "^2.0.0",
"shorthash": "0.0.2", "chai": "^3.3.0",
"simple-sha1": "^2.1.0", "codecov": "^2.3.0",
"strip-json-comments": "^2.0.1", "coveralls": "^2.13.1",
"try-require": "^1.2.1", "diffable-html": "^2.1.0",
"warp10": "^1.0.0" "eslint": "^4.11.0",
}, "eslint-config-prettier": "^2.9.0",
"devDependencies": { "express": "^4.16.1",
"babel-cli": "^6.24.1", "fs-extra": "^2.0.0",
"babel-core": "^6.24.1", "husky": "^0.14.3",
"babel-plugin-minprops": "^2.0.1", "ignoring-watcher": "^1.0.2",
"benchmark": "^2.1.1", "istanbul-lib-instrument": "^1.8.0",
"bluebird": "^3.4.7", "it-fails": "^1.0.0",
"builtins": "^2.0.0", "jquery": "^3.1.1",
"chai": "^3.3.0", "jsdom": "^9.12.0",
"codecov": "^2.3.0", "lasso-resolve-from": "^1.2.0",
"coveralls": "^2.13.1", "lint-staged": "^7.0.0",
"diffable-html": "^2.1.0", "marko-widgets": "^7.0.1",
"express": "^4.16.1", "micromatch": "^3.0.4",
"fs-extra": "^2.0.0", "mkdirp": "^0.5.1",
"ignoring-watcher": "^1.0.2", "mocha": "^5.0.1",
"istanbul-lib-instrument": "^1.8.0", "nyc": "^10.3.2",
"it-fails": "^1.0.0", "open": "0.0.5",
"jquery": "^3.1.1", "prettier": "^1.11.1",
"jsdom": "^9.12.0", "request": "^2.72.0",
"jshint": "^2.5.0", "semver": "^5.4.1",
"lasso-resolve-from": "^1.2.0", "shelljs": "^0.7.7",
"marko-widgets": "^7.0.1", "through": "^2.3.4",
"micromatch": "^3.0.4", "through2": "^2.0.1"
"mkdirp": "^0.5.1", },
"mocha": "^5.0.1", "main": "index.js",
"nyc": "^10.3.2", "browser": {
"open": "0.0.5", "./compiler.js": "./compiler-browser.marko",
"request": "^2.72.0", "./components.js": "./components-browser.marko",
"semver": "^5.4.1", "./index.js": "./index-browser.marko",
"shelljs": "^0.7.7", "./legacy-components.js": "./legacy-components-browser.marko"
"through": "^2.3.4", },
"through2": "^2.0.1" "bin": {
}, "markoc": "bin/markoc"
"license": "MIT", },
"bin": { "nyc": {
"markoc": "bin/markoc" "exclude": [
}, "**/benchmark/**",
"main": "index.js", "**/scripts/**",
"browser": { "**/coverage/**",
"./compiler.js": "./compiler-browser.marko", "**/test/**",
"./components.js": "./components-browser.marko", "**/test-dist/**",
"./index.js": "./index-browser.marko", "**/test-generated/**",
"./legacy-components.js": "./legacy-components-browser.marko" "**/dist/**"
}, ]
"publishConfig": { },
"registry": "https://registry.npmjs.org/" "homepage": "http://markojs.com/",
}, "logo": {
"nyc": { "url": "https://raw.githubusercontent.com/marko-js/branding/master/marko-logo-small.png"
"exclude": [ },
"**/benchmark/**", "repository": {
"**/scripts/**", "type": "git",
"**/coverage/**", "url": "https://github.com/marko-js/marko.git"
"**/test/**", },
"**/test-dist/**", "publishConfig": {
"**/test-generated/**", "registry": "https://registry.npmjs.org/"
"**/dist/**" },
"author": "Patrick Steele-Idem <pnidem@gmail.com>",
"maintainers": [
"Patrick Steele-Idem <pnidem@gmail.com>",
"Michael Rawlings <ml.rawlings@gmail.com>",
"Phillip Gates-Idem <phillip.idem@gmail.com>",
"Austin Kelleher <a@alk.im>",
"Dylan Piercey <pierceydylan@gmail.com>",
"Martin Aberer"
],
"keywords": [
"front-end",
"templating",
"template",
"async",
"streaming",
"components",
"ui",
"vdom",
"dom",
"morphdom",
"virtual",
"virtual-dom"
] ]
},
"homepage": "http://markojs.com/",
"version": "4.9.1",
"logo": {
"url": "https://raw.githubusercontent.com/marko-js/branding/master/marko-logo-small.png"
}
} }

View File

@ -2,61 +2,73 @@
* Babel plugin for production builds which converts "MARKO_DEBUG" * Babel plugin for production builds which converts "MARKO_DEBUG"
* strings to false to be removed by babel-plugin-minify-dead-code-elimination. * strings to false to be removed by babel-plugin-minify-dead-code-elimination.
*/ */
module.exports = function babelPluginMarkoDebug (babel) { module.exports = function babelPluginMarkoDebug() {
const t = babel.types; return {
return { visitor: {
visitor: { IfStatement: path => {
IfStatement: (path) => { const node = path.node;
const node = path.node; return replaceMarkoDebug(
return replaceMarkoDebug(path, node.test, node.consequent, node.alternate); path,
}, node.test,
ConditionalExpression: (path) => { node.consequent,
const node = path.node; node.alternate
return replaceMarkoDebug(path, node.test, node.consequent, node.alternate); );
}, },
LogicalExpression: (path) => { ConditionalExpression: path => {
const node = path.node; const node = path.node;
return replaceMarkoDebug(
path,
node.test,
node.consequent,
node.alternate
);
},
LogicalExpression: path => {
const node = path.node;
if (node.operator === '&&') { if (node.operator === "&&") {
return replaceMarkoDebug(path, node.left, node.right); return replaceMarkoDebug(path, node.left, node.right);
} else { } else {
return replaceMarkoDebug(path, node.left, null, node.right); return replaceMarkoDebug(path, node.left, null, node.right);
}
}
} }
} };
} };
}
}
/** /**
* Replaces any conditions containing "MARKO_DEBUG" or !"MARKO_DEBUG" and collapses them. * Replaces any conditions containing "MARKO_DEBUG" or !"MARKO_DEBUG" and collapses them.
*/ */
function replaceMarkoDebug (path, test, consequent, alternate) { function replaceMarkoDebug(path, test, consequent, alternate) {
// Reverse replacement when negating the expression. // Reverse replacement when negating the expression.
if (test.type === 'UnaryExpression' && test.operator === '!') { if (test.type === "UnaryExpression" && test.operator === "!") {
const temp = consequent; const temp = consequent;
consequent = alternate; consequent = alternate;
alternate = temp; alternate = temp;
test = test.argument; test = test.argument;
}
// Only look for "MARKO_DEBUG" strings.
if (test.type !== 'StringLiteral') {
return;
}
// If we found a condition that is a string (and isn't "MARKO_DEBUG") it's most likely a mistake.
if (test.value !== 'MARKO_DEBUG') {
throw new Error(`Found if statement with StringLiteral "${test.value}", did you mean "MARKO_DEBUG".`);
}
if (alternate) {
if (alternate.type === 'BlockStatement') {
path.replaceWithMultiple(alternate.body);
} else {
path.replaceWith(alternate);
} }
} else {
path.remove();
}
}
// Only look for "MARKO_DEBUG" strings.
if (test.type !== "StringLiteral") {
return;
}
// If we found a condition that is a string (and isn't "MARKO_DEBUG") it's most likely a mistake.
if (test.value !== "MARKO_DEBUG") {
throw new Error(
`Found if statement with StringLiteral "${
test.value
}", did you mean "MARKO_DEBUG".`
);
}
if (alternate) {
if (alternate.type === "BlockStatement") {
path.replaceWithMultiple(alternate.body);
} else {
path.replaceWith(alternate);
}
} else {
path.remove();
}
}

View File

@ -1,59 +1,55 @@
'use strict'; "use strict";
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
const buildDir = require('./util').buildDir; const buildDir = require("./util").buildDir;
const babelOptions = { const babelOptions = {
"plugins": [ plugins: [
[ [
"minprops", { "minprops",
"matchPrefix": "___", {
"prefix": "", matchPrefix: "___",
"suffix": "_", prefix: "",
"hello": "world", suffix: "_",
"context": "marko" hello: "world",
context: "marko"
} }
], ],
require.resolve('./babel-plugin-marko-debug') require.resolve("./babel-plugin-marko-debug")
] ]
}; };
var target = process.argv[2]; var target = process.argv[2];
var shouldBuildSrc = true; var shouldBuildSrc = true;
var shouldBuildTest = true; var shouldBuildTest = true;
if (target === 'src') { if (target === "src") {
shouldBuildTest = false; shouldBuildTest = false;
} }
if (shouldBuildSrc) { if (shouldBuildSrc) {
buildDir('src', 'dist', { buildDir("src", "dist", {
babelExclude: [ babelExclude: ["/taglibs/async/client-reorder-runtime.min.js"],
'/taglibs/async/client-reorder-runtime.min.js'
],
babelOptions babelOptions
}); });
} }
fs.writeFileSync( fs.writeFileSync(
path.join(__dirname, '../dist/build.json'), path.join(__dirname, "../dist/build.json"),
JSON.stringify({ isDebug: false }, null, 4), JSON.stringify({ isDebug: false }, null, 4),
{ encoding: 'utf8' }); { encoding: "utf8" }
);
if (shouldBuildTest) { if (shouldBuildTest) {
buildDir('test', 'test-dist', { buildDir("test", "test-dist", {
babelExclude: [ babelExclude: ["*expected*.*", "input.js*"],
'*expected*.*',
'input.js*'
],
exclude: [ exclude: [
'/generated', "/generated",
'*.marko.js', "*.marko.js",
'*.skip', "*.skip",
'*.generated.*', "*.generated.*",
'*actual*.*', "*actual*.*",
'actualized-expected.html*' "actualized-expected.html*"
], ],
babelOptions babelOptions
}); });

View File

@ -1,30 +1,32 @@
const shelljs = require('shelljs'); /* eslint-disable no-console */
const shelljs = require("shelljs");
const mkdir = shelljs.mkdir; const mkdir = shelljs.mkdir;
const rm = shelljs.rm; const rm = shelljs.rm;
const cp = shelljs.cp; const cp = shelljs.cp;
const path = require('path'); const path = require("path");
const fs = require('fs'); const fs = require("fs");
const babel = require("babel-core"); const babel = require("babel-core");
const mm = require('micromatch'); const mm = require("micromatch");
const rootDir = path.join(__dirname, '..'); const rootDir = path.join(__dirname, "..");
function babelTransformFile(sourceFile, targetFile, babelOptions) { function babelTransformFile(sourceFile, targetFile, babelOptions) {
babelOptions = Object.assign({}, babelOptions); babelOptions = Object.assign({}, babelOptions);
babelOptions.filename = sourceFile; babelOptions.filename = sourceFile;
var source = fs.readFileSync(sourceFile, 'utf-8'); var source = fs.readFileSync(sourceFile, "utf-8");
var transformed = babel.transform(source, babelOptions).code; var transformed = babel.transform(source, babelOptions).code;
fs.writeFileSync(targetFile, transformed, { encoding: 'utf8' }); fs.writeFileSync(targetFile, transformed, { encoding: "utf8" });
} }
function createMatcher(patterns) { function createMatcher(patterns) {
var matchers = patterns.map((pattern) => { var matchers = patterns.map(pattern => {
return mm.matcher(pattern, { matchBase: true }); return mm.matcher(pattern, { matchBase: true });
}); });
return function isMatch(file) { return function isMatch(file) {
for (var i=0; i<matchers.length; i++) { for (var i = 0; i < matchers.length; i++) {
if (matchers[i](file)) { if (matchers[i](file)) {
return true; return true;
} }
@ -37,7 +39,7 @@ function createMatcher(patterns) {
function findFiles(dir, callback, isExcluded) { function findFiles(dir, callback, isExcluded) {
function findFilesHelper(parentDir, parentRelativePath) { function findFilesHelper(parentDir, parentRelativePath) {
var names = fs.readdirSync(parentDir); var names = fs.readdirSync(parentDir);
for (var i=0; i<names.length; i++) { for (var i = 0; i < names.length; i++) {
var name = names[i]; var name = names[i];
var file = path.join(parentDir, name); var file = path.join(parentDir, name);
var relativePath = path.join(parentRelativePath, name); var relativePath = path.join(parentRelativePath, name);
@ -56,7 +58,7 @@ function findFiles(dir, callback, isExcluded) {
} }
} }
findFilesHelper(dir, '/'); findFilesHelper(dir, "/");
} }
exports.buildDir = function buildDir(sourceName, targetName, options) { exports.buildDir = function buildDir(sourceName, targetName, options) {
@ -78,24 +80,27 @@ exports.buildDir = function buildDir(sourceName, targetName, options) {
isBabelExcluded = createMatcher(options.babelExclude); isBabelExcluded = createMatcher(options.babelExclude);
} }
rm('-rf', distDir); rm("-rf", distDir);
findFiles(sourceDir, function(sourceFile, relativePath, stat) { findFiles(
var targetFile = path.join(distDir, relativePath); sourceDir,
var targetDir = path.dirname(targetFile); function(sourceFile, relativePath, stat) {
var targetFile = path.join(distDir, relativePath);
var targetDir = path.dirname(targetFile);
if (stat.isFile()) { if (stat.isFile()) {
mkdir('-p', targetDir); mkdir("-p", targetDir);
var ext = path.extname(relativePath); var ext = path.extname(relativePath);
if (ext !== '.js' || isBabelExcluded(relativePath)) { if (ext !== ".js" || isBabelExcluded(relativePath)) {
console.log('Copying file:', relativePath); console.log("Copying file:", relativePath);
cp(sourceFile, targetDir + '/'); cp(sourceFile, targetDir + "/");
} else { } else {
console.log("Running babel: " + relativePath); console.log("Running babel: " + relativePath);
babelTransformFile(sourceFile, targetFile, babelOptions); babelTransformFile(sourceFile, targetFile, babelOptions);
}
} }
} },
}, isExcluded); isExcluded
);
}; };

5
src/.eslintrc Normal file
View File

@ -0,0 +1,5 @@
{
"env": {
"browser": true
}
}

View File

@ -1,5 +1,5 @@
var enabled = false; var enabled = false;
var browserRefreshClient = require('browser-refresh-client'); var browserRefreshClient = require("browser-refresh-client");
exports.enable = function(options) { exports.enable = function(options) {
if (!browserRefreshClient.isBrowserRefreshEnabled()) { if (!browserRefreshClient.isBrowserRefreshEnabled()) {
@ -16,13 +16,13 @@ exports.enable = function(options) {
// We set an environment variable so that _all_ marko modules // We set an environment variable so that _all_ marko modules
// installed in the project will have browser refresh enabled. // installed in the project will have browser refresh enabled.
process.env.MARKO_BROWSER_REFRESH = 'true'; process.env.MARKO_BROWSER_REFRESH = "true";
var hotReload = require('./hot-reload'); var hotReload = require("./hot-reload");
hotReload.enable(options); hotReload.enable(options);
browserRefreshClient browserRefreshClient
.enableSpecialReload('*.marko marko.json marko-tag.json') .enableSpecialReload("*.marko marko.json marko-tag.json")
.onFileModified(function(path) { .onFileModified(function(path) {
hotReload.handleFileModified(path, options); hotReload.handleFileModified(path, options);
}); });

View File

@ -1,85 +1,86 @@
'use strict'; "use strict";
var isArray = Array.isArray; var isArray = Array.isArray;
var ok = require('assert').ok; var ok = require("assert").ok;
var Node = require('./ast/Node'); var Node = require("./ast/Node");
var Program = require('./ast/Program'); var Program = require("./ast/Program");
var TemplateRoot = require('./ast/TemplateRoot'); var TemplateRoot = require("./ast/TemplateRoot");
var FunctionDeclaration = require('./ast/FunctionDeclaration'); var FunctionDeclaration = require("./ast/FunctionDeclaration");
var FunctionCall = require('./ast/FunctionCall'); var FunctionCall = require("./ast/FunctionCall");
var Literal = require('./ast/Literal'); var Literal = require("./ast/Literal");
var Identifier = require('./ast/Identifier'); var Identifier = require("./ast/Identifier");
var Comment = require('./ast/Comment'); var Comment = require("./ast/Comment");
var If = require('./ast/If'); var If = require("./ast/If");
var ElseIf = require('./ast/ElseIf'); var ElseIf = require("./ast/ElseIf");
var Else = require('./ast/Else'); var Else = require("./ast/Else");
var Assignment = require('./ast/Assignment'); var Assignment = require("./ast/Assignment");
var BinaryExpression = require('./ast/BinaryExpression'); var BinaryExpression = require("./ast/BinaryExpression");
var LogicalExpression = require('./ast/LogicalExpression'); var LogicalExpression = require("./ast/LogicalExpression");
var Vars = require('./ast/Vars'); var Vars = require("./ast/Vars");
var Return = require('./ast/Return'); var Return = require("./ast/Return");
var HtmlElement = require('./ast/HtmlElement'); var HtmlElement = require("./ast/HtmlElement");
var Html = require('./ast/Html'); var Html = require("./ast/Html");
var Text = require('./ast/Text'); var Text = require("./ast/Text");
var ForEach = require('./ast/ForEach'); var ForEach = require("./ast/ForEach");
var ForEachProp = require('./ast/ForEachProp'); var ForEachProp = require("./ast/ForEachProp");
var ForRange = require('./ast/ForRange'); var ForRange = require("./ast/ForRange");
var HtmlComment = require('./ast/HtmlComment'); var HtmlComment = require("./ast/HtmlComment");
var SelfInvokingFunction = require('./ast/SelfInvokingFunction'); var SelfInvokingFunction = require("./ast/SelfInvokingFunction");
var ForStatement = require('./ast/ForStatement'); var ForStatement = require("./ast/ForStatement");
var BinaryExpression = require('./ast/BinaryExpression'); var UpdateExpression = require("./ast/UpdateExpression");
var UpdateExpression = require('./ast/UpdateExpression'); var UnaryExpression = require("./ast/UnaryExpression");
var UnaryExpression = require('./ast/UnaryExpression'); var MemberExpression = require("./ast/MemberExpression");
var MemberExpression = require('./ast/MemberExpression'); var Code = require("./ast/Code");
var Code = require('./ast/Code'); var InvokeMacro = require("./ast/InvokeMacro");
var InvokeMacro = require('./ast/InvokeMacro'); var Macro = require("./ast/Macro");
var Macro = require('./ast/Macro'); var ConditionalExpression = require("./ast/ConditionalExpression");
var ConditionalExpression = require('./ast/ConditionalExpression'); var NewExpression = require("./ast/NewExpression");
var NewExpression = require('./ast/NewExpression'); var ObjectExpression = require("./ast/ObjectExpression");
var ObjectExpression = require('./ast/ObjectExpression'); var ArrayExpression = require("./ast/ArrayExpression");
var ArrayExpression = require('./ast/ArrayExpression'); var Property = require("./ast/Property");
var Property = require('./ast/Property'); var VariableDeclarator = require("./ast/VariableDeclarator");
var VariableDeclarator = require('./ast/VariableDeclarator'); var ThisExpression = require("./ast/ThisExpression");
var ThisExpression = require('./ast/ThisExpression'); var Expression = require("./ast/Expression");
var Expression = require('./ast/Expression'); var Scriptlet = require("./ast/Scriptlet");
var Scriptlet = require('./ast/Scriptlet'); var ContainerNode = require("./ast/ContainerNode");
var ContainerNode = require('./ast/ContainerNode'); var WhileStatement = require("./ast/WhileStatement");
var WhileStatement = require('./ast/WhileStatement'); var DocumentType = require("./ast/DocumentType");
var DocumentType = require('./ast/DocumentType'); var Declaration = require("./ast/Declaration");
var Declaration = require('./ast/Declaration'); var SequenceExpression = require("./ast/SequenceExpression");
var SequenceExpression = require('./ast/SequenceExpression'); var CustomTag = require("./ast/CustomTag");
var CustomTag = require('./ast/CustomTag');
var parseExpression = require('./util/parseExpression'); var parseExpression = require("./util/parseExpression");
var parseStatement = require('./util/parseStatement'); var parseStatement = require("./util/parseStatement");
var parseJavaScriptArgs = require('./util/parseJavaScriptArgs'); var parseJavaScriptArgs = require("./util/parseJavaScriptArgs");
var replacePlaceholderEscapeFuncs = require('./util/replacePlaceholderEscapeFuncs'); var replacePlaceholderEscapeFuncs = require("./util/replacePlaceholderEscapeFuncs");
var isValidJavaScriptIdentifier = require('./util/isValidJavaScriptIdentifier'); var isValidJavaScriptIdentifier = require("./util/isValidJavaScriptIdentifier");
var DEFAULT_BUILDER; var DEFAULT_BUILDER;
function makeNode(arg) { function makeNode(arg) {
if (typeof arg === 'string') { if (typeof arg === "string") {
return parseExpression(arg, DEFAULT_BUILDER); return parseExpression(arg, DEFAULT_BUILDER);
} else if (arg instanceof Node) { } else if (arg instanceof Node) {
return arg; return arg;
} else if (arg == null) { } else if (arg == null) {
return undefined; return undefined;
} else if (Array.isArray(arg)) { } else if (Array.isArray(arg)) {
return arg.map((arg) => { return arg.map(arg => {
return makeNode(arg); return makeNode(arg);
}); });
} else { } else {
throw new Error('Argument should be a string or Node or null. Actual: ' + arg); throw new Error(
"Argument should be a string or Node or null. Actual: " + arg
);
} }
} }
var literalNull = new Literal({value: null}); var literalNull = new Literal({ value: null });
var literalUndefined = new Literal({value: undefined}); var literalUndefined = new Literal({ value: undefined });
var literalTrue = new Literal({value: true}); var literalTrue = new Literal({ value: true });
var literalFalse = new Literal({value: false}); var literalFalse = new Literal({ value: false });
var identifierOut = new Identifier({name: 'out'}); var identifierOut = new Identifier({ name: "out" });
var identifierRequire = new Identifier({name: 'require'}); var identifierRequire = new Identifier({ name: "require" });
class Builder { class Builder {
arrayExpression(elements) { arrayExpression(elements) {
@ -88,38 +89,38 @@ class Builder {
elements = [elements]; elements = [elements];
} }
for (var i=0; i<elements.length; i++) { for (var i = 0; i < elements.length; i++) {
elements[i] = makeNode(elements[i]); elements[i] = makeNode(elements[i]);
} }
} else { } else {
elements = []; elements = [];
} }
return new ArrayExpression({elements}); return new ArrayExpression({ elements });
} }
assignment(left, right, operator) { assignment(left, right, operator) {
if (operator == null) { if (operator == null) {
operator = '='; operator = "=";
} }
left = makeNode(left); left = makeNode(left);
right = makeNode(right); right = makeNode(right);
return new Assignment({left, right, operator}); return new Assignment({ left, right, operator });
} }
binaryExpression(left, operator, right) { binaryExpression(left, operator, right) {
left = makeNode(left); left = makeNode(left);
right = makeNode(right); right = makeNode(right);
return new BinaryExpression({left, operator, right}); return new BinaryExpression({ left, operator, right });
} }
sequenceExpression(expressions) { sequenceExpression(expressions) {
expressions = makeNode(expressions); expressions = makeNode(expressions);
return new SequenceExpression({expressions}); return new SequenceExpression({ expressions });
} }
code(value) { code(value) {
return new Code({value}); return new Code({ value });
} }
computedMemberExpression(object, property) { computedMemberExpression(object, property) {
@ -127,7 +128,7 @@ class Builder {
property = makeNode(property); property = makeNode(property);
let computed = true; let computed = true;
return new MemberExpression({object, property, computed}); return new MemberExpression({ object, property, computed });
} }
/** /**
@ -138,32 +139,34 @@ class Builder {
*/ */
concat(args) { concat(args) {
var prev; var prev;
let operator = '+'; let operator = "+";
args = Array.isArray(args) ? args : Array.prototype.slice.call(arguments, 0); args = Array.isArray(args)
? args
: Array.prototype.slice.call(arguments, 0);
for (var i=1; i<args.length; i++) { for (var i = 1; i < args.length; i++) {
var left; var left;
var right = makeNode(args[i]); var right = makeNode(args[i]);
if (i === 1) { if (i === 1) {
left = makeNode(args[i-1]); left = makeNode(args[i - 1]);
} else { } else {
left = prev; left = prev;
} }
prev = new BinaryExpression({left, operator, right}); prev = new BinaryExpression({ left, operator, right });
} }
return prev; return prev;
} }
conditionalExpression(test, consequent, alternate) { conditionalExpression(test, consequent, alternate) {
return new ConditionalExpression({test, consequent, alternate}); return new ConditionalExpression({ test, consequent, alternate });
} }
containerNode(type, codeGenerator) { containerNode(type, codeGenerator) {
if (typeof type === 'function') { if (typeof type === "function") {
codeGenerator = arguments[0]; codeGenerator = arguments[0];
type = 'ContainerNode'; type = "ContainerNode";
} }
var node = new ContainerNode(type); var node = new ContainerNode(type);
@ -178,25 +181,25 @@ class Builder {
} }
declaration(declaration) { declaration(declaration) {
return new Declaration({declaration}); return new Declaration({ declaration });
} }
documentType(documentType) { documentType(documentType) {
return new DocumentType({documentType}); return new DocumentType({ documentType });
} }
elseStatement(body) { elseStatement(body) {
return new Else({body}); return new Else({ body });
} }
elseIfStatement(test, body, elseStatement) { elseIfStatement(test, body, elseStatement) {
test = makeNode(test); test = makeNode(test);
return new ElseIf({test, body, else: elseStatement}); return new ElseIf({ test, body, else: elseStatement });
} }
expression(value) { expression(value) {
return new Expression({value}); return new Expression({ value });
} }
forEach(varName, inExpression, body) { forEach(varName, inExpression, body) {
@ -206,7 +209,7 @@ class Builder {
} else { } else {
varName = makeNode(varName); varName = makeNode(varName);
inExpression = makeNode(inExpression); inExpression = makeNode(inExpression);
return new ForEach({varName, in: inExpression, body}); return new ForEach({ varName, in: inExpression, body });
} }
} }
@ -218,7 +221,12 @@ class Builder {
nameVarName = makeNode(nameVarName); nameVarName = makeNode(nameVarName);
valueVarName = makeNode(valueVarName); valueVarName = makeNode(valueVarName);
inExpression = makeNode(inExpression); inExpression = makeNode(inExpression);
return new ForEachProp({nameVarName, valueVarName, in: inExpression, body}); return new ForEachProp({
nameVarName,
valueVarName,
in: inExpression,
body
});
} }
} }
@ -233,7 +241,7 @@ class Builder {
step = makeNode(step); step = makeNode(step);
body = makeNode(body); body = makeNode(body);
return new ForRange({varName, from, to, step, body}); return new ForRange({ varName, from, to, step, body });
} }
} }
@ -245,7 +253,7 @@ class Builder {
init = makeNode(init); init = makeNode(init);
test = makeNode(test); test = makeNode(test);
update = makeNode(update); update = makeNode(update);
return new ForStatement({init, test, update, body}); return new ForStatement({ init, test, update, body });
} }
} }
@ -257,79 +265,86 @@ class Builder {
throw new Error('"args" should be an array'); throw new Error('"args" should be an array');
} }
for (var i=0; i<args.length; i++) { for (var i = 0; i < args.length; i++) {
args[i] = makeNode(args[i]); args[i] = makeNode(args[i]);
} }
} else { } else {
args = []; args = [];
} }
return new FunctionCall({callee, args}); return new FunctionCall({ callee, args });
} }
functionDeclaration(name, params, body) { functionDeclaration(name, params, body) {
return new FunctionDeclaration({name, params, body}); return new FunctionDeclaration({ name, params, body });
} }
html(argument) { html(argument) {
argument = makeNode(argument); argument = makeNode(argument);
return new Html({argument}); return new Html({ argument });
} }
htmlComment(comment) { htmlComment(comment) {
return new HtmlComment({comment}); return new HtmlComment({ comment });
} }
comment(comment) { comment(comment) {
return new Comment({comment}); return new Comment({ comment });
} }
htmlElement(tagName, attributes, body, argument, openTagOnly, selfClosed) { htmlElement(tagName, attributes, body, argument, openTagOnly, selfClosed) {
if (typeof tagName === 'object' && !(tagName instanceof Node)) { if (typeof tagName === "object" && !(tagName instanceof Node)) {
let def = arguments[0]; let def = arguments[0];
return new HtmlElement(def); return new HtmlElement(def);
} else { } else {
return new HtmlElement({tagName, attributes, body, argument, openTagOnly, selfClosed}); return new HtmlElement({
tagName,
attributes,
body,
argument,
openTagOnly,
selfClosed
});
} }
} }
htmlLiteral(htmlCode) { htmlLiteral(htmlCode) {
var argument = new Literal({value: htmlCode}); var argument = new Literal({ value: htmlCode });
return new Html({argument}); return new Html({ argument });
} }
identifier(name) { identifier(name) {
ok(typeof name === 'string', '"name" should be a string'); ok(typeof name === "string", '"name" should be a string');
if (!isValidJavaScriptIdentifier(name)) { if (!isValidJavaScriptIdentifier(name)) {
var error = new Error('Invalid JavaScript identifier: ' + name); var error = new Error("Invalid JavaScript identifier: " + name);
error.code = 'INVALID_IDENTIFIER'; error.code = "INVALID_IDENTIFIER";
throw error; throw error;
} }
return new Identifier({name}); return new Identifier({ name });
} }
identifierOut(name) { identifierOut() {
return identifierOut; return identifierOut;
} }
ifStatement(test, body, elseStatement) { ifStatement(test, body, elseStatement) {
test = makeNode(test); test = makeNode(test);
return new If({test, body, else: elseStatement}); return new If({ test, body, else: elseStatement });
} }
invokeMacro(name, args, body) { invokeMacro(name, args, body) {
return new InvokeMacro({name, args, body}); return new InvokeMacro({ name, args, body });
} }
invokeMacroFromEl(el) { invokeMacroFromEl(el) {
return new InvokeMacro({el}); return new InvokeMacro({ el });
} }
literal(value) { literal(value) {
return new Literal({value}); return new Literal({ value });
} }
literalFalse() { literalFalse() {
@ -351,28 +366,32 @@ class Builder {
logicalExpression(left, operator, right) { logicalExpression(left, operator, right) {
left = makeNode(left); left = makeNode(left);
right = makeNode(right); right = makeNode(right);
return new LogicalExpression({left, operator, right}); return new LogicalExpression({ left, operator, right });
} }
macro(name, params, body) { macro(name, params, body) {
return new Macro({name, params, body}); return new Macro({ name, params, body });
} }
memberExpression(object, property, computed) { memberExpression(object, property, computed) {
object = makeNode(object); object = makeNode(object);
property = makeNode(property); property = makeNode(property);
return new MemberExpression({object, property, computed}); return new MemberExpression({ object, property, computed });
} }
moduleExports(value) { moduleExports(value) {
let object = new Identifier({name: 'module'}); let object = new Identifier({ name: "module" });
let property = new Identifier({name: 'exports'}); let property = new Identifier({ name: "exports" });
var moduleExports = new MemberExpression({object, property }); var moduleExports = new MemberExpression({ object, property });
if (value) { if (value) {
return new Assignment({left: moduleExports, right: value, operator: '='}); return new Assignment({
left: moduleExports,
right: value,
operator: "="
});
} else { } else {
return moduleExports; return moduleExports;
} }
@ -381,9 +400,9 @@ class Builder {
negate(argument) { negate(argument) {
argument = makeNode(argument); argument = makeNode(argument);
var operator = '!'; var operator = "!";
var prefix = true; var prefix = true;
return new UnaryExpression({argument, operator, prefix}); return new UnaryExpression({ argument, operator, prefix });
} }
newExpression(callee, args) { newExpression(callee, args) {
@ -394,20 +413,20 @@ class Builder {
args = [args]; args = [args];
} }
for (var i=0; i<args.length; i++) { for (var i = 0; i < args.length; i++) {
args[i] = makeNode(args[i]); args[i] = makeNode(args[i]);
} }
} else { } else {
args = []; args = [];
} }
return new NewExpression({callee, args}); return new NewExpression({ callee, args });
} }
node(type, generateCode) { node(type, generateCode) {
if (typeof type === 'function') { if (typeof type === "function") {
generateCode = arguments[0]; generateCode = arguments[0];
type = 'Node'; type = "Node";
} }
var node = new Node(type); var node = new Node(type);
@ -420,21 +439,21 @@ class Builder {
objectExpression(properties) { objectExpression(properties) {
if (properties) { if (properties) {
if (isArray(properties)) { if (isArray(properties)) {
for (var i=0; i<properties.length; i++) { for (var i = 0; i < properties.length; i++) {
let prop = properties[i]; let prop = properties[i];
prop.value = makeNode(prop.value); prop.value = makeNode(prop.value);
} }
} else { } else {
let propertiesObject = properties; let propertiesObject = properties;
properties = Object.keys(propertiesObject).map((key) => { properties = Object.keys(propertiesObject).map(key => {
let value = propertiesObject[key]; let value = propertiesObject[key];
if (!(value instanceof Node)) { if (!(value instanceof Node)) {
value = value = new Literal({value}); value = value = new Literal({ value });
} }
key = new Literal({value: key}); key = new Literal({ value: key });
let property = new Property({key, value}); let property = new Property({ key, value });
return property; return property;
}); });
} }
@ -442,22 +461,22 @@ class Builder {
properties = []; properties = [];
} }
return new ObjectExpression({properties}); return new ObjectExpression({ properties });
} }
parseExpression(str, options) { parseExpression(str) {
ok(typeof str === 'string', '"str" should be a string expression'); ok(typeof str === "string", '"str" should be a string expression');
var parsed = parseExpression(str, DEFAULT_BUILDER); var parsed = parseExpression(str, DEFAULT_BUILDER);
return parsed; return parsed;
} }
parseJavaScriptArgs(args) { parseJavaScriptArgs(args) {
ok(typeof args === 'string', '"args" should be a string'); ok(typeof args === "string", '"args" should be a string');
return parseJavaScriptArgs(args, DEFAULT_BUILDER); return parseJavaScriptArgs(args, DEFAULT_BUILDER);
} }
parseStatement(str, options) { parseStatement(str) {
ok(typeof str === 'string', '"str" should be a string expression'); ok(typeof str === "string", '"str" should be a string expression');
var parsed = parseStatement(str, DEFAULT_BUILDER); var parsed = parseStatement(str, DEFAULT_BUILDER);
return parsed; return parsed;
} }
@ -467,7 +486,7 @@ class Builder {
} }
program(body) { program(body) {
return new Program({body}); return new Program({ body });
} }
property(key, value, computed) { property(key, value, computed) {
@ -475,41 +494,41 @@ class Builder {
value = makeNode(value); value = makeNode(value);
computed = computed === true; computed = computed === true;
return new Property({key, value, computed}); return new Property({ key, value, computed });
} }
renderBodyFunction(body, params) { renderBodyFunction(body, params) {
let name = 'renderBody'; let name = "renderBody";
if (!params) { if (!params) {
params = [new Identifier({name: 'out'})]; params = [new Identifier({ name: "out" })];
} }
return new FunctionDeclaration({name, params, body}); return new FunctionDeclaration({ name, params, body });
} }
require(path) { require(path) {
path = makeNode(path); path = makeNode(path);
let callee = identifierRequire; let callee = identifierRequire;
let args = [ path ]; let args = [path];
return new FunctionCall({callee, args}); return new FunctionCall({ callee, args });
} }
requireResolve(path) { requireResolve(path) {
path = makeNode(path); path = makeNode(path);
let callee = new MemberExpression({ let callee = new MemberExpression({
object: new Identifier({name: 'require'}), object: new Identifier({ name: "require" }),
property: new Identifier({name: 'resolve'}) property: new Identifier({ name: "resolve" })
}); });
let args = [ path ]; let args = [path];
return new FunctionCall({callee, args}); return new FunctionCall({ callee, args });
} }
returnStatement(argument) { returnStatement(argument) {
argument = makeNode(argument); argument = makeNode(argument);
return new Return({argument}); return new Return({ argument });
} }
scriptlet(scriptlet) { scriptlet(scriptlet) {
@ -527,29 +546,29 @@ class Builder {
args = null; args = null;
} }
return new SelfInvokingFunction({params, args, body}); return new SelfInvokingFunction({ params, args, body });
} }
strictEquality(left, right) { strictEquality(left, right) {
left = makeNode(left); left = makeNode(left);
right = makeNode(right); right = makeNode(right);
var operator = '==='; var operator = "===";
return new BinaryExpression({left, right, operator}); return new BinaryExpression({ left, right, operator });
} }
templateRoot(body) { templateRoot(body) {
return new TemplateRoot({body}); return new TemplateRoot({ body });
} }
text(argument, escape, preserveWhitespace) { text(argument, escape, preserveWhitespace) {
if (typeof argument === 'object' && !(argument instanceof Node)) { if (typeof argument === "object" && !(argument instanceof Node)) {
var def = arguments[0]; var def = arguments[0];
return new Text(def); return new Text(def);
} }
argument = makeNode(argument); argument = makeNode(argument);
return new Text({argument, escape, preserveWhitespace}); return new Text({ argument, escape, preserveWhitespace });
} }
thisExpression() { thisExpression() {
@ -559,94 +578,93 @@ class Builder {
unaryExpression(argument, operator, prefix) { unaryExpression(argument, operator, prefix) {
argument = makeNode(argument); argument = makeNode(argument);
return new UnaryExpression({argument, operator, prefix}); return new UnaryExpression({ argument, operator, prefix });
} }
updateExpression(argument, operator, prefix) { updateExpression(argument, operator, prefix) {
argument = makeNode(argument); argument = makeNode(argument);
return new UpdateExpression({argument, operator, prefix}); return new UpdateExpression({ argument, operator, prefix });
} }
variableDeclarator(id, init) { variableDeclarator(id, init) {
if (typeof id === 'string') { if (typeof id === "string") {
id = new Identifier({name: id}); id = new Identifier({ name: id });
} }
if (init) { if (init) {
init = makeNode(init); init = makeNode(init);
} }
return new VariableDeclarator({id, init}); return new VariableDeclarator({ id, init });
} }
var(id, init, kind) { var(id, init, kind) {
if (!kind) { if (!kind) {
kind = 'var'; kind = "var";
} }
id = makeNode(id); id = makeNode(id);
init = makeNode(init); init = makeNode(init);
var declarations = [ var declarations = [new VariableDeclarator({ id, init })];
new VariableDeclarator({id, init})
];
return new Vars({declarations, kind}); return new Vars({ declarations, kind });
} }
vars(declarations, kind) { vars(declarations, kind) {
if (declarations) { if (declarations) {
if (Array.isArray(declarations)) { if (Array.isArray(declarations)) {
for (let i=0; i<declarations.length; i++) { for (let i = 0; i < declarations.length; i++) {
var declaration = declarations[i]; var declaration = declarations[i];
if (!declaration) { if (!declaration) {
throw new Error('Invalid variable declaration'); throw new Error("Invalid variable declaration");
} }
if (typeof declaration === 'string') { if (typeof declaration === "string") {
declarations[i] = new VariableDeclarator({ declarations[i] = new VariableDeclarator({
id: new Identifier({name: declaration}) id: new Identifier({ name: declaration })
}); });
} else if (declaration instanceof Identifier) { } else if (declaration instanceof Identifier) {
declarations[i] = new VariableDeclarator({ declarations[i] = new VariableDeclarator({
id: declaration id: declaration
}); });
} else if (typeof declaration === 'object') { } else if (typeof declaration === "object") {
if (!(declaration instanceof VariableDeclarator)) { if (!(declaration instanceof VariableDeclarator)) {
let id = declaration.id; let id = declaration.id;
let init = declaration.init; let init = declaration.init;
if (typeof id === 'string') { if (typeof id === "string") {
id = new Identifier({name: id}); id = new Identifier({ name: id });
} }
if (!id) { if (!id) {
throw new Error('Invalid variable declaration'); throw new Error("Invalid variable declaration");
} }
if (init) { if (init) {
init = makeNode(init); init = makeNode(init);
} }
declarations[i] = new VariableDeclarator({
declarations[i] = new VariableDeclarator({id, init}); id,
init
});
} }
} }
} }
} else if (typeof declarations === 'object') { } else if (typeof declarations === "object") {
// Convert the object into an array of variables // Convert the object into an array of variables
declarations = Object.keys(declarations).map((key) => { declarations = Object.keys(declarations).map(key => {
let id = new Identifier({name: key}); let id = new Identifier({ name: key });
let init = makeNode(declarations[key]); let init = makeNode(declarations[key]);
return new VariableDeclarator({ id, init }); return new VariableDeclarator({ id, init });
}); });
} }
} }
return new Vars({ declarations, kind });
return new Vars({declarations, kind});
} }
whileStatement(test, body) { whileStatement(test, body) {
return new WhileStatement({test, body}); return new WhileStatement({ test, body });
} }
} }

View File

@ -1,14 +1,14 @@
'use strict'; "use strict";
const isArray = Array.isArray; const isArray = Array.isArray;
const Node = require('./ast/Node'); const Node = require("./ast/Node");
const Literal = require('./ast/Literal'); const Literal = require("./ast/Literal");
const Identifier = require('./ast/Identifier'); const Identifier = require("./ast/Identifier");
const HtmlElement = require('./ast/HtmlElement'); const HtmlElement = require("./ast/HtmlElement");
const Html = require('./ast/Html'); const Html = require("./ast/Html");
const ok = require('assert').ok; const ok = require("assert").ok;
const Container = require('./ast/Container'); const Container = require("./ast/Container");
const createError = require('raptor-util/createError'); const createError = require("raptor-util/createError");
class GeneratorEvent { class GeneratorEvent {
constructor(node, codegen) { constructor(node, codegen) {
@ -59,26 +59,23 @@ class FinalNodes {
} }
class CodeGenerator { class CodeGenerator {
constructor(context, options) { constructor(context) {
options = options || {};
this.root = null; this.root = null;
this._code = ''; this._code = "";
this.currentIndent = ''; this.currentIndent = "";
this.inFunction = false; this.inFunction = false;
this._doneListeners = []; this._doneListeners = [];
this.builder = context.builder; this.builder = context.builder;
this.context = context; this.context = context;
ok(this.builder, '"this.builder" is required'); ok(this.builder, '"this.builder" is required');
this._codegenCodeMethodName = 'generate' + this._codegenCodeMethodName =
context.outputType.toUpperCase() + "generate" + context.outputType.toUpperCase() + "Code";
'Code';
} }
addVar(name, value) { addVar(name, value) {
@ -120,20 +117,20 @@ class CodeGenerator {
} else { } else {
return func.call(node, node, this); return func.call(node, node, this);
} }
} catch(err) { } catch (err) {
var errorMessage = 'Generating code for '; var errorMessage = "Generating code for ";
if (node instanceof HtmlElement) { if (node instanceof HtmlElement) {
errorMessage += '<'+node.tagName+'> tag'; errorMessage += "<" + node.tagName + "> tag";
} else { } else {
errorMessage += node.type + ' node'; errorMessage += node.type + " node";
} }
if (node.pos) { if (node.pos) {
errorMessage += ' ('+this.context.getPosInfo(node.pos)+')'; errorMessage += " (" + this.context.getPosInfo(node.pos) + ")";
} }
errorMessage += ' failed. Error: ' + err; errorMessage += " failed. Error: " + err;
throw createError(errorMessage, err /* cause */); throw createError(errorMessage, err /* cause */);
} }
@ -141,12 +138,12 @@ class CodeGenerator {
_generateCode(node, finalNodes) { _generateCode(node, finalNodes) {
if (isArray(node)) { if (isArray(node)) {
node.forEach((child) => { node.forEach(child => {
this._generateCode(child, finalNodes); this._generateCode(child, finalNodes);
}); });
return; return;
} else if (node instanceof Container) { } else if (node instanceof Container) {
node.forEach((child) => { node.forEach(child => {
if (child.container === node) { if (child.container === node) {
this._generateCode(child, finalNodes); this._generateCode(child, finalNodes);
} }
@ -158,7 +155,11 @@ class CodeGenerator {
return; return;
} }
if (typeof node === 'string' || node._finalNode || !(node instanceof Node)) { if (
typeof node === "string" ||
node._finalNode ||
!(node instanceof Node)
) {
finalNodes.push(node); finalNodes.push(node);
return; return;
} }
@ -179,9 +180,12 @@ class CodeGenerator {
} }
beforeAfterEvent.isBefore = true; beforeAfterEvent.isBefore = true;
beforeAfterEvent.node.emit('beforeGenerateCode', beforeAfterEvent); beforeAfterEvent.node.emit("beforeGenerateCode", beforeAfterEvent);
this.context.emit('beforeGenerateCode:' + beforeAfterEvent.node.type, beforeAfterEvent); this.context.emit(
this.context.emit('beforeGenerateCode', beforeAfterEvent); "beforeGenerateCode:" + beforeAfterEvent.node.type,
beforeAfterEvent
);
this.context.emit("beforeGenerateCode", beforeAfterEvent);
if (beforeAfterEvent.insertedNodes) { if (beforeAfterEvent.insertedNodes) {
this._generateCode(beforeAfterEvent.insertedNodes, finalNodes); this._generateCode(beforeAfterEvent.insertedNodes, finalNodes);
@ -197,11 +201,18 @@ class CodeGenerator {
if (codeGeneratorFunc) { if (codeGeneratorFunc) {
node.setCodeGenerator(null); node.setCodeGenerator(null);
generatedCode = this._invokeCodeGenerator(codeGeneratorFunc, node, false); generatedCode = this._invokeCodeGenerator(
codeGeneratorFunc,
node,
false
);
if (generatedCode === null) { if (generatedCode === null) {
node = null; node = null;
} else if (generatedCode !== undefined && generatedCode !== node) { } else if (
generatedCode !== undefined &&
generatedCode !== node
) {
node = null; node = null;
this._generateCode(generatedCode, finalNodes); this._generateCode(generatedCode, finalNodes);
} }
@ -216,7 +227,11 @@ class CodeGenerator {
} }
if (codeGeneratorFunc) { if (codeGeneratorFunc) {
generatedCode = this._invokeCodeGenerator(codeGeneratorFunc, node, true); generatedCode = this._invokeCodeGenerator(
codeGeneratorFunc,
node,
true
);
if (generatedCode === undefined || generatedCode === node) { if (generatedCode === undefined || generatedCode === node) {
finalNodes.push(node); finalNodes.push(node);
@ -231,9 +246,12 @@ class CodeGenerator {
} }
beforeAfterEvent.isBefore = false; beforeAfterEvent.isBefore = false;
beforeAfterEvent.node.emit('afterGenerateCode', beforeAfterEvent); beforeAfterEvent.node.emit("afterGenerateCode", beforeAfterEvent);
this.context.emit('afterGenerateCode:' + beforeAfterEvent.node.type, beforeAfterEvent); this.context.emit(
this.context.emit('afterGenerateCode', beforeAfterEvent); "afterGenerateCode:" + beforeAfterEvent.node.type,
beforeAfterEvent
);
this.context.emit("afterGenerateCode", beforeAfterEvent);
if (beforeAfterEvent.insertedNodes) { if (beforeAfterEvent.insertedNodes) {
this._generateCode(beforeAfterEvent.insertedNodes, finalNodes); this._generateCode(beforeAfterEvent.insertedNodes, finalNodes);
@ -258,7 +276,7 @@ class CodeGenerator {
let finalNodes = new FinalNodes(); let finalNodes = new FinalNodes();
var isList = typeof node.forEach === 'function'; var isList = typeof node.forEach === "function";
this._generateCode(node, finalNodes); this._generateCode(node, finalNodes);
@ -292,12 +310,12 @@ class CodeGenerator {
let node = this._currentNode; let node = this._currentNode;
if (typeof message === 'object') { if (typeof message === "object") {
let errorInfo = message; let errorInfo = message;
errorInfo.node = node; errorInfo.node = node;
this.context.addError(errorInfo); this.context.addError(errorInfo);
} else { } else {
this.context.addError({node, message, code}); this.context.addError({ node, message, code });
} }
} }
@ -314,4 +332,4 @@ class CodeGenerator {
} }
} }
module.exports = CodeGenerator; module.exports = CodeGenerator;

View File

@ -1,13 +1,13 @@
'use strict'; "use strict";
const isArray = Array.isArray; const isArray = Array.isArray;
const Node = require('./ast/Node'); const Node = require("./ast/Node");
const Literal = require('./ast/Literal'); const Literal = require("./ast/Literal");
const Identifier = require('./ast/Identifier'); const Identifier = require("./ast/Identifier");
const ok = require('assert').ok; const ok = require("assert").ok;
const Container = require('./ast/Container'); const Container = require("./ast/Container");
const Comment = require('./ast/Comment'); const Comment = require("./ast/Comment");
const isValidJavaScriptVarName = require('./util/isValidJavaScriptVarName'); const isValidJavaScriptVarName = require("./util/isValidJavaScriptVarName");
class CodeWriter { class CodeWriter {
constructor(options, builder) { constructor(options, builder) {
@ -15,11 +15,11 @@ class CodeWriter {
options = options || {}; options = options || {};
this.builder = builder; this.builder = builder;
this.root = null; this.root = null;
this._indentStr = options.indent != null ? options.indent : ' '; this._indentStr = options.indent != null ? options.indent : " ";
this._indentSize = this._indentStr.length; this._indentSize = this._indentStr.length;
this._code = ''; this._code = "";
this.currentIndent = ''; this.currentIndent = "";
} }
getCode() { getCode() {
@ -28,29 +28,30 @@ class CodeWriter {
writeBlock(body) { writeBlock(body) {
if (!body) { if (!body) {
this.write('{}'); this.write("{}");
return; return;
} }
if (typeof body === 'function') { if (typeof body === "function") {
body = body(); body = body();
} }
if (!body || if (
!body ||
(Array.isArray(body) && body.length === 0) || (Array.isArray(body) && body.length === 0) ||
(body instanceof Container && body.length === 0)) { (body instanceof Container && body.length === 0)
this.write('{}'); ) {
this.write("{}");
return; return;
} }
this.write('{\n') this.write("{\n").incIndent();
.incIndent();
this.writeStatements(body); this.writeStatements(body);
this.decIndent() this.decIndent()
.writeLineIndent() .writeLineIndent()
.write('}'); .write("}");
} }
writeStatements(nodes) { writeStatements(nodes) {
@ -61,34 +62,37 @@ class CodeWriter {
ok(nodes, '"nodes" expected'); ok(nodes, '"nodes" expected');
let firstStatement = true; let firstStatement = true;
var writeNode = (node) => { var writeNode = node => {
if (Array.isArray(node) || (node instanceof Container)) { if (Array.isArray(node) || node instanceof Container) {
node.forEach(writeNode); node.forEach(writeNode);
return; return;
} else { } else {
if (firstStatement) { if (firstStatement) {
firstStatement = false; firstStatement = false;
} else { } else {
this._write('\n'); this._write("\n");
} }
this.writeLineIndent(); this.writeLineIndent();
if (typeof node === 'string') { if (typeof node === "string") {
this._write(node); this._write(node);
} else { } else {
node.statement = true; node.statement = true;
this.write(node); this.write(node);
} }
if (this._code.endsWith('\n')) { if (this._code.endsWith("\n")) {
// Do nothing // Do nothing
} else if (this._code.endsWith(';')) { } else if (this._code.endsWith(";")) {
this._code += '\n'; this._code += "\n";
} else if (this._code.endsWith('\n' + this.currentIndent) || node instanceof Comment) { } else if (
this._code.endsWith("\n" + this.currentIndent) ||
node instanceof Comment
) {
// Do nothing // Do nothing
} else { } else {
this._code += ';\n'; this._code += ";\n";
} }
} }
}; };
@ -101,25 +105,28 @@ class CodeWriter {
} }
write(code) { write(code) {
if (code == null || code === '') { if (code == null || code === "") {
return; return;
} }
if (code instanceof Node) { if (code instanceof Node) {
let node = code; let node = code;
if (!node.writeCode) { if (!node.writeCode) {
throw new Error('Node does not have a `writeCode` method: ' + JSON.stringify(node, null, 4)); throw new Error(
"Node does not have a `writeCode` method: " +
JSON.stringify(node, null, 4)
);
} }
node.writeCode(this); node.writeCode(this);
} else if (isArray(code) || code instanceof Container) { } else if (isArray(code) || code instanceof Container) {
code.forEach(this.write, this); code.forEach(this.write, this);
return; return;
} else if (typeof code === 'string') { } else if (typeof code === "string") {
this._code += code; this._code += code;
} else if (typeof code === 'boolean' || typeof code === 'number') { } else if (typeof code === "boolean" || typeof code === "number") {
this._code += code.toString(); this._code += code.toString();
} else { } else {
throw new Error('Illegal argument: ' + JSON.stringify(code)); throw new Error("Illegal argument: " + JSON.stringify(code));
} }
return this; return this;
@ -132,8 +139,8 @@ class CodeWriter {
incIndent(count) { incIndent(count) {
if (count != null) { if (count != null) {
for (let i=0; i<count; i++) { for (let i = 0; i < count; i++) {
this.currentIndent += ' '; this.currentIndent += " ";
} }
} else { } else {
this.currentIndent += this._indentStr; this.currentIndent += this._indentStr;
@ -149,7 +156,8 @@ class CodeWriter {
this.currentIndent = this.currentIndent.substring( this.currentIndent = this.currentIndent.substring(
0, 0,
this.currentIndent.length - count); this.currentIndent.length - count
);
return this; return this;
} }
@ -174,25 +182,25 @@ class CodeWriter {
writeLiteral(value) { writeLiteral(value) {
if (value === null) { if (value === null) {
this.write('null'); this.write("null");
} else if (value === undefined) { } else if (value === undefined) {
this.write('undefined'); this.write("undefined");
} else if (typeof value === 'string') { } else if (typeof value === "string") {
this.write(JSON.stringify(value)); this.write(JSON.stringify(value));
} else if (value === true) { } else if (value === true) {
this.write('true'); this.write("true");
} else if (value === false) { } else if (value === false) {
this.write('false'); this.write("false");
} else if (isArray(value)) { } else if (isArray(value)) {
if (value.length === 0) { if (value.length === 0) {
this.write('[]'); this.write("[]");
return; return;
} }
this.write('[\n'); this.write("[\n");
this.incIndent(); this.incIndent();
for (let i=0; i<value.length; i++) { for (let i = 0; i < value.length; i++) {
let v = value[i]; let v = value[i];
this.writeLineIndent(); this.writeLineIndent();
@ -204,40 +212,40 @@ class CodeWriter {
} }
if (i < value.length - 1) { if (i < value.length - 1) {
this.write(',\n'); this.write(",\n");
} else { } else {
this.write('\n'); this.write("\n");
} }
} }
this.decIndent(); this.decIndent();
this.writeLineIndent(); this.writeLineIndent();
this.write(']'); this.write("]");
} else if (typeof value === 'number') { } else if (typeof value === "number") {
this.write(value.toString()); this.write(value.toString());
} else if (value instanceof RegExp) { } else if (value instanceof RegExp) {
this.write(value.toString()); this.write(value.toString());
} else if (typeof value === 'object') { } else if (typeof value === "object") {
let keys = Object.keys(value); let keys = Object.keys(value);
if (keys.length === 0) { if (keys.length === 0) {
this.write('{}'); this.write("{}");
return; return;
} }
this.incIndent(); this.incIndent();
this.write('{\n'); this.write("{\n");
this.incIndent(); this.incIndent();
for (let i=0; i<keys.length; i++) { for (let i = 0; i < keys.length; i++) {
let k = keys[i]; let k = keys[i];
let v = value[k]; let v = value[k];
this.writeLineIndent(); this.writeLineIndent();
if (isValidJavaScriptVarName(k)) { if (isValidJavaScriptVarName(k)) {
this.write(k + ': '); this.write(k + ": ");
} else { } else {
this.write(JSON.stringify(k) + ': '); this.write(JSON.stringify(k) + ": ");
} }
if (v instanceof Node) { if (v instanceof Node) {
@ -247,15 +255,15 @@ class CodeWriter {
} }
if (i < keys.length - 1) { if (i < keys.length - 1) {
this.write(',\n'); this.write(",\n");
} else { } else {
this.write('\n'); this.write("\n");
} }
} }
this.decIndent(); this.decIndent();
this.writeLineIndent(); this.writeLineIndent();
this.write('}'); this.write("}");
this.decIndent(); this.decIndent();
} }
} }

View File

@ -1,27 +1,26 @@
'use strict'; "use strict";
var ok = require('assert').ok; var ok = require("assert").ok;
var path = require('path'); var path = require("path");
var complain = require('complain'); var complain = require("complain");
var taglibLookup = require('./taglib-lookup'); var taglibLookup = require("./taglib-lookup");
var charProps = require('char-props'); var charProps = require("char-props");
var UniqueVars = require('./util/UniqueVars'); var UniqueVars = require("./util/UniqueVars");
var PosInfo = require('./util/PosInfo'); var PosInfo = require("./util/PosInfo");
var CompileError = require('./CompileError'); var CompileError = require("./CompileError");
var path = require('path'); var Node = require("./ast/Node");
var Node = require('./ast/Node'); var macros = require("./util/macros");
var macros = require('./util/macros'); var extend = require("raptor-util/extend");
var extend = require('raptor-util/extend'); var Walker = require("./Walker");
var Walker = require('./Walker'); var EventEmitter = require("events").EventEmitter;
var EventEmitter = require('events').EventEmitter; var utilFingerprint = require("./util/finger-print");
var utilFingerprint = require('./util/finger-print'); var htmlElements = require("./util/html-elements");
var htmlElements = require('./util/html-elements'); var markoModules = require("./modules");
var markoModules = require('./modules');
const markoPkgVersion = require('../../package.json').version; const markoPkgVersion = require("../../package.json").version;
const rootDir = path.join(__dirname, '../'); const rootDir = path.join(__dirname, "../");
const isDebug = require('../build.json').isDebug; const isDebug = require("../build.json").isDebug;
// const FLAG_IS_SVG = 1; // const FLAG_IS_SVG = 1;
// const FLAG_IS_TEXTAREA = 2; // const FLAG_IS_TEXTAREA = 2;
@ -29,10 +28,10 @@ const isDebug = require('../build.json').isDebug;
// const FLAG_PRESERVE = 8; // const FLAG_PRESERVE = 8;
const FLAG_CUSTOM_ELEMENT = 16; const FLAG_CUSTOM_ELEMENT = 16;
const FLAG_PRESERVE_WHITESPACE = 'PRESERVE_WHITESPACE'; const FLAG_PRESERVE_WHITESPACE = "PRESERVE_WHITESPACE";
function getTaglibPath(taglibPath) { function getTaglibPath(taglibPath) {
if (typeof window === 'undefined') { if (typeof window === "undefined") {
return path.relative(process.cwd(), taglibPath); return path.relative(process.cwd(), taglibPath);
} else { } else {
return taglibPath; return taglibPath;
@ -50,76 +49,80 @@ function removeExt(filename) {
function requireResolve(builder, path) { function requireResolve(builder, path) {
var requireResolveNode = builder.memberExpression( var requireResolveNode = builder.memberExpression(
builder.identifier('require'), builder.identifier("require"),
builder.identifier('resolve')); builder.identifier("resolve")
);
return builder.functionCall(requireResolveNode, [path]);
return builder.functionCall(requireResolveNode, [ path ]);
} }
const helpers = { const helpers = {
'attr': 'a', attr: "a",
'attrs': 'as', attrs: "as",
'classAttr': 'ca', classAttr: "ca",
'classList': 'cl', classList: "cl",
'const': 'const', const: "const",
'createElement': 'e', createElement: "e",
'createInlineTemplate': { createInlineTemplate: {
vdom: { module: 'marko/runtime/vdom/helper-createInlineTemplate'}, vdom: { module: "marko/runtime/vdom/helper-createInlineTemplate" },
html: { module: 'marko/runtime/html/helper-createInlineTemplate'} html: { module: "marko/runtime/html/helper-createInlineTemplate" }
}, },
'defineComponent': { defineComponent: {
module: 'marko/components/helpers', module: "marko/components/helpers",
method: 'c' method: "c"
}, },
'defineComponent-legacy': { "defineComponent-legacy": {
module: 'marko/components/legacy/helpers', module: "marko/components/legacy/helpers",
method: 'c' method: "c"
}, },
'defineWidget-legacy': { "defineWidget-legacy": {
module: 'marko/components/legacy/helpers', module: "marko/components/legacy/helpers",
method: 'w' method: "w"
}, },
'escapeXml': 'x', escapeXml: "x",
'escapeXmlAttr': 'xa', escapeXmlAttr: "xa",
'escapeScript': 'xs', escapeScript: "xs",
'escapeStyle': 'xc', escapeStyle: "xc",
'forEach': 'f', forEach: "f",
'forEachProp': { module: 'marko/runtime/helper-forEachProperty' }, forEachProp: { module: "marko/runtime/helper-forEachProperty" },
'forEachPropStatusVar': { module: 'marko/runtime/helper-forEachPropStatusVar' }, forEachPropStatusVar: {
'forEachWithStatusVar': { module: 'marko/runtime/helper-forEachWithStatusVar' }, module: "marko/runtime/helper-forEachPropStatusVar"
'forRange': { module: 'marko/runtime/helper-forRange' },
'include': 'i',
'loadNestedTag': { module: 'marko/runtime/helper-loadNestedTag' },
'loadTag': 't',
'loadTemplate': { module: 'marko/runtime/helper-loadTemplate' },
'mergeNestedTagsHelper': { module: 'marko/runtime/helper-mergeNestedTags' },
'merge': { module: 'marko/runtime/helper-merge' },
'propsForPreviousNode': 'p',
'renderer': {
module: 'marko/components/helpers',
method: 'r'
}, },
'rendererLegacy': { forEachWithStatusVar: {
module: 'marko/components/legacy/helpers', module: "marko/runtime/helper-forEachWithStatusVar"
method: 'r'
}, },
'registerComponent': { forRange: { module: "marko/runtime/helper-forRange" },
module: 'marko/components/helpers', include: "i",
method: 'rc' loadNestedTag: { module: "marko/runtime/helper-loadNestedTag" },
loadTag: "t",
loadTemplate: { module: "marko/runtime/helper-loadTemplate" },
mergeNestedTagsHelper: { module: "marko/runtime/helper-mergeNestedTags" },
merge: { module: "marko/runtime/helper-merge" },
propsForPreviousNode: "p",
renderer: {
module: "marko/components/helpers",
method: "r"
}, },
'str': 's', rendererLegacy: {
'styleAttr': { module: "marko/components/legacy/helpers",
vdom: { module: 'marko/runtime/vdom/helper-styleAttr'}, method: "r"
html: 'sa'
}, },
'createText': 't' registerComponent: {
module: "marko/components/helpers",
method: "rc"
},
str: "s",
styleAttr: {
vdom: { module: "marko/runtime/vdom/helper-styleAttr" },
html: "sa"
},
createText: "t"
}; };
class CompileContext extends EventEmitter { class CompileContext extends EventEmitter {
constructor(src, filename, builder, options) { constructor(src, filename, builder, options) {
super(); super();
ok(typeof src === 'string', '"src" string is required'); ok(typeof src === "string", '"src" string is required');
ok(filename, '"filename" is required'); ok(filename, '"filename" is required');
this.src = src; this.src = src;
@ -137,12 +140,14 @@ class CompileContext extends EventEmitter {
const writeVersionComment = this.options.writeVersionComment; const writeVersionComment = this.options.writeVersionComment;
this.root = undefined; // root will be set by the Compiler this.root = undefined; // root will be set by the Compiler
this.target = this.options.browser ? 'browser' : 'server'; this.target = this.options.browser ? "browser" : "server";
this.outputType = this.options.output || 'html'; this.outputType = this.options.output || "html";
this.compilerType = this.options.compilerType || 'marko'; this.compilerType = this.options.compilerType || "marko";
this.compilerVersion = this.options.compilerVersion || markoPkgVersion; this.compilerVersion = this.options.compilerVersion || markoPkgVersion;
this.writeVersionComment = writeVersionComment !== 'undefined' ? writeVersionComment : true; this.writeVersionComment =
this.ignoreUnrecognizedTags = this.options.ignoreUnrecognizedTags === true; writeVersionComment !== "undefined" ? writeVersionComment : true;
this.ignoreUnrecognizedTags =
this.options.ignoreUnrecognizedTags === true;
this.escapeAtTags = this.options.escapeAtTags === true; this.escapeAtTags = this.options.escapeAtTags === true;
this._vars = {}; this._vars = {};
@ -158,9 +163,11 @@ class CompileContext extends EventEmitter {
this._preserveComments = null; this._preserveComments = null;
this.inline = this.options.inline === true; this.inline = this.options.inline === true;
this.useMeta = this.options.meta !== false; this.useMeta = this.options.meta !== false;
this.markoModulePrefix = isDebug ? 'marko/src/' : 'marko/dist/'; this.markoModulePrefix = isDebug ? "marko/src/" : "marko/dist/";
this._moduleRuntimeTarget = this.markoModulePrefix + (this.outputType === 'vdom' ? 'vdom' : 'html'); this._moduleRuntimeTarget =
this.markoModulePrefix +
(this.outputType === "vdom" ? "vdom" : "html");
this.unrecognizedTags = []; this.unrecognizedTags = [];
this._parsingFinished = false; this._parsingFinished = false;
@ -182,8 +189,9 @@ class CompileContext extends EventEmitter {
} }
getPosInfo(pos) { getPosInfo(pos) {
var srcCharProps = this._srcCharProps || (this._srcCharProps = charProps(this.src)); var srcCharProps =
let line = srcCharProps.lineAt(pos)+1; this._srcCharProps || (this._srcCharProps = charProps(this.src));
let line = srcCharProps.lineAt(pos) + 1;
let column = srcCharProps.columnAt(pos); let column = srcCharProps.columnAt(pos);
return new PosInfo(this.filename, line, column); return new PosInfo(this.filename, line, column);
} }
@ -218,7 +226,9 @@ class CompileContext extends EventEmitter {
popFlag(name) { popFlag(name) {
if (!this._flags.hasOwnProperty(name)) { if (!this._flags.hasOwnProperty(name)) {
throw new Error('popFlag() called for "' + name + '" when flag was not set'); throw new Error(
'popFlag() called for "' + name + '" when flag was not set'
);
} }
if (--this._flags[name] === 0) { if (--this._flags[name] === 0) {
@ -245,7 +255,8 @@ class CompileContext extends EventEmitter {
var dataStack = this._dataStacks[key]; var dataStack = this._dataStacks[key];
if (!dataStack || dataStack.length === 0) { if (!dataStack || dataStack.length === 0) {
throw new Error('No data pushed for "' + key + '"'); } throw new Error('No data pushed for "' + key + '"');
}
dataStack.pop(); dataStack.pop();
@ -286,7 +297,7 @@ class CompileContext extends EventEmitter {
code, code,
pos pos
}; };
} else if (typeof errorInfo === 'string') { } else if (typeof errorInfo === "string") {
let message = arguments[0]; let message = arguments[0];
let code = arguments[1]; let code = arguments[1];
let pos = arguments[2]; let pos = arguments[2];
@ -298,7 +309,7 @@ class CompileContext extends EventEmitter {
}; };
} }
if(errorInfo && !errorInfo.node) { if (errorInfo && !errorInfo.node) {
errorInfo.node = this._currentNode; errorInfo.node = this._currentNode;
} }
@ -315,33 +326,36 @@ class CompileContext extends EventEmitter {
getRequirePath(targetFilename) { getRequirePath(targetFilename) {
if (targetFilename.startsWith(rootDir)) { if (targetFilename.startsWith(rootDir)) {
var deresolved = this.markoModulePrefix + targetFilename.substring(rootDir.length); var deresolved =
this.markoModulePrefix +
targetFilename.substring(rootDir.length);
var ext = path.extname(deresolved); var ext = path.extname(deresolved);
if (ext === '.js') { if (ext === ".js") {
deresolved = deresolved.slice(0, 0 - ext.length); deresolved = deresolved.slice(0, 0 - ext.length);
} }
return deresolved.replace(/\\/g, '/'); return deresolved.replace(/\\/g, "/");
} }
return markoModules.deresolve(targetFilename, this.dirname); return markoModules.deresolve(targetFilename, this.dirname);
} }
importModule(varName, path) { importModule(varName, path) {
if (typeof path !== 'string') { if (typeof path !== "string") {
throw new Error('"path" should be a string'); throw new Error('"path" should be a string');
} }
if (path === 'marko') { if (path === "marko") {
path = this.markoModulePrefix; path = this.markoModulePrefix;
} else if (path.startsWith('marko/')) { } else if (path.startsWith("marko/")) {
if (path.startsWith('marko/src/') || path.startsWith('marko/dist/')) { if (
!path.startsWith("marko/src/") &&
} else { !path.startsWith("marko/dist/")
path = this.markoModulePrefix + path.substring('marko/'.length); ) {
path = this.markoModulePrefix + path.substring("marko/".length);
} }
} }
var key = path + ':' + (varName != null); var key = path + ":" + (varName != null);
var varId = this._imports[key]; var varId = this._imports[key];
if (varId === undefined) { if (varId === undefined) {
@ -350,8 +364,10 @@ class CompileContext extends EventEmitter {
var requireFuncCall = builder.require(builder.literal(path)); var requireFuncCall = builder.require(builder.literal(path));
if (varName) { if (varName) {
this._imports[key] = varId = this.addStaticVar(varName, requireFuncCall); this._imports[key] = varId = this.addStaticVar(
varName,
requireFuncCall
);
} else { } else {
this.addStaticCode(requireFuncCall); this.addStaticCode(requireFuncCall);
this._imports[key] = null; this._imports[key] = null;
@ -386,7 +402,7 @@ class CompileContext extends EventEmitter {
return; return;
} }
if (typeof code === 'string') { if (typeof code === "string") {
// Wrap the String code in a Code AST node so that // Wrap the String code in a Code AST node so that
// the code will be indented properly // the code will be indented properly
code = this.builder.code(code); code = this.builder.code(code);
@ -406,7 +422,7 @@ class CompileContext extends EventEmitter {
getTagDef(tagName) { getTagDef(tagName) {
var taglibLookup = this.taglibLookup; var taglibLookup = this.taglibLookup;
if (typeof tagName === 'string') { if (typeof tagName === "string") {
return taglibLookup.getTag(tagName); return taglibLookup.getTag(tagName);
} else { } else {
let elNode = tagName; let elNode = tagName;
@ -421,7 +437,10 @@ class CompileContext extends EventEmitter {
addErrorUnrecognizedTag(tagName, elNode) { addErrorUnrecognizedTag(tagName, elNode) {
this.addError({ this.addError({
node: elNode, node: elNode,
message: 'Unrecognized tag: ' + tagName + ' - More details: https://github.com/marko-js/marko/wiki/Error:-Unrecognized-Tag' message:
"Unrecognized tag: " +
tagName +
" - More details: https://github.com/marko-js/marko/wiki/Error:-Unrecognized-Tag"
}); });
} }
@ -429,7 +448,7 @@ class CompileContext extends EventEmitter {
var elDef; var elDef;
var builder = this.builder; var builder = this.builder;
if (typeof tagName === 'object') { if (typeof tagName === "object") {
elDef = tagName; elDef = tagName;
tagName = elDef.tagName; tagName = elDef.tagName;
attributes = elDef.attributes; attributes = elDef.attributes;
@ -437,39 +456,41 @@ class CompileContext extends EventEmitter {
elDef = { tagName, argument, attributes, openTagOnly, selfClosed }; elDef = { tagName, argument, attributes, openTagOnly, selfClosed };
} }
if (elDef.tagName === '') { if (elDef.tagName === "") {
elDef.tagName = tagName = 'assign'; elDef.tagName = tagName = "assign";
} }
if (!attributes) { if (!attributes) {
attributes = elDef.attributes = []; attributes = elDef.attributes = [];
} else if (typeof attributes === 'object') { } else if (typeof attributes === "object") {
if (!Array.isArray(attributes)) { if (!Array.isArray(attributes)) {
attributes = elDef.attributes = Object.keys(attributes).map((attrName) => { attributes = elDef.attributes = Object.keys(attributes).map(
var attrDef = { attrName => {
name: attrName var attrDef = {
}; name: attrName
};
var val = attributes[attrName]; var val = attributes[attrName];
if (val == null) { if (val != null) {
if (val instanceof Node) {
attrDef.value = val;
} else {
extend(attrDef, val);
}
}
} if (val instanceof Node) { return attrDef;
attrDef.value = val;
} else {
extend(attrDef, val);
} }
);
return attrDef;
});
} }
} else { } else {
throw new Error('Invalid attributes'); throw new Error("Invalid attributes");
} }
var isAtTag = typeof tagName === 'string' && tagName.startsWith('@'); var isAtTag = typeof tagName === "string" && tagName.startsWith("@");
if (isAtTag && this.escapeAtTags) { if (isAtTag && this.escapeAtTags) {
tagName = tagName.replace(/^@/, 'at_'); tagName = tagName.replace(/^@/, "at_");
elDef.tagName = tagName; elDef.tagName = tagName;
} }
@ -489,16 +510,25 @@ class CompileContext extends EventEmitter {
node = builder.customTag(elNode); node = builder.customTag(elNode);
node.body = node.makeContainer(node.body.items); node.body = node.makeContainer(node.body.items);
} else { } else {
if (typeof tagName === 'string') { if (typeof tagName === "string") {
tagDef = taglibLookup.getTag(tagName); tagDef = taglibLookup.getTag(tagName);
if (!tagDef && !this.isMacro(tagName) && tagName.indexOf(':') === -1) { if (
var customElement = htmlElements.getRegisteredElement(tagName, this.dirname); !tagDef &&
!this.isMacro(tagName) &&
tagName.indexOf(":") === -1
) {
var customElement = htmlElements.getRegisteredElement(
tagName,
this.dirname
);
if (customElement) { if (customElement) {
elNode.customElement = customElement; elNode.customElement = customElement;
elNode.addRuntimeFlag(FLAG_CUSTOM_ELEMENT); elNode.addRuntimeFlag(FLAG_CUSTOM_ELEMENT);
if (customElement.import) { if (customElement.import) {
this.addDependency(this.getRequirePath(customElement.import)); this.addDependency(
this.getRequirePath(customElement.import)
);
} }
} else if (!this.ignoreUnrecognizedTags) { } else if (!this.ignoreUnrecognizedTags) {
if (this._parsingFinished) { if (this._parsingFinished) {
@ -520,13 +550,19 @@ class CompileContext extends EventEmitter {
if (nodeFactoryFunc) { if (nodeFactoryFunc) {
var newNode = nodeFactoryFunc(elNode, this); var newNode = nodeFactoryFunc(elNode, this);
if (!(newNode instanceof Node)) { if (!(newNode instanceof Node)) {
throw new Error('Invalid node returned from node factory for tag "' + tagName + '".'); throw new Error(
'Invalid node returned from node factory for tag "' +
tagName +
'".'
);
} }
if (newNode != node) { if (newNode != node) {
// Make sure the body container is associated with the correct node // Make sure the body container is associated with the correct node
if (newNode.body && newNode.body !== node) { if (newNode.body && newNode.body !== node) {
newNode.body = newNode.makeContainer(newNode.body.items); newNode.body = newNode.makeContainer(
newNode.body.items
);
} }
node = newNode; node = newNode;
} }
@ -547,7 +583,7 @@ class CompileContext extends EventEmitter {
var foundAttrs = {}; var foundAttrs = {};
// Validate the attributes // Validate the attributes
attributes.forEach((attr) => { attributes.forEach(attr => {
let attrName = attr.name; let attrName = attr.name;
if (!attrName) { if (!attrName) {
// Attribute will be name for placeholder attributes. For example: <div ${data.myAttrs}> // Attribute will be name for placeholder attributes. For example: <div ${data.myAttrs}>
@ -564,9 +600,15 @@ class CompileContext extends EventEmitter {
//Tag doesn't allow dynamic attributes //Tag doesn't allow dynamic attributes
this.addError({ this.addError({
node: node, node: node,
message: 'The tag "' + tagName + '" in taglib "' + getTaglibPath(tagDef.taglibId) + '" does not support attribute "' + attrName + '"' message:
'The tag "' +
tagName +
'" in taglib "' +
getTaglibPath(tagDef.taglibId) +
'" does not support attribute "' +
attrName +
'"'
}); });
} }
return; return;
} }
@ -588,10 +630,13 @@ class CompileContext extends EventEmitter {
// Add default values for any attributes. If an attribute has a declared // Add default values for any attributes. If an attribute has a declared
// default value and the attribute was not found on the element // default value and the attribute was not found on the element
// then add the attribute with the specified default value // then add the attribute with the specified default value
tagDef.forEachAttribute((attrDef) => { tagDef.forEachAttribute(attrDef => {
var attrName = attrDef.name; var attrName = attrDef.name;
if (attrDef.hasOwnProperty('defaultValue') && !foundAttrs.hasOwnProperty(attrName)) { if (
attrDef.hasOwnProperty("defaultValue") &&
!foundAttrs.hasOwnProperty(attrName)
) {
attributes.push({ attributes.push({
name: attrName, name: attrName,
value: builder.literal(attrDef.defaultValue) value: builder.literal(attrDef.defaultValue)
@ -601,7 +646,14 @@ class CompileContext extends EventEmitter {
if (!foundAttrs.hasOwnProperty(attrName)) { if (!foundAttrs.hasOwnProperty(attrName)) {
this.addError({ this.addError({
node: node, node: node,
message: 'The "' + attrName + '" attribute is required for tag "' + tagName + '" in taglib "' + getTaglibPath(tagDef.taglibId) + '".' message:
'The "' +
attrName +
'" attribute is required for tag "' +
tagName +
'" in taglib "' +
getTaglibPath(tagDef.taglibId) +
'".'
}); });
} }
} }
@ -638,35 +690,45 @@ class CompileContext extends EventEmitter {
} }
importTemplate(relativePath, varName) { importTemplate(relativePath, varName) {
ok(typeof relativePath === 'string', '"path" should be a string'); ok(typeof relativePath === "string", '"path" should be a string');
var builder = this.builder; var builder = this.builder;
varName = varName || removeExt(path.basename(relativePath)) + '_template'; varName =
varName || removeExt(path.basename(relativePath)) + "_template";
var templateVar; var templateVar;
if (this.options.browser || this.options.requireTemplates) { if (this.options.browser || this.options.requireTemplates) {
// When compiling a Marko template for the browser we just use `require('./template.marko')` // When compiling a Marko template for the browser we just use `require('./template.marko')`
templateVar = this.addStaticVar(varName, builder.require(builder.literal(relativePath))); templateVar = this.addStaticVar(
varName,
builder.require(builder.literal(relativePath))
);
} else { } else {
// When compiling a Marko template for the server we just use `loadTemplate(require.resolve('./template.marko'))` // When compiling a Marko template for the server we just use `loadTemplate(require.resolve('./template.marko'))`
let loadTemplateArg = requireResolve(builder, builder.literal(relativePath)); let loadTemplateArg = requireResolve(
let loadFunctionCall = builder.functionCall(this.helper('loadTemplate'), [ loadTemplateArg ]); builder,
builder.literal(relativePath)
);
let loadFunctionCall = builder.functionCall(
this.helper("loadTemplate"),
[loadTemplateArg]
);
templateVar = this.addStaticVar(varName, loadFunctionCall); templateVar = this.addStaticVar(varName, loadFunctionCall);
} }
this.pushMeta('tags', relativePath, true); this.pushMeta("tags", relativePath, true);
return templateVar; return templateVar;
} }
addDependency(path, type, options) { addDependency(path, type) {
var dependency; var dependency;
if (type) { if (type) {
dependency = { type, path }; dependency = { type, path };
} else { } else {
dependency = path; dependency = path;
} }
this.pushMeta('deps', dependency, true); this.pushMeta("deps", dependency, true);
} }
pushMeta(key, value, unique) { pushMeta(key, value, unique) {
@ -674,9 +736,12 @@ class CompileContext extends EventEmitter {
property = this.meta[key]; property = this.meta[key];
if(!property) { if (!property) {
this.meta[key] = [value]; this.meta[key] = [value];
} else if(!unique || !property.some(e => JSON.stringify(e) === JSON.stringify(value))) { } else if (
!unique ||
!property.some(e => JSON.stringify(e) === JSON.stringify(value))
) {
property.push(value); property.push(value);
} }
} }
@ -698,7 +763,10 @@ class CompileContext extends EventEmitter {
} }
isPreserveWhitespace() { isPreserveWhitespace() {
if (this.isFlagSet(FLAG_PRESERVE_WHITESPACE) || this._preserveWhitespace === true) { if (
this.isFlagSet(FLAG_PRESERVE_WHITESPACE) ||
this._preserveWhitespace === true
) {
return true; return true;
} }
} }
@ -721,10 +789,13 @@ class CompileContext extends EventEmitter {
resolvePath(pathExpression) { resolvePath(pathExpression) {
ok(pathExpression, '"pathExpression" is required'); ok(pathExpression, '"pathExpression" is required');
if (pathExpression.type === 'Literal') { if (pathExpression.type === "Literal") {
let path = pathExpression.value; let path = pathExpression.value;
if (typeof path === 'string') { if (typeof path === "string") {
return this.addStaticVar(path, this.builder.requireResolve(pathExpression)); return this.addStaticVar(
path,
this.builder.requireResolve(pathExpression)
);
} }
} }
return pathExpression; return pathExpression;
@ -733,9 +804,9 @@ class CompileContext extends EventEmitter {
resolveTemplate(pathExpression) { resolveTemplate(pathExpression) {
ok(pathExpression, '"pathExpression" is required'); ok(pathExpression, '"pathExpression" is required');
if (pathExpression.type === 'Literal') { if (pathExpression.type === "Literal") {
let path = pathExpression.value; let path = pathExpression.value;
if (typeof path === 'string') { if (typeof path === "string") {
return this.importTemplate(path); return this.importTemplate(path);
} }
} }
@ -748,12 +819,12 @@ class CompileContext extends EventEmitter {
let staticNodes = []; let staticNodes = [];
let staticVars = this.getStaticVars(); let staticVars = this.getStaticVars();
let staticVarNodes = Object.keys(staticVars).map((varName) => { let staticVarNodes = Object.keys(staticVars).map(varName => {
var varInit = staticVars[varName]; var varInit = staticVars[varName];
return builder.variableDeclarator(varName, varInit); return builder.variableDeclarator(varName, varInit);
}); });
if(additionalVars) { if (additionalVars) {
staticVarNodes = additionalVars.concat(staticVarNodes); staticVarNodes = additionalVars.concat(staticVarNodes);
} }
@ -772,8 +843,14 @@ class CompileContext extends EventEmitter {
get helpersIdentifier() { get helpersIdentifier() {
if (!this._helpersIdentifier) { if (!this._helpersIdentifier) {
var target = this.outputType === 'vdom' ? 'marko/runtime/vdom/helpers' : 'marko/runtime/html/helpers'; var target =
this._helpersIdentifier = this.importModule('marko_helpers', target); this.outputType === "vdom"
? "marko/runtime/vdom/helpers"
: "marko/runtime/html/helpers";
this._helpersIdentifier = this.importModule(
"marko_helpers",
target
);
} }
return this._helpersIdentifier; return this._helpersIdentifier;
} }
@ -783,38 +860,50 @@ class CompileContext extends EventEmitter {
if (!helperIdentifier) { if (!helperIdentifier) {
var helperInfo = helpers[name]; var helperInfo = helpers[name];
if (helperInfo && typeof helperInfo === 'object') { if (helperInfo && typeof helperInfo === "object") {
if (!helperInfo.module) { if (!helperInfo.module) {
helperInfo = helperInfo[this.outputType]; helperInfo = helperInfo[this.outputType];
} }
} }
if (!helperInfo) { if (!helperInfo) {
throw new Error('Invalid helper: ' + name); throw new Error("Invalid helper: " + name);
} }
if (typeof helperInfo === 'string') { if (typeof helperInfo === "string") {
let methodName = helperInfo; let methodName = helperInfo;
var methodIdentifier = this.builder.identifier(methodName); var methodIdentifier = this.builder.identifier(methodName);
helperIdentifier = this.addStaticVar( helperIdentifier = this.addStaticVar(
'marko_' + name, "marko_" + name,
this.builder.memberExpression(this.helpersIdentifier, methodIdentifier)); this.builder.memberExpression(
this.helpersIdentifier,
methodIdentifier
)
);
} else if (helperInfo && helperInfo.module) { } else if (helperInfo && helperInfo.module) {
if (helperInfo.method) { if (helperInfo.method) {
let moduleIdentifier = this.importModule('marko_' + helperInfo.module, helperInfo.module); let moduleIdentifier = this.importModule(
"marko_" + helperInfo.module,
helperInfo.module
);
helperIdentifier = this.addStaticVar( helperIdentifier = this.addStaticVar(
'marko_' + name, "marko_" + name,
this.builder.memberExpression(moduleIdentifier, helperInfo.method)); this.builder.memberExpression(
moduleIdentifier,
helperInfo.method
)
);
} else { } else {
helperIdentifier = this.importModule('marko_' + name, helperInfo.module); helperIdentifier = this.importModule(
"marko_" + name,
helperInfo.module
);
} }
} else { } else {
throw new Error('Invalid helper: ' + name); throw new Error("Invalid helper: " + name);
} }
this._helpers[name] = helperIdentifier; this._helpers[name] = helperIdentifier;
} }
@ -844,7 +933,7 @@ class CompileContext extends EventEmitter {
optimize(rootNode) { optimize(rootNode) {
if (this._optimizers) { if (this._optimizers) {
this._optimizers.forEach((optimizer) => { this._optimizers.forEach(optimizer => {
optimizer.optimize(rootNode, this); optimizer.optimize(rootNode, this);
}); });
} }
@ -855,17 +944,17 @@ class CompileContext extends EventEmitter {
} }
isBrowserTarget() { isBrowserTarget() {
return this.target === 'browser'; return this.target === "browser";
} }
isServerTarget() { isServerTarget() {
return this.target === 'server'; return this.target === "server";
} }
} }
CompileContext.prototype.util = { CompileContext.prototype.util = {
isValidJavaScriptIdentifier: require('./util/isValidJavaScriptIdentifier'), isValidJavaScriptIdentifier: require("./util/isValidJavaScriptIdentifier"),
isJavaScriptReservedWord: require('./util/isJavaScriptReservedWord') isJavaScriptReservedWord: require("./util/isJavaScriptReservedWord")
}; };
module.exports = CompileContext; module.exports = CompileContext;

View File

@ -1,4 +1,4 @@
'use strict'; "use strict";
class CompileError { class CompileError {
constructor(errorInfo, context) { constructor(errorInfo, context) {
@ -33,16 +33,16 @@ class CompileError {
toString() { toString() {
var pos = this.pos; var pos = this.pos;
if (pos) { if (pos) {
pos = '[' + pos + '] '; pos = "[" + pos + "] ";
} else { } else {
pos = ''; pos = "";
} }
var str = pos + this.message; var str = pos + this.message;
if (pos == null && this.node) { if (pos == null && this.node) {
str += ' (' + this.node.toString() + ')'; str += " (" + this.node.toString() + ")";
} }
return str; return str;
} }
} }
module.exports = CompileError; module.exports = CompileError;

View File

@ -1,18 +1,20 @@
'use strict'; "use strict";
var ok = require('assert').ok; var ok = require("assert").ok;
var path = require('path'); var path = require("path");
var CodeGenerator = require('./CodeGenerator'); var CodeGenerator = require("./CodeGenerator");
var CodeWriter = require('./CodeWriter'); var CodeWriter = require("./CodeWriter");
var createError = require('raptor-util/createError'); var createError = require("raptor-util/createError");
var resolveDep = require('../components/legacy/dependencies').resolveDep; var resolveDep = require("../components/legacy/dependencies").resolveDep;
const FLAG_TRANSFORMER_APPLIED = 'transformerApply'; const FLAG_TRANSFORMER_APPLIED = "transformerApply";
function transformNode(node, context) { function transformNode(node, context) {
try { try {
context.taglibLookup.forEachNodeTransformer(node, function (transformer) { context.taglibLookup.forEachNodeTransformer(node, function(
transformer
) {
if (node.isDetached()) { if (node.isDetached()) {
return; //The node might have been removed from the tree return; //The node might have been removed from the tree
} }
if (!node.isTransformerApplied(transformer)) { if (!node.isTransformerApplied(transformer)) {
//Check to make sure a transformer of a certain type is only applied once to a node //Check to make sure a transformer of a certain type is only applied once to a node
@ -24,11 +26,19 @@ function transformNode(node, context) {
//Set the flag to indicate that a node was transformed //Set the flag to indicate that a node was transformed
// node.compiler = this; // node.compiler = this;
var transformerFunc = transformer.getFunc(); var transformerFunc = transformer.getFunc();
transformerFunc.call(transformer, node, context); //Have the transformer process the node (NOTE: Just because a node is being processed by the transformer doesn't mean that it has to modify the parse tree) transformerFunc.call(transformer, node, context); //Have the transformer process the node (NOTE: Just because a node is being processed by the transformer doesn't mean that it has to modify the parse tree)
} }
}); });
} catch (e) { } catch (e) {
throw createError(new Error('Unable to compile template at path "' + context.filename + '". Error: ' + e.message), e); throw createError(
new Error(
'Unable to compile template at path "' +
context.filename +
'". Error: ' +
e.message
),
e
);
} }
} }
@ -43,14 +53,13 @@ function transformTreeHelper(node, context) {
* The checks to prevent transformers from being applied multiple times makes * The checks to prevent transformers from being applied multiple times makes
* sure that this is not a problem. * sure that this is not a problem.
*/ */
node.forEachChild(function (childNode) { node.forEachChild(function(childNode) {
transformTreeHelper(childNode, context); transformTreeHelper(childNode, context);
}); });
} }
function transformTree(rootNode, context) { function transformTree(rootNode, context) {
context.taglibLookup.forEachTemplateTransformer(transformer => {
context.taglibLookup.forEachTemplateTransformer((transformer) => {
var transformFunc = transformer.getFunc(); var transformFunc = transformer.getFunc();
rootNode = transformFunc(rootNode, context) || rootNode; rootNode = transformFunc(rootNode, context) || rootNode;
}); });
@ -63,7 +72,7 @@ function transformTree(rootNode, context) {
do { do {
context.clearFlag(FLAG_TRANSFORMER_APPLIED); context.clearFlag(FLAG_TRANSFORMER_APPLIED);
//Reset the flag to indicate that no transforms were yet applied to any of the nodes for this pass //Reset the flag to indicate that no transforms were yet applied to any of the nodes for this pass
transformTreeHelper(rootNode, context); //Run the transforms on the tree transformTreeHelper(rootNode, context); //Run the transforms on the tree
} while (context.isFlagSet(FLAG_TRANSFORMER_APPLIED)); } while (context.isFlagSet(FLAG_TRANSFORMER_APPLIED));
return rootNode; return rootNode;
@ -74,10 +83,13 @@ function handleErrors(context) {
if (context.hasErrors()) { if (context.hasErrors()) {
var errors = context.getErrors(); var errors = context.getErrors();
var message = 'An error occurred while trying to compile template at path "' + context.filename + '". Error(s) in template:\n'; var message =
'An error occurred while trying to compile template at path "' +
context.filename +
'". Error(s) in template:\n';
for (var i = 0, len = errors.length; i < len; i++) { for (var i = 0, len = errors.length; i < len; i++) {
let error = errors[i]; let error = errors[i];
message += (i + 1) + ') ' + error.toString() + '\n'; message += i + 1 + ") " + error.toString() + "\n";
} }
var error = new Error(message); var error = new Error(message);
error.errors = errors; error.errors = errors;
@ -86,7 +98,7 @@ function handleErrors(context) {
} }
class CompiledTemplate { class CompiledTemplate {
constructor(ast, context, codeGenerator) { constructor(ast, context) {
this.ast = ast; this.ast = ast;
this.context = context; this.context = context;
this.filename = context.filename; this.filename = context.filename;
@ -111,7 +123,10 @@ class CompiledTemplate {
handleErrors(this.context); handleErrors(this.context);
// console.log(module.id, 'FINAL AST:' + JSON.stringify(finalAST, null, 4)); // console.log(module.id, 'FINAL AST:' + JSON.stringify(finalAST, null, 4));
var codeWriter = new CodeWriter(this.context.options, this.context.builder); var codeWriter = new CodeWriter(
this.context.options,
this.context.builder
);
codeWriter.write(this.ast); codeWriter.write(this.ast);
handleErrors(this.context); handleErrors(this.context);
@ -123,7 +138,7 @@ class CompiledTemplate {
} }
class Compiler { class Compiler {
constructor(options, userOptions, inline) { constructor(options) {
ok(options, '"options" is required'); ok(options, '"options" is required');
this.builder = options.builder; this.builder = options.builder;
@ -134,7 +149,7 @@ class Compiler {
} }
compile(src, context) { compile(src, context) {
ok(typeof src === 'string', '"src" argument should be a string'); ok(typeof src === "string", '"src" argument should be a string');
var codeGenerator = new CodeGenerator(context); var codeGenerator = new CodeGenerator(context);
@ -143,11 +158,14 @@ class Compiler {
context._parsingFinished = true; context._parsingFinished = true;
if (!context.ignoreUnrecognizedTags && context.unrecognizedTags) { if (!context.ignoreUnrecognizedTags && context.unrecognizedTags) {
for(let i=0; i<context.unrecognizedTags.length; i++) { for (let i = 0; i < context.unrecognizedTags.length; i++) {
let unrecognizedTag = context.unrecognizedTags[i]; let unrecognizedTag = context.unrecognizedTags[i];
// See if the tag is a macro // See if the tag is a macro
if (!context.isMacro(unrecognizedTag.tagName)) { if (!context.isMacro(unrecognizedTag.tagName)) {
context.addErrorUnrecognizedTag(unrecognizedTag.tagName, unrecognizedTag.node); context.addErrorUnrecognizedTag(
unrecognizedTag.tagName,
unrecognizedTag.node
);
} }
} }
} }

View File

@ -1,9 +1,10 @@
'use strict'; "use strict";
var htmljs = require('htmljs-parser'); var htmljs = require("htmljs-parser");
class HtmlJsParser { class HtmlJsParser {
constructor(options) { constructor(options) {
this.ignorePlaceholders = options && options.ignorePlaceholders === true; this.ignorePlaceholders =
options && options.ignorePlaceholders === true;
} }
parse(src, handlers, filename) { parse(src, handlers, filename) {
@ -15,27 +16,29 @@ class HtmlJsParser {
onPlaceholder(event) { onPlaceholder(event) {
if (event.withinBody) { if (event.withinBody) {
if (!event.withinString) { if (!event.withinString) {
handlers.handleBodyTextPlaceholder(event.value, event.escape); handlers.handleBodyTextPlaceholder(
event.value,
event.escape
);
} }
} else if (event.withinOpenTag) { } else if (event.withinOpenTag) {
// Don't escape placeholder for dynamic attributes. For example: <div ${data.myAttrs}></div> // Don't escape placeholder for dynamic attributes. For example: <div ${data.myAttrs}></div>
} else { } else {
// placeholder within attribute // placeholder within attribute
if (event.escape) { if (event.escape) {
event.value = '$escapeXml(' + event.value + ')'; event.value = "$escapeXml(" + event.value + ")";
} else { } else {
event.value = '$noEscapeXml(' + event.value + ')'; event.value = "$noEscapeXml(" + event.value + ")";
} }
} }
// placeholder within content // placeholder within content
}, },
onCDATA(event) { onCDATA(event) {
handlers.handleCharacters(event.value, 'static-text'); handlers.handleCharacters(event.value, "static-text");
}, },
onOpenTagName(event, parser) { onOpenTagName(event) {
event.selfClosed = false; // Don't allow self-closed tags event.selfClosed = false; // Don't allow self-closed tags
var tagParseOptions = handlers.getTagParseOptions(event); var tagParseOptions = handlers.getTagParseOptions(event);
@ -61,7 +64,6 @@ class HtmlJsParser {
}, },
onDocumentType(event) { onDocumentType(event) {
// Document type: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd // Document type: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd
// NOTE: The value will be all of the text between "<!" and ">"" // NOTE: The value will be all of the text between "<!" and ">""
handlers.handleDocumentType(event.value); handlers.handleDocumentType(event.value);
@ -86,12 +88,12 @@ class HtmlJsParser {
} }
}; };
var parser = this.parser = htmljs.createParser(listeners, { var parser = (this.parser = htmljs.createParser(listeners, {
ignorePlaceholders: this.ignorePlaceholders, ignorePlaceholders: this.ignorePlaceholders,
isOpenTagOnly: function(tagName) { isOpenTagOnly: function(tagName) {
return handlers.isOpenTagOnly(tagName); return handlers.isOpenTagOnly(tagName);
} }
}); }));
parser.parse(src, filename); parser.parse(src, filename);
} }
} }

View File

@ -1,28 +1,28 @@
'use strict'; "use strict";
let CodeWriter = require('./CodeWriter'); let CodeWriter = require("./CodeWriter");
function fixIndentation(lines) { function fixIndentation(lines) {
let length = lines.length; let length = lines.length;
let startLine = 0; let startLine = 0;
let endLine = length; let endLine = length;
for (; startLine<length; startLine++) { for (; startLine < length; startLine++) {
let line = lines[startLine]; let line = lines[startLine];
if (line.trim() !== '') { if (line.trim() !== "") {
break; break;
} }
} }
for (; endLine>startLine; endLine--) { for (; endLine > startLine; endLine--) {
let line = lines[endLine-1]; let line = lines[endLine - 1];
if (line.trim() !== '') { if (line.trim() !== "") {
break; break;
} }
} }
if (endLine === startLine) { if (endLine === startLine) {
return ''; return "";
} }
if (startLine !== 0 || endLine !== length) { if (startLine !== 0 || endLine !== length) {
@ -33,7 +33,7 @@ function fixIndentation(lines) {
let indentToRemove = /^\s*/.exec(firstLine)[0]; let indentToRemove = /^\s*/.exec(firstLine)[0];
if (indentToRemove) { if (indentToRemove) {
for (let i=0; i<lines.length; i++) { for (let i = 0; i < lines.length; i++) {
let line = lines[i]; let line = lines[i];
if (line.startsWith(indentToRemove)) { if (line.startsWith(indentToRemove)) {
lines[i] = line.substring(indentToRemove.length); lines[i] = line.substring(indentToRemove.length);
@ -41,13 +41,13 @@ function fixIndentation(lines) {
} }
} }
return lines.join('\n'); return lines.join("\n");
} }
function normalizeTemplateSrc(src) { function normalizeTemplateSrc(src) {
let lines = src.split(/\r\n|\n\r|\n/); let lines = src.split(/\r\n|\n\r|\n/);
if (lines.length) { if (lines.length) {
if (lines[0].trim() === '') { if (lines[0].trim() === "") {
return fixIndentation(lines); return fixIndentation(lines);
} }
} }
@ -82,4 +82,4 @@ class InlineCompiler {
} }
} }
module.exports = InlineCompiler; module.exports = InlineCompiler;

View File

@ -1,13 +1,13 @@
'use strict'; "use strict";
var ok = require('assert').ok; var ok = require("assert").ok;
var replacePlaceholderEscapeFuncs = require('./util/replacePlaceholderEscapeFuncs'); var replacePlaceholderEscapeFuncs = require("./util/replacePlaceholderEscapeFuncs");
var extend = require('raptor-util/extend'); var extend = require("raptor-util/extend");
var COMPILER_ATTRIBUTE_HANDLERS = { var COMPILER_ATTRIBUTE_HANDLERS = {
'preserve-whitespace': function(attr, context) { "preserve-whitespace": function(attr, context) {
context.setPreserveWhitespace(true); context.setPreserveWhitespace(true);
}, },
'preserve-comments': function(attr, context) { "preserve-comments": function(attr, context) {
context.setPreserveComments(true); context.setPreserveComments(true);
} }
}; };
@ -20,11 +20,11 @@ function isIEConditionalComment(comment) {
function mergeShorthandClassNames(el, shorthandClassNames, context) { function mergeShorthandClassNames(el, shorthandClassNames, context) {
var builder = context.builder; var builder = context.builder;
let classNames = shorthandClassNames.map((className) => { let classNames = shorthandClassNames.map(className => {
return builder.parseExpression(className.value); return builder.parseExpression(className.value);
}); });
var classAttr = el.getAttributeValue('class'); var classAttr = el.getAttributeValue("class");
if (classAttr) { if (classAttr) {
classNames.push(classAttr); classNames.push(classAttr);
} }
@ -33,10 +33,14 @@ function mergeShorthandClassNames(el, shorthandClassNames, context) {
var finalClassNames = []; var finalClassNames = [];
for (var i=0; i<classNames.length; i++) { for (var i = 0; i < classNames.length; i++) {
let className = classNames[i]; let className = classNames[i];
if (prevClassName && className.type === 'Literal' && prevClassName.type === 'Literal') { if (
prevClassName.value += ' ' + className.value; prevClassName &&
className.type === "Literal" &&
prevClassName.type === "Literal"
) {
prevClassName.value += " " + className.value;
} else { } else {
finalClassNames.push(className); finalClassNames.push(className);
prevClassName = className; prevClassName = className;
@ -44,47 +48,48 @@ function mergeShorthandClassNames(el, shorthandClassNames, context) {
} }
if (finalClassNames.length === 1) { if (finalClassNames.length === 1) {
el.setAttributeValue('class', finalClassNames[0]); el.setAttributeValue("class", finalClassNames[0]);
} else { } else {
el.setAttributeValue( el.setAttributeValue(
'class', "class",
builder.functionCall( builder.functionCall(context.helper("classList"), [
context.helper('classList'), builder.literal(finalClassNames)
[ ])
builder.literal(finalClassNames) );
]));
} }
} }
function getParserStateForTag(parser, el, tagDef) { function getParserStateForTag(parser, el, tagDef) {
var attributes = el.attributes; var attributes = el.attributes;
if (attributes) { if (attributes) {
for (var i=0; i<attributes.length; i++) { for (var i = 0; i < attributes.length; i++) {
var attr = attributes[i]; var attr = attributes[i];
var attrName = attr.name; var attrName = attr.name;
if (attrName === 'marko-body') { if (attrName === "marko-body") {
var parseMode; var parseMode;
if (attr.literalValue) { if (attr.literalValue) {
parseMode = attr.literalValue; parseMode = attr.literalValue;
} }
if (parseMode === 'static-text' || if (
parseMode === 'parsed-text' || parseMode === "static-text" ||
parseMode === 'html') { parseMode === "parsed-text" ||
parseMode === "html"
) {
return parseMode; return parseMode;
} else { } else {
parser.context.addError({ parser.context.addError({
message: 'Value for "marko-body" should be one of the following: "static-text", "parsed-text", "html"', message:
code: 'ERR_INVALID_ATTR' 'Value for "marko-body" should be one of the following: "static-text", "parsed-text", "html"',
code: "ERR_INVALID_ATTR"
}); });
return; return;
} }
} else if (attrName === 'template-helpers') { } else if (attrName === "template-helpers") {
return 'static-text'; return "static-text";
} else if (attrName === 'marko-init') { } else if (attrName === "marko-init") {
return 'static-text'; return "static-text";
} }
} }
} }
@ -122,7 +127,7 @@ class Parser {
} }
parse(src, context) { parse(src, context) {
ok(typeof src === 'string', '"src" should be a string'); ok(typeof src === "string", '"src" should be a string');
ok(context, '"context" is required'); ok(context, '"context" is required');
this._reset(); this._reset();
@ -144,12 +149,16 @@ class Parser {
handleCharacters(text, parseMode) { handleCharacters(text, parseMode) {
var builder = this.context.builder; var builder = this.context.builder;
var escape = parseMode !== 'html'; var escape = parseMode !== "html";
// NOTE: If parseMode is 'static-text' or 'parsed-text' then that means that special // NOTE: If parseMode is 'static-text' or 'parsed-text' then that means that special
// HTML characters may not have been escaped on the way in so we need to escape // HTML characters may not have been escaped on the way in so we need to escape
// them on the way out // them on the way out
if (this.prevTextNode && this.prevTextNode.isLiteral() && this.prevTextNode.escape === escape) { if (
this.prevTextNode &&
this.prevTextNode.isLiteral() &&
this.prevTextNode.escape === escape
) {
this.prevTextNode.argument.value += text; this.prevTextNode.argument.value += text;
} else { } else {
this.prevTextNode = builder.text(builder.literal(text), escape); this.prevTextNode = builder.text(builder.literal(text), escape);
@ -175,17 +184,23 @@ class Parser {
if (!raw) { if (!raw) {
if (tagNameExpression) { if (tagNameExpression) {
tagName = builder.parseExpression(tagNameExpression); tagName = builder.parseExpression(tagNameExpression);
} else if (tagName === 'marko-compiler-options') { } else if (tagName === "marko-compiler-options") {
this.parentNode.setTrimStartEnd(true); this.parentNode.setTrimStartEnd(true);
attributes.forEach(function (attr) { attributes.forEach(function(attr) {
let attrName = attr.name; let attrName = attr.name;
let handler = COMPILER_ATTRIBUTE_HANDLERS[attrName]; let handler = COMPILER_ATTRIBUTE_HANDLERS[attrName];
if (!handler) { if (!handler) {
context.addError({ context.addError({
code: 'ERR_INVALID_COMPILER_OPTION', code: "ERR_INVALID_COMPILER_OPTION",
message: 'Invalid Marko compiler option of "' + attrName + '". Allowed: ' + Object.keys(COMPILER_ATTRIBUTE_HANDLERS).join(', '), message:
'Invalid Marko compiler option of "' +
attrName +
'". Allowed: ' +
Object.keys(COMPILER_ATTRIBUTE_HANDLERS).join(
", "
),
pos: el.pos, pos: el.pos,
node: el node: el
}); });
@ -205,21 +220,24 @@ class Parser {
var attributeParseErrors = []; var attributeParseErrors = [];
// <div class="foo"> -> "div class=foo" // <div class="foo"> -> "div class=foo"
var tagString = parser.substring(el.pos, el.endPos) var tagString = parser
.replace(/^<|\/>$|>$/g, "").trim(); .substring(el.pos, el.endPos)
.replace(/^<|\/>$|>$/g, "")
.trim();
var shouldParsedAttributes = !tagDef || tagDef.parseAttributes !== false; var shouldParsedAttributes =
!tagDef || tagDef.parseAttributes !== false;
var parsedAttributes = []; var parsedAttributes = [];
if (shouldParsedAttributes) { if (shouldParsedAttributes) {
attributes.forEach((attr) => { attributes.forEach(attr => {
var attrName = attr.name; var attrName = attr.name;
var attrRawValue = attr.value; var attrRawValue = attr.value;
var attrSpread; var attrSpread;
var attrValue; var attrValue;
if (attr.hasOwnProperty('literalValue')) { if (attr.hasOwnProperty("literalValue")) {
attrValue = builder.literal(attr.literalValue); attrValue = builder.literal(attr.literalValue);
} else if (/^\.\.\./.test(attrName)) { } else if (/^\.\.\./.test(attrName)) {
attrRawValue = attrName; attrRawValue = attrName;
@ -232,27 +250,34 @@ class Parser {
attrValue = attrRawValue; attrValue = attrRawValue;
} }
if (typeof attrValue === 'string') { if (typeof attrValue === "string") {
let parsedExpression; let parsedExpression;
let valid = true; let valid = true;
try { try {
parsedExpression = builder.parseExpression(attrValue); parsedExpression = builder.parseExpression(attrValue);
} catch(e) { } catch (e) {
if (shouldParsedAttributes) { if (shouldParsedAttributes) {
valid = false; valid = false;
attributeParseErrors.push('Invalid JavaScript expression for attribute "' + attr.name + '": ' + e); attributeParseErrors.push(
'Invalid JavaScript expression for attribute "' +
attr.name +
'": ' +
e
);
} else { } else {
// Attribute failed to parse. Skip it... // Attribute failed to parse. Skip it...
return; return;
} }
} }
if (valid) { if (valid) {
if (raw) { if (raw) {
attrValue = parsedExpression; attrValue = parsedExpression;
} else { } else {
attrValue = replacePlaceholderEscapeFuncs(parsedExpression, context); attrValue = replacePlaceholderEscapeFuncs(
parsedExpression,
context
);
} }
} else { } else {
attrValue = null; attrValue = null;
@ -275,7 +300,12 @@ class Parser {
} }
if (attrName) { if (attrName) {
if (attrName === 'for-key' || attrName === 'for-ref' || attrName === 'w-for' || attrName.endsWith(':key')) { if (
attrName === "for-key" ||
attrName === "for-ref" ||
attrName === "w-for" ||
attrName.endsWith(":key")
) {
context.data.hasLegacyForKey = true; context.data.hasLegacyForKey = true;
} }
} }
@ -312,8 +342,7 @@ class Parser {
} }
if (attributeParseErrors.length) { if (attributeParseErrors.length) {
attributeParseErrors.forEach(e => {
attributeParseErrors.forEach((e) => {
context.addError(node, e); context.addError(node, e);
}); });
} }
@ -325,10 +354,12 @@ class Parser {
} }
if (el.shorthandClassNames) { if (el.shorthandClassNames) {
node.rawShorthandClassNames = el.shorthandClassNames.map((className) => { node.rawShorthandClassNames = el.shorthandClassNames.map(
let parsed = builder.parseExpression(className.value); className => {
return parsed.value; let parsed = builder.parseExpression(className.value);
}); return parsed.value;
}
);
} }
} else { } else {
if (el.shorthandClassNames) { if (el.shorthandClassNames) {
@ -336,10 +367,16 @@ class Parser {
} }
if (el.shorthandId) { if (el.shorthandId) {
if (node.hasAttribute('id')) { if (node.hasAttribute("id")) {
context.addError(node, 'A shorthand ID cannot be used in conjunction with the "id" attribute'); context.addError(
node,
'A shorthand ID cannot be used in conjunction with the "id" attribute'
);
} else { } else {
node.setAttributeValue('id', builder.parseExpression(el.shorthandId.value)); node.setAttributeValue(
"id",
builder.parseExpression(el.shorthandId.value)
);
} }
} }
} }
@ -354,7 +391,7 @@ class Parser {
handleEndElement(elementName) { handleEndElement(elementName) {
if (this.raw !== true) { if (this.raw !== true) {
if (elementName === 'marko-compiler-options') { if (elementName === "marko-compiler-options") {
return; return;
} }
} }
@ -368,7 +405,8 @@ class Parser {
var builder = this.context.builder; var builder = this.context.builder;
var preserveComment = this.context.isPreserveComments() || var preserveComment =
this.context.isPreserveComments() ||
isIEConditionalComment(comment); isIEConditionalComment(comment);
if (this.raw || preserveComment) { if (this.raw || preserveComment) {
@ -422,7 +460,7 @@ class Parser {
} }
get parentNode() { get parentNode() {
var last = this.stack[this.stack.length-1]; var last = this.stack[this.stack.length - 1];
return last.node; return last.node;
} }

View File

@ -1,6 +1,6 @@
'use strict'; "use strict";
var isArray = Array.isArray; var isArray = Array.isArray;
var Container = require('./ast/Container'); var Container = require("./ast/Container");
function noop() {} function noop() {}
@ -53,7 +53,7 @@ class Walker {
}); });
if (hasRemoval) { if (hasRemoval) {
for (let i=array.length-1; i>=0; i--) { for (let i = array.length - 1; i >= 0; i--) {
if (array[i] == null) { if (array[i] == null) {
array.splice(i, 1); array.splice(i, 1);
} }
@ -66,7 +66,7 @@ class Walker {
} }
_walkContainer(nodes) { _walkContainer(nodes) {
nodes.forEach((node) => { nodes.forEach(node => {
var transformed = this.walk(node); var transformed = this.walk(node);
if (!transformed) { if (!transformed) {
node.container.removeChild(node); node.container.removeChild(node);
@ -77,13 +77,15 @@ class Walker {
} }
walk(node) { walk(node) {
if (!node || this._stopped || typeof node === 'string') { if (!node || this._stopped || typeof node === "string") {
return node; return node;
} }
this._reset(); this._reset();
var parent = this._stack.length ? this._stack[this._stack.length - 1] : undefined; var parent = this._stack.length
? this._stack[this._stack.length - 1]
: undefined;
this._stack.push(node); this._stack.push(node);
@ -149,4 +151,3 @@ class Walker {
} }
module.exports = Walker; module.exports = Walker;

View File

@ -1,8 +1,8 @@
'use strict'; "use strict";
var ok = require('assert').ok; var ok = require("assert").ok;
var isArray = Array.isArray; var isArray = Array.isArray;
var Container = require('./Container'); var Container = require("./Container");
class ArrayContainer extends Container { class ArrayContainer extends Container {
constructor(node, array) { constructor(node, array) {
@ -12,7 +12,7 @@ class ArrayContainer extends Container {
forEach(callback, thisObj) { forEach(callback, thisObj) {
var array = this.array.concat([]); var array = this.array.concat([]);
for (var i=0; i<array.length; i++) { for (var i = 0; i < array.length; i++) {
var item = array[i]; var item = array[i];
if (item.container === this) { if (item.container === this) {
callback.call(thisObj, item, i); callback.call(thisObj, item, i);
@ -25,7 +25,7 @@ class ArrayContainer extends Container {
var array = this.array; var array = this.array;
var len = array.length; var len = array.length;
for (var i=0; i<len; i++) { for (var i = 0; i < len; i++) {
var curChild = array[i]; var curChild = array[i];
if (curChild === oldChild) { if (curChild === oldChild) {
array[i] = newChild; array[i] = newChild;
@ -68,11 +68,11 @@ class ArrayContainer extends Container {
insertChildBefore(newChild, referenceNode) { insertChildBefore(newChild, referenceNode) {
ok(newChild, '"newChild" is required"'); ok(newChild, '"newChild" is required"');
ok(referenceNode, 'Invalid reference child'); ok(referenceNode, "Invalid reference child");
var array = this.array; var array = this.array;
var len = array.length; var len = array.length;
for (var i=0; i<len; i++) { for (var i = 0; i < len; i++) {
var curChild = array[i]; var curChild = array[i];
if (curChild === referenceNode) { if (curChild === referenceNode) {
array.splice(i, 0, newChild); array.splice(i, 0, newChild);
@ -81,33 +81,36 @@ class ArrayContainer extends Container {
} }
} }
throw new Error('Reference node not found'); throw new Error("Reference node not found");
} }
insertChildAfter(newChild, referenceNode) { insertChildAfter(newChild, referenceNode) {
ok(newChild, '"newChild" is required"'); ok(newChild, '"newChild" is required"');
ok(referenceNode, 'Invalid reference child'); ok(referenceNode, "Invalid reference child");
var array = this.array; var array = this.array;
var len = array.length; var len = array.length;
for (var i=0; i<len; i++) { for (var i = 0; i < len; i++) {
var curChild = array[i]; var curChild = array[i];
if (curChild === referenceNode) { if (curChild === referenceNode) {
array.splice(i+1, 0, newChild); array.splice(i + 1, 0, newChild);
newChild.container = this; newChild.container = this;
return; return;
} }
} }
throw new Error('Reference node not found'); throw new Error("Reference node not found");
} }
moveChildrenTo(target) { moveChildrenTo(target) {
ok(target.appendChild, 'Node does not support appendChild(node): ' + target); ok(
target.appendChild,
"Node does not support appendChild(node): " + target
);
var array = this.array; var array = this.array;
var len = array.length; var len = array.length;
for (var i=0; i<len; i++) { for (var i = 0; i < len; i++) {
var curChild = array[i]; var curChild = array[i];
curChild.container = null; // Detach the child from this container curChild.container = null; // Detach the child from this container
target.appendChild(curChild); target.appendChild(curChild);
@ -118,50 +121,48 @@ class ArrayContainer extends Container {
getPreviousSibling(node) { getPreviousSibling(node) {
if (node.container !== this) { if (node.container !== this) {
throw new Error('Node does not belong to container: ' + node); throw new Error("Node does not belong to container: " + node);
} }
var array = this.array; var array = this.array;
for (var i = 0; i < array.length; i++) {
for (var i=0; i<array.length; i++) {
var curNode = array[i]; var curNode = array[i];
if (curNode.container !== this) { if (curNode.container !== this) {
continue; continue;
} }
if (curNode === node) { if (curNode === node) {
return i-1 >= 0 ? array[i-1] : undefined; return i - 1 >= 0 ? array[i - 1] : undefined;
} }
} }
} }
getNextSibling(node) { getNextSibling(node) {
if (node.container !== this) { if (node.container !== this) {
throw new Error('Node does not belong to container: ' + node); throw new Error("Node does not belong to container: " + node);
} }
var array = this.array; var array = this.array;
for (var i=0; i<array.length; i++) { for (var i = 0; i < array.length; i++) {
var curNode = array[i]; var curNode = array[i];
if (curNode.container !== this) { if (curNode.container !== this) {
continue; continue;
} }
if (curNode === node) { if (curNode === node) {
return i+1 < array.length ? array[i+1] : undefined; return i + 1 < array.length ? array[i + 1] : undefined;
} }
} }
} }
forEachNextSibling(node, callback, thisObj) { forEachNextSibling(node, callback, thisObj) {
if (node.container !== this) { if (node.container !== this) {
throw new Error('Node does not belong to container: ' + node); throw new Error("Node does not belong to container: " + node);
} }
var array = this.array.concat([]); var array = this.array.concat([]);
var found = false; var found = false;
for (var i=0; i<array.length; i++) { for (var i = 0; i < array.length; i++) {
var curNode = array[i]; var curNode = array[i];
if (curNode.container !== this) { if (curNode.container !== this) {
continue; continue;
@ -194,9 +195,9 @@ class ArrayContainer extends Container {
set items(newItems) { set items(newItems) {
if (newItems) { if (newItems) {
ok(isArray(newItems), 'Invalid array'); ok(isArray(newItems), "Invalid array");
for (let i=0; i<newItems.length; i++) { for (let i = 0; i < newItems.length; i++) {
newItems[i].container = this; newItems[i].container = this;
} }
} }

View File

@ -1,10 +1,10 @@
'use strict'; "use strict";
var Node = require('./Node'); var Node = require("./Node");
class ArrayExpression extends Node { class ArrayExpression extends Node {
constructor(def) { constructor(def) {
super('ArrayExpression'); super("ArrayExpression");
this.elements = def.elements; this.elements = def.elements;
} }
@ -17,12 +17,12 @@ class ArrayExpression extends Node {
var elements = this.elements; var elements = this.elements;
if (!elements || !elements.length) { if (!elements || !elements.length) {
writer.write('[]'); writer.write("[]");
return; return;
} }
writer.incIndent(); writer.incIndent();
writer.write('[\n'); writer.write("[\n");
writer.incIndent(); writer.incIndent();
elements.forEach((element, i) => { elements.forEach((element, i) => {
@ -30,15 +30,15 @@ class ArrayExpression extends Node {
writer.write(element); writer.write(element);
if (i < elements.length - 1) { if (i < elements.length - 1) {
writer.write(',\n'); writer.write(",\n");
} else { } else {
writer.write('\n'); writer.write("\n");
} }
}); });
writer.decIndent(); writer.decIndent();
writer.writeLineIndent(); writer.writeLineIndent();
writer.write(']'); writer.write("]");
writer.decIndent(); writer.decIndent();
} }
@ -48,25 +48,25 @@ class ArrayExpression extends Node {
toJSON() { toJSON() {
return { return {
type: 'ArrayExpression', type: "ArrayExpression",
elements: this.elements elements: this.elements
}; };
} }
toString() { toString() {
var result = '['; var result = "[";
var elements = this.elements; var elements = this.elements;
if (elements) { if (elements) {
elements.forEach((element, i) => { elements.forEach((element, i) => {
if (i !== 0) { if (i !== 0) {
result += ', '; result += ", ";
} }
result += element.toString(); result += element.toString();
}); });
} }
return result + ']'; return result + "]";
} }
} }
module.exports = ArrayExpression; module.exports = ArrayExpression;

View File

@ -1,10 +1,10 @@
'use strict'; "use strict";
var Node = require('./Node'); var Node = require("./Node");
class Assignment extends Node { class Assignment extends Node {
constructor(def) { constructor(def) {
super('Assignment'); super("Assignment");
this.left = def.left; this.left = def.left;
this.right = def.right; this.right = def.right;
this.operator = def.operator; this.operator = def.operator;
@ -22,18 +22,18 @@ class Assignment extends Node {
var operator = this.operator; var operator = this.operator;
writer.write(left); writer.write(left);
writer.write(' ' + (operator || '=') + ' '); writer.write(" " + (operator || "=") + " ");
var wrap = right instanceof Assignment; var wrap = right instanceof Assignment;
if (wrap) { if (wrap) {
writer.write('('); writer.write("(");
} }
writer.write(right); writer.write(right);
if (wrap) { if (wrap) {
writer.write(')'); writer.write(")");
} }
} }
@ -58,22 +58,22 @@ class Assignment extends Node {
var right = this.right; var right = this.right;
var operator = this.operator; var operator = this.operator;
var result = left.toString() + ' ' + (operator || '=') + ' '; var result = left.toString() + " " + (operator || "=") + " ";
var wrap = right instanceof Assignment; var wrap = right instanceof Assignment;
if (wrap) { if (wrap) {
result += '('; result += "(";
} }
result += right.toString(); result += right.toString();
if (wrap) { if (wrap) {
result += ')'; result += ")";
} }
return result; return result;
} }
} }
module.exports = Assignment; module.exports = Assignment;

View File

@ -1,10 +1,10 @@
'use strict'; "use strict";
var Node = require('./Node'); var Node = require("./Node");
class AttributePlaceholder extends Node { class AttributePlaceholder extends Node {
constructor(def) { constructor(def) {
super('AttributePlaceholder'); super("AttributePlaceholder");
this.value = def.value; this.value = def.value;
this.escape = def.escape; this.escape = def.escape;
} }
@ -34,4 +34,4 @@ class AttributePlaceholder extends Node {
} }
} }
module.exports = AttributePlaceholder; module.exports = AttributePlaceholder;

View File

@ -1,35 +1,35 @@
'use strict'; "use strict";
var Node = require('./Node'); var Node = require("./Node");
var isCompoundExpression = require('../util/isCompoundExpression'); var isCompoundExpression = require("../util/isCompoundExpression");
function writeCodeForOperand(node, writer) { function writeCodeForOperand(node, writer) {
var wrap = isCompoundExpression(node); var wrap = isCompoundExpression(node);
if (wrap) { if (wrap) {
writer.write('('); writer.write("(");
} }
writer.write(node); writer.write(node);
if (wrap) { if (wrap) {
writer.write(')'); writer.write(")");
} }
} }
function operandToString(node) { function operandToString(node) {
var wrap = isCompoundExpression(node); var wrap = isCompoundExpression(node);
var result = ''; var result = "";
if (wrap) { if (wrap) {
result += '('; result += "(";
} }
result += node.toString(); result += node.toString();
if (wrap) { if (wrap) {
result += ')'; result += ")";
} }
return result; return result;
@ -37,7 +37,7 @@ function operandToString(node) {
class BinaryExpression extends Node { class BinaryExpression extends Node {
constructor(def) { constructor(def) {
super('BinaryExpression'); super("BinaryExpression");
this.left = def.left; this.left = def.left;
this.operator = def.operator; this.operator = def.operator;
this.right = def.right; this.right = def.right;
@ -52,19 +52,19 @@ class BinaryExpression extends Node {
var operator = this.operator; var operator = this.operator;
if (!left || !right) { if (!left || !right) {
throw new Error('Invalid BinaryExpression: ' + this); throw new Error("Invalid BinaryExpression: " + this);
} }
var builder = codegen.builder; var builder = codegen.builder;
if (left.type === 'Literal' && right.type === 'Literal') { if (left.type === "Literal" && right.type === "Literal") {
if (operator === '+') { if (operator === "+") {
return builder.literal(left.value + right.value); return builder.literal(left.value + right.value);
} else if (operator === '-') { } else if (operator === "-") {
return builder.literal(left.value - right.value); return builder.literal(left.value - right.value);
} else if (operator === '*') { } else if (operator === "*") {
return builder.literal(left.value * right.value); return builder.literal(left.value * right.value);
} else if (operator === '/') { } else if (operator === "/") {
return builder.literal(left.value / right.value); return builder.literal(left.value / right.value);
} }
} }
@ -78,13 +78,13 @@ class BinaryExpression extends Node {
var right = this.right; var right = this.right;
if (!left || !right) { if (!left || !right) {
throw new Error('Invalid BinaryExpression: ' + this); throw new Error("Invalid BinaryExpression: " + this);
} }
writeCodeForOperand(left, writer); writeCodeForOperand(left, writer);
writer.write(' '); writer.write(" ");
writer.write(operator); writer.write(operator);
writer.write(' '); writer.write(" ");
writeCodeForOperand(right, writer); writeCodeForOperand(right, writer);
} }
@ -94,7 +94,7 @@ class BinaryExpression extends Node {
toJSON() { toJSON() {
return { return {
type: 'BinaryExpression', type: "BinaryExpression",
left: this.left, left: this.left,
operator: this.operator, operator: this.operator,
right: this.right right: this.right
@ -112,11 +112,17 @@ class BinaryExpression extends Node {
var right = this.right; var right = this.right;
if (!left || !right) { if (!left || !right) {
throw new Error('Invalid BinaryExpression: ' + this); throw new Error("Invalid BinaryExpression: " + this);
} }
return operandToString(left) + ' ' + operator + ' ' + operandToString(right); return (
operandToString(left) +
" " +
operator +
" " +
operandToString(right)
);
} }
} }
module.exports = BinaryExpression; module.exports = BinaryExpression;

View File

@ -1,15 +1,15 @@
'use strict'; "use strict";
var Node = require('./Node'); var Node = require("./Node");
var adjustIndent = require('../util/adjustIndent'); var adjustIndent = require("../util/adjustIndent");
class Code extends Node { class Code extends Node {
constructor(def) { constructor(def) {
super('Code'); super("Code");
this.value = def.value; this.value = def.value;
} }
generateCode(codegen) { generateCode() {
return this; return this;
} }
@ -26,4 +26,4 @@ class Code extends Node {
} }
} }
module.exports = Code; module.exports = Code;

View File

@ -1,14 +1,14 @@
'use strict'; "use strict";
const Node = require('./Node'); const Node = require("./Node");
function _isMultilineComment(comment) { function _isMultilineComment(comment) {
return comment && comment.indexOf('\n') !== -1; return comment && comment.indexOf("\n") !== -1;
} }
class Comment extends Node { class Comment extends Node {
constructor(def) { constructor(def) {
super('Comment'); super("Comment");
const comment = def.comment; const comment = def.comment;
@ -19,7 +19,7 @@ class Comment extends Node {
} }
} }
generateCode(codegen) { generateCode() {
return this; return this;
} }

View File

@ -1,10 +1,10 @@
'use strict'; "use strict";
var Node = require('./Node'); var Node = require("./Node");
class ConditionalExpression extends Node { class ConditionalExpression extends Node {
constructor(def) { constructor(def) {
super('ConditionalExpression'); super("ConditionalExpression");
this.test = def.test; this.test = def.test;
this.consequent = def.consequent; this.consequent = def.consequent;
this.alternate = def.alternate; this.alternate = def.alternate;
@ -23,9 +23,9 @@ class ConditionalExpression extends Node {
var alternate = this.alternate; var alternate = this.alternate;
writer.write(test); writer.write(test);
writer.write(' ? '); writer.write(" ? ");
writer.write(consequent); writer.write(consequent);
writer.write(' : '); writer.write(" : ");
writer.write(alternate); writer.write(alternate);
} }
@ -35,7 +35,7 @@ class ConditionalExpression extends Node {
toJSON() { toJSON() {
return { return {
type: 'ConditionalExpression', type: "ConditionalExpression",
test: this.test, test: this.test,
consequent: this.consequent, consequent: this.consequent,
alternate: this.alternate alternate: this.alternate
@ -52,8 +52,8 @@ class ConditionalExpression extends Node {
var test = this.test; var test = this.test;
var consequent = this.consequent; var consequent = this.consequent;
var alternate = this.alternate; var alternate = this.alternate;
return test.toString() + ' ? ' + consequent + ' : ' + alternate; return test.toString() + " ? " + consequent + " : " + alternate;
} }
} }
module.exports = ConditionalExpression; module.exports = ConditionalExpression;

View File

@ -1,4 +1,4 @@
'use strict'; "use strict";
class Container { class Container {
constructor(node) { constructor(node) {
@ -10,4 +10,4 @@ class Container {
} }
} }
module.exports = Container; module.exports = Container;

View File

@ -1,6 +1,6 @@
'use strict'; "use strict";
var Node = require('./Node'); var Node = require("./Node");
class ContainerNode extends Node { class ContainerNode extends Node {
constructor(type) { constructor(type) {
@ -17,4 +17,4 @@ class ContainerNode extends Node {
} }
} }
module.exports = ContainerNode; module.exports = ContainerNode;

Some files were not shown because too many files have changed in this diff Show More