├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .local.dic
├── .remarkignore
├── .remarkrc.js
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package.json
├── src
├── assets
│ └── downloads
│ │ ├── data
│ │ └── api
│ │ │ ├── rentals.json
│ │ │ └── rentals
│ │ │ ├── downtown-charm.json
│ │ │ ├── grand-old-mansion.json
│ │ │ └── urban-living.json
│ │ ├── style.css
│ │ └── teaching-tomster.png
├── bin
│ └── generate.ts
├── lib
│ ├── index.ts
│ ├── plugins
│ │ ├── do-not-edit
│ │ │ ├── index.ts
│ │ │ ├── options.ts
│ │ │ └── walker.ts
│ │ ├── retina-images
│ │ │ ├── index.ts
│ │ │ ├── options.ts
│ │ │ └── walker.ts
│ │ ├── run-code-blocks
│ │ │ ├── cfg.ts
│ │ │ ├── commands.ts
│ │ │ ├── directives
│ │ │ │ ├── checkpoint.ts
│ │ │ │ ├── command.ts
│ │ │ │ ├── file
│ │ │ │ │ ├── copy.ts
│ │ │ │ │ ├── create.ts
│ │ │ │ │ ├── patch.ts
│ │ │ │ │ └── show.ts
│ │ │ │ ├── ignore.ts
│ │ │ │ ├── pause.ts
│ │ │ │ ├── screenshot.ts
│ │ │ │ └── server
│ │ │ │ │ ├── start.ts
│ │ │ │ │ └── stop.ts
│ │ │ ├── index.ts
│ │ │ ├── lineno.ts
│ │ │ ├── options.ts
│ │ │ ├── parse-args.ts
│ │ │ ├── parse-error.ts
│ │ │ ├── server.ts
│ │ │ ├── servers.ts
│ │ │ └── walker.ts
│ │ ├── todo-links
│ │ │ ├── index.ts
│ │ │ └── walker.ts
│ │ └── zoey-says
│ │ │ ├── index.ts
│ │ │ ├── markdown-to-html.ts
│ │ │ └── walker.ts
│ └── walker.ts
├── markdown
│ └── tutorial
│ │ ├── acceptance-test.md
│ │ ├── autocomplete-component.md
│ │ ├── deploying.md
│ │ ├── ember-cli.md
│ │ ├── ember-data.md
│ │ ├── hbs-helper.md
│ │ ├── index.md
│ │ ├── installing-addons.md
│ │ ├── model-hook.md
│ │ ├── part-1
│ │ ├── 01-orientation.md
│ │ ├── 02-building-pages.md
│ │ ├── 03-automated-testing.md
│ │ ├── 04-component-basics.md
│ │ ├── 05-more-about-components.md
│ │ ├── 06-interactive-components.md
│ │ ├── 07-reusable-components.md
│ │ ├── 08-working-with-data.md
│ │ ├── index.md
│ │ └── recap.md
│ │ ├── part-2
│ │ ├── 09-route-params.md
│ │ ├── 10-service-injection.md
│ │ ├── 11-ember-data.md
│ │ ├── 12-provider-components.md
│ │ ├── index.md
│ │ └── recap.md
│ │ ├── routes-and-templates.md
│ │ ├── service.md
│ │ ├── simple-component.md
│ │ └── subroutes.md
└── types
│ ├── remark-frontmatter.d.ts
│ └── remark-parse-yaml.d.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | pull_request: {}
4 | push:
5 | branches:
6 | - main
7 | schedule:
8 | - cron: '0 0 * * *'
9 | workflow_dispatch: {}
10 |
11 | jobs:
12 | build:
13 | name: Build (${{ matrix.channel }})
14 | runs-on: ubuntu-22.04 # Update when chromium bug mentioned https://github.com/puppeteer/puppeteer/issues/12818 is resolved
15 | env:
16 | CI: 'true'
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | channel:
21 | - release
22 | - beta
23 | steps:
24 | - name: Set up Git
25 | run: |
26 | git config --global user.name "Tomster"
27 | git config --global user.email "tomster@emberjs.com"
28 | - name: Checkout
29 | uses: actions/checkout@v4
30 | - name: Set up Node
31 | uses: actions/setup-node@v4
32 | with:
33 | node-version: '18'
34 | cache: 'yarn'
35 | - name: Install dependencies (apt-get)
36 | run: |
37 | sudo apt-get update -y
38 | sudo apt-get install -y tree
39 | - name: Install dependencies (yarn)
40 | run: |
41 | if [[ "$EMBER_RELEASE_CHANNEL" == "beta" ]]; then
42 | yarn upgrade ember-cli@beta
43 | else
44 | yarn upgrade ember-cli
45 | fi
46 |
47 | yarn install
48 | env:
49 | EMBER_RELEASE_CHANNEL: ${{ matrix.channel }}
50 | - name: Lint (markdown source)
51 | run: yarn lint:md:src
52 | - name: Lint (typescript)
53 | run: yarn lint:ts
54 | - name: Compile (typescript)
55 | run: yarn tsc
56 | - name: Generate
57 | run: yarn generate
58 | env:
59 | # This is needed for external PRs to build, since secrets are not
60 | # available there. Get your own Mapbox token for local development
61 | # at: https://account.mapbox.com/access-tokens
62 | MAPBOX_ACCESS_TOKEN: 'pk.eyJ1IjoiZW1iZXJqcyIsImEiOiJjazBydnZjb2wwYXA5M2Rwc3IydGF2eXM0In0.EQiBFsRi9Jm70XFPiXnoHg'
63 | - name: Lint (markdown output)
64 | run: yarn lint:md:dist
65 | - name: Prune artifacts
66 | if: always()
67 | working-directory: dist/code/super-rentals
68 | run: git clean -dfX
69 | - name: Upload artifacts (assets)
70 | uses: actions/upload-artifact@v4
71 | if: always()
72 | with:
73 | name: assets (${{ matrix.channel }})
74 | path: dist/assets
75 | - name: Upload artifacts (markdown)
76 | uses: actions/upload-artifact@v4
77 | if: always()
78 | with:
79 | name: markdown (${{ matrix.channel }})
80 | path: dist/markdown
81 | - name: Upload artifacts (code)
82 | uses: actions/upload-artifact@v4
83 | if: always()
84 | with:
85 | name: code (${{ matrix.channel }})
86 | path: dist/code/super-rentals
87 | include-hidden-files: true
88 | - name: Notify Discord # This is a step rather than a job because of the matrix
89 | uses: sarisia/actions-status-discord@v1
90 | if: ${{ failure() && github.event_name == 'schedule' }}
91 | with:
92 | webhook: ${{ secrets.CORE_META_WEBHOOK }}
93 | status: "Failure"
94 | title: "Super Rentals Tutorial Build ${{ matrix.channel }}"
95 | color: 0xcc0000
96 | url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
97 | username: GitHub Actions
98 |
99 | deploy-guides:
100 | # This job deploys the markdown output to the super-rentals-tutorial branch
101 | # of the ember-learn/guides-source repository, along with the necessary
102 | # assets (downloads and screenshots).
103 | #
104 | # For the markdown, we can simply copy over the output into the appropriate
105 | # folder in guides-source. We start by emptying out the folder to account
106 | # for any deleted or renamed chapters.
107 | #
108 | # For the downloads, it is also mostly a matter of copying them into the
109 | # right location. As with the markdown content, we empty out the folder
110 | # beforehand to account for deleted and renamed files.
111 | #
112 | # The JSON data used in the tutorial (for mocking an API server) is stored
113 | # as loose files in this repository, to make them easier to change and
114 | # review. However, we ultimately want to provide it for download as a zip
115 | # file, so we need to zip up the contents.
116 | #
117 | # However, even if the JSON content didn't actually change, the raw bytes
118 | # of the zip file may be different from the current version committed to
119 | # git, due to differences in compression settings, etc. To ensure we don't
120 | # churn the git blobs for no reason, we use the zipcmp utility to compare
121 | # the contents before adding the zip file. We also run it through advzip to
122 | # improve the compression ratio.
123 | #
124 | # Finally, screenshots also have the same problems as the zip file. Even
125 | # if the screenshot didn't visually change, the raw bytes are likely to be
126 | # different due to micro differences in pixel colors, etc. To account for
127 | # this, we use the perceptualdiff utility to compare them against what is
128 | # in git before adding them. We also run them through optipng and advpng to
129 | # improve the compression ratio. This can be quite slow, so only do it if
130 | # we detected visual changes in the screenshots.
131 | #
132 | # Once all the necessary parts are added, we simply commit and push to the
133 | # super-rentals-tutorial branch of the ember-learn/guides-source repository
134 | # if there are any changes in its content.
135 | #
136 | name: Deploy to ember-learn/guides-source (super-rentals-tutorial branch)
137 | needs: build
138 | runs-on: ubuntu-latest
139 | if: github.ref == 'refs/heads/main'
140 | steps:
141 | - name: Set up Git
142 | run: |
143 | git config --global user.name "Tomster"
144 | git config --global user.email "tomster@emberjs.com"
145 | - name: Set up SSH
146 | uses: webfactory/ssh-agent@v0.4.1
147 | with:
148 | ssh-private-key: ${{ secrets.GUIDES_SOURCE_DEPLOY_KEY }}
149 | - name: Install dependencies
150 | run: |
151 | sudo apt-get update -y
152 | sudo apt-get install -y zipcmp advancecomp optipng perceptualdiff
153 | - name: Download artifacts (assets)
154 | uses: actions/download-artifact@v4
155 | with:
156 | name: assets (release)
157 | path: assets
158 | - name: Download artifacts (markdown)
159 | uses: actions/download-artifact@v4
160 | with:
161 | name: markdown (release)
162 | path: markdown
163 | - name: Clone
164 | run: |
165 | set -euo pipefail
166 | IFS=$'\n\t'
167 |
168 | if ! git clone git@github.com:ember-learn/guides-source --depth 1 -b super-rentals-tutorial; then
169 | git clone git@github.com:ember-learn/guides-source --depth 1 -b master
170 | cd guides-source
171 | git checkout -b super-rentals-tutorial
172 | fi
173 | - name: Add markdown
174 | working-directory: guides-source
175 | run: |
176 | mkdir -p guides/release/tutorial/
177 | rm -rf guides/release/tutorial/*
178 | cp -r ../markdown/* guides/release/
179 | git add guides/release
180 | - name: Add downloads
181 | working-directory: guides-source
182 | run: |
183 | set -euo pipefail
184 | IFS=$'\n\t'
185 |
186 | mkdir -p public/downloads/
187 | rm -rf public/downloads/*
188 | cp -r ../assets/downloads/* public/downloads/
189 |
190 | pushd public/downloads/data/
191 | zip -r current.zip .
192 | popd
193 |
194 | if ! (git checkout -- public/downloads/data.zip 2>&1 && zipcmp public/downloads/data.zip public/downloads/data/current.zip); then
195 | mv public/downloads/data/current.zip public/downloads/data.zip
196 | advzip -z -q -4 public/downloads/data.zip
197 | fi
198 |
199 | rm -rf public/downloads/data
200 |
201 | git add public/downloads
202 | - name: Add screenshots
203 | working-directory: guides-source
204 | run: |
205 | set -euo pipefail
206 | IFS=$'\n\t'
207 |
208 | mkdir -p tmp/prev
209 | git checkout-index -f -a --prefix=tmp/prev/
210 |
211 | mkdir -p public/images/tutorial
212 | rm -rf public/images/tutorial/*
213 |
214 | function add_screenshot {
215 | local src="$1";
216 | local prev=$(echo -n "$1" | sed "s|../assets/|tmp/prev/public/|");
217 | local dest=$(echo -n "$1" | sed "s|../assets/|public/|");
218 | local diff=$(echo -n "$1" | sed "s|../assets/|tmp/perceptualdiff/|" | sed "s|@2x.png|.ppm|");
219 |
220 | mkdir -p "$(dirname "$dest")"
221 | mkdir -p "$(dirname "$diff")"
222 |
223 | if [[ -f "$prev" ]]; then
224 | if git diff --no-index -- "$prev" "$src" > /dev/null 2>&1; then
225 | echo "$dest (unchanged)"
226 | cp "$prev" "$dest"
227 | return 0
228 | elif perceptualdiff --output "$diff" --downsample 2 --colorfactor 0.5 --threshold 1000 "$prev" "$src" > /dev/null; then
229 | echo "$dest (unchanged)"
230 | cp "$prev" "$dest"
231 | return 0
232 | fi
233 | fi
234 |
235 | cp "$src" "$dest"
236 | optipng -q -o5 "$dest"
237 | advpng -z -q -4 "$dest"
238 |
239 | local before=$(wc -c < "$src")
240 | local after=$(wc -c < "$dest")
241 | local percentage=$(( $after * 100 / $before ))
242 |
243 | echo "$dest ($percentage%)"
244 | return 0
245 | }
246 |
247 | export -f add_screenshot
248 | find ../assets/images -type f -name "*.png" | xargs -n 1 -P 2 -I {} bash -c 'add_screenshot "$@"' _ {}
249 |
250 | git add public/images
251 | - name: Upload artifacts (perceptualdiff)
252 | uses: actions/upload-artifact@v4
253 | if: always()
254 | with:
255 | name: perceptualdiff (release)
256 | path: guides-source/tmp/perceptualdiff
257 | - name: Commit
258 | working-directory: guides-source
259 | run: |
260 | set -euo pipefail
261 | IFS=$'\n\t'
262 |
263 | if [[ "$GITHUB_EVENT_NAME" == "schedule" ]]; then
264 | COMMIT_TITLE="[CRON] $(date +"%A %b %d, %Y")"
265 | else
266 | COMMIT_TITLE="$(
267 | echo -n "$ORIGINAL_COMMIT_MESSAGE" | \
268 | sed -E "s|^Merge pull request #(.+) from .+$|Merge $GITHUB_REPOSITORY#\1|"
269 | )"
270 | fi
271 |
272 | COMMIT_MESSAGE="$COMMIT_TITLE
273 |
274 | ---
275 |
276 | Commit: $GITHUB_REPOSITORY@$GITHUB_SHA
277 | Script: https://github.com/$GITHUB_REPOSITORY/blob/$GITHUB_SHA/.github/workflows/build.yml
278 | Logs: https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA/checks"
279 |
280 | git commit --allow-empty -m "$COMMIT_MESSAGE"
281 | env:
282 | ORIGINAL_COMMIT_MESSAGE: ${{ github.event.commits[0].message }}
283 | - name: Push
284 | working-directory: guides-source
285 | run: |
286 | set -euo pipefail
287 | IFS=$'\n\t'
288 |
289 | if git diff --exit-code HEAD~; then
290 | echo "Nothing to push"
291 | else
292 | git push -u origin super-rentals-tutorial
293 | fi
294 |
295 | deploy-code-preserving-history:
296 | # This job deploys the built app to the super-rentals-tutorial-output
297 | # branch of the ember-learn/super-rentals repository. This branch preserves
298 | # the commit history of the tutorial flow (i.e. one git commit per chapter),
299 | # and is force-pushed on every build. We add an empty commit on top as sort
300 | # of a "cover page" to preserve the metadata (links to the commit, build
301 | # logs, etc).
302 | #
303 | name: Deploy to ember-learn/super-rentals (super-rentals-tutorial-output branch)
304 | needs: build
305 | runs-on: ubuntu-latest
306 | if: github.ref == 'refs/heads/main'
307 | steps:
308 | - name: Set up Git
309 | run: |
310 | git config --global user.name "Tomster"
311 | git config --global user.email "tomster@emberjs.com"
312 | - name: Set up SSH
313 | uses: webfactory/ssh-agent@v0.4.1
314 | with:
315 | ssh-private-key: ${{ secrets.SUPER_RENTALS_DEPLOY_KEY }}
316 | - name: Download artifacts
317 | uses: actions/download-artifact@v4
318 | with:
319 | name: code (release)
320 | path: .
321 | - name: Commit
322 | run: |
323 | set -euo pipefail
324 | IFS=$'\n\t'
325 |
326 | if [[ "$GITHUB_EVENT_NAME" == "schedule" ]]; then
327 | COMMIT_TITLE="[CRON] $(date +"%A %b %d, %Y")"
328 | else
329 | COMMIT_TITLE="$(
330 | echo -n "$ORIGINAL_COMMIT_MESSAGE" | \
331 | sed -E "s|^Merge pull request #(.+) from .+$|Merge $GITHUB_REPOSITORY#\1|"
332 | )"
333 | fi
334 |
335 | COMMIT_MESSAGE="$COMMIT_TITLE
336 |
337 | ---
338 |
339 | Commit: $GITHUB_REPOSITORY@$GITHUB_SHA
340 | Script: https://github.com/$GITHUB_REPOSITORY/blob/$GITHUB_SHA/.github/workflows/build.yml
341 | Logs: https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA/checks"
342 |
343 | git commit --allow-empty -m "$COMMIT_MESSAGE"
344 | env:
345 | ORIGINAL_COMMIT_MESSAGE: ${{ github.event.commits[0].message }}
346 | - name: Push
347 | run: |
348 | git remote add super-rentals git@github.com:ember-learn/super-rentals.git
349 | git push -f super-rentals master:super-rentals-tutorial-output
350 |
351 | deploy-code-flattened:
352 | # This job deploys the built app to the main branch of the
353 | # ember-learn/super-rentals repository. This branch does not preserve
354 | # the commit history of the tutorial flow. Instead, it squashes the changes
355 | # into a single commit on the main branch. This preserves
356 | # a linear history of changes to the built app over time, either due to
357 | # changes to the generator blueprints, or changes to the tutorial content
358 | # itself (e.g. refactoring to use new ember features). A lot of times, the
359 | # built app's source code remains stable and so there may be no changes to
360 | # push here.
361 | #
362 | name: Deploy to ember-learn/super-rentals (main branch)
363 | needs: build
364 | runs-on: ubuntu-latest
365 | if: github.ref == 'refs/heads/main'
366 | steps:
367 | - name: Set up Git
368 | run: |
369 | git config --global user.name "Tomster"
370 | git config --global user.email "tomster@emberjs.com"
371 | - name: Set up SSH
372 | uses: webfactory/ssh-agent@v0.4.1
373 | with:
374 | ssh-private-key: ${{ secrets.SUPER_RENTALS_DEPLOY_KEY }}
375 | - name: Download artifacts
376 | uses: actions/download-artifact@v4
377 | with:
378 | name: code (release)
379 | path: output
380 | - name: Clone
381 | run: git clone git@github.com:ember-learn/super-rentals --depth 1
382 | - name: Commit
383 | working-directory: super-rentals
384 | run: |
385 | set -euo pipefail
386 | IFS=$'\n\t'
387 |
388 | git rm -rf '*'
389 |
390 | if [[ "$GITHUB_EVENT_NAME" == "schedule" ]]; then
391 | COMMIT_TITLE="[CRON] $(date +"%A %b %d, %Y")"
392 | else
393 | COMMIT_TITLE="$(
394 | echo -n "$ORIGINAL_COMMIT_MESSAGE" | \
395 | sed -E "s|^Merge pull request #(.+) from .+$|Merge $GITHUB_REPOSITORY#\1|"
396 | )"
397 | fi
398 |
399 | COMMIT_MESSAGE="$COMMIT_TITLE
400 |
401 | ---
402 |
403 | Commit: $GITHUB_REPOSITORY@$GITHUB_SHA
404 | Script: https://github.com/$GITHUB_REPOSITORY/blob/$GITHUB_SHA/.github/workflows/build.yml
405 | Logs: https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA/checks"
406 |
407 | git commit --allow-empty -m "$COMMIT_MESSAGE"
408 | git remote add output ../output
409 | git fetch output
410 | git merge --squash --no-commit --allow-unrelated-histories output/master
411 | git reset HEAD~ -- README.md app.json
412 | git commit --allow-empty --amend --no-edit
413 | env:
414 | ORIGINAL_COMMIT_MESSAGE: ${{ github.event.commits[0].message }}
415 | - name: Push
416 | working-directory: super-rentals
417 | run: |
418 | set -euo pipefail
419 | IFS=$'\n\t'
420 |
421 | if git diff --exit-code origin/main; then
422 | echo "Nothing to push"
423 | else
424 | git push
425 | fi
426 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | tmp
4 | *.log
5 |
--------------------------------------------------------------------------------
/.local.dic:
--------------------------------------------------------------------------------
1 | blockquote
2 | callouts
3 | focusability
4 | GIFs
5 | Hm
6 | invoker
7 | invokers
8 | Mapbox
9 | misspelt
10 | nav-bar
11 | *NPM
12 | page-crafter
13 | param
14 | params
15 | parameterizing
16 | PRs
17 | pre-determined
18 | pre-populating
19 | prepend
20 | presentational
21 | repo
22 | repos
23 | Runnable
24 | runnable
25 | RunDOC
26 | source-readibility
27 | Splattributes
28 | swappable
29 | syntaxes
30 | triple-backtick
31 | unstyled
32 | untracked
33 | Voilà
34 | voilà
35 | yay
36 | EmberData
37 | RequestManager
38 | centric
--------------------------------------------------------------------------------
/.remarkignore:
--------------------------------------------------------------------------------
1 | CONTRIBUTING.md
2 | README.md
3 |
--------------------------------------------------------------------------------
/.remarkrc.js:
--------------------------------------------------------------------------------
1 | // .remarkrc.js
2 | /* eslint-env node */
3 | const unified = require("unified");
4 | const read = require("fs").readFileSync;
5 | const ember = require("ember-dictionary");
6 |
7 | exports.plugins = [
8 | [
9 | require("remark-retext"),
10 | unified().use({
11 | plugins: [
12 | [require("retext-contractions"), { straight: true }],
13 | require("retext-english"),
14 | require("retext-indefinite-article"),
15 | require("retext-repeated-words"),
16 | require("retext-syntax-urls"),
17 | [
18 | require("retext-spell"),
19 | {
20 | dictionary: ember,
21 | personal: read("./.local.dic")
22 | }
23 | ]
24 | ]
25 | })
26 | ],
27 | "remark-preset-lint-consistent",
28 | "remark-preset-lint-recommended",
29 | ["remark-lint-list-item-indent", "space"]
30 | ];
31 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "program": "${workspaceFolder}/dist/bin/generate.js",
12 | "console": "integratedTerminal",
13 | "preLaunchTask": "compile"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "files.exclude": {
4 | "**/.git": true,
5 | "**/.svn": true,
6 | "**/.hg": true,
7 | "**/CVS": true,
8 | "**/.DS_Store": true,
9 | "node_modules": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "compile",
8 | "type": "shell",
9 | "command": "yarn",
10 | "args": ["compile"],
11 | "problemMatcher": [
12 | "$tslint5",
13 | "$tsc"
14 | ]
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for your interest in improving the Super Rentals Tutorial! This repository contains the logic to generate the prose markdown for our tutorial, as well as the code for the tutorial _app_ itself.
4 |
5 | Please note that all of the content to generate markdown and code are located in individual chapter files in `/src/markdown`. Any modifications/additions should occur there.
6 |
7 | When building this tutorial you may notice that you have a `/dist/markdown` generated locally. Please note that this directory is _not_ committed, and contains the output of your locally-generated tutorial content. Please be mindful not to modify anything in the `/dist` directory as it is git ignored and any changes there will not impact what is generate! Instead, make your changes inside of the `/src` directory only.
8 |
9 | ## Making changes to the tutorial prose
10 |
11 | If you are modifying or adding new prose to the tutorial, please be sure to follow the formatting style guide below! Following these conventions will make it easier to review your contribution. 😊
12 |
13 | ### Formatting style guide
14 |
15 | * When naming chapters, be sure that they are prefixed with two digits before the chapter name to ensure that they are ordered correctly.
16 | * For example: `01-orientation.md`.
17 | * When referring to filenames, be sure to specify the full relative path to a file. This is to make it easy to copy-paste the filename when people use the tutorial.
18 | * For example: _"Let's start by creating a new file at `app/components/jumbo.hbs`."_
19 | * When referring to other chapters, link to the chapter using its relative path in the tutorial. Please be sure to **include the trailing slash** in the path so that the markdown will be linted correctly in the guides repo.
20 | * For example: `We learned about this in a [previous chapter](../02-building-pages/)`.
21 | * When referring to important key terms, be sure to _italicize_ the term and link to a definition for it. This highlighting only needs to be done once, ideally on the first instance of the term. Note that, when using the "TODO" format (a placeholder for a link to be added in the future), we used `[]` rather than `()` to wrap the link markdown. This ensures that the generated output does not have broken links.
22 | * For example: `*[route][TODO: add a link to route]*`.
23 | * When referring to component names, keywords, helpers, or HTML elements, be sure to use code markup.
24 | * For example: _"Inside of this file, we have the `` component and the `{{outlet}}` keyword."_
25 | * When using the "Zoey says" callouts, be sure to use the blockquote formatting, ensuring that an extra `>` is included between the "Zoey says..." text and the actual note content. This is required in order to properly convert the markdown into HTML.
26 | * For example:
27 | ```
28 | > Zoey says...
29 | >
30 | > Here's a very helpful comment!
31 | ```
32 |
33 | ## Making changes to the tutorial code
34 |
35 | The workflow for updating the Super Rentals Tutorial involves working within two directories of this project: `/src/markdown` and `/dist`. The editable prose lives in `src/markdown`; any changes that are made to the prose should be done there. The tutorial uses the content in `/src/markdown` to generate the prose markdown that is output into `/dist/markdown`. Interspersed with the prose are commands that are used to generate the code for the _actual_ tutorial app, which is outputted at `/dist/code/super-rentals`.
36 |
37 | Once you have made your changes in the tutorial prose or code, you can run a few different commands to verify that your changes render as expected.
38 |
39 | * To regenerate all of the chapters (and rebuild the app), run `yarn build`. This will `rm -rf` anything previously generated in the `/dist` directory.
40 | * To generate a single chapter without removing all the previously generated content, run `yarn generate src/markdown/07-your-chapter-to-edit-here.md`. This will rebuild _just_ the chapter that is specified without deleting everything else in the `/dist` directory. Please note that, before running this command, the _current state_ of the `/dist/code` directory has to match the precise state of what it would be after running the generate command on the previous chapter. In other words, in order for the generate command to work correctly, the state of the code in `/dist/code` should match exactly the code that would be generated by `yarn generate src/markdown/06-the-previous-chapter.md` before it is run for `src/markdown/07-your-chapter-to-edit-here.md` .
41 |
42 | ### Code changes
43 |
44 | To make code changes, you will first need to locate the chapter you wish to edit in `/src/markdown`. Depending on the type of code change you'd like to make, you will need to use different commands.
45 |
46 | #### Running commands
47 |
48 | If you'd like to run a command, you will need to use the `run: command`. Be sure to include `cwd=super-rentals` so that the file is generated in the app's directory.
49 |
50 | For example:
51 |
52 | ```run:command hidden=true cwd=super-rentals
53 | ember generate component rental
54 | ```
55 |
56 | Note that running a command by default will include the output of that command in the final markdown. To avoid output in markdown, you can use `hidden=true` or `captureOutput=false` as appropriate, as detailed in the [README](./README.md).
57 |
58 | #### Creating files
59 |
60 | To create a file, you will need to use the `run:file:create` command. Be sure to specify the language, the filename, as well as the `cwd` directory.
61 |
62 | For example:
63 |
64 | ```run:file:create lang=handlebars cwd=super-rentals filename=app/templates/about.hbs
65 | About Super Rentals
66 | ```
67 |
68 | After creating a file, you should be sure to _add_ it shortly afterwards so that it is tracked by git, and will be committed in the process of generating the tutorial app. This can be a `hidden` command, since it is not part of the prose of the tutorial.
69 |
70 | For example:
71 |
72 | ```run:command hidden=true cwd=super-rentals
73 | git add app/templates/about.hbs
74 | ```
75 |
76 | If you are adding a _test_ file, be sure to run `ember test --path dist` before adding the test file. This way, the generator will run the tests before adding them, and will fail in generating broken code if the tests themselves fail. The `--path` flag will speed up the test build and prevent test timeouts.
77 |
78 | #### Using `run:pause`
79 |
80 | When running commands, generating new files, or making changes to preexisting ones, you may want to see the current state of the generated app. The `run: pause` command is useful in this case. Use this command inside of your existing chapter, inserting it at points where you want the generator to stop. This is similar to running something like `pauseTest()`; it will pause the generator and allow you to see the current state of the code you have generated (located in `/dist/code/super-rentals`).
81 |
82 | For example:
83 |
84 | ```run:pause
85 | Note to self: check that `application.hbs` has the correct markup and remember to git add it!
86 | ```
87 |
88 | Any content inside of the code blocks will be printed in your command line.
89 |
90 | #### Generating diffs
91 |
92 | _⚠️ Please note: you may find it easier to generate diffs and make edits to the tutorial by opening `/dist/code/super-rentals/code` in a separate code editor window._
93 |
94 | Before you start make any changes to capture a diff, navigate to `/dist/code/super-rentals/code` in the terminal. Run `git diff` and make sure there are no changes and that you have an empty diff.
95 |
96 | In the case of generating diffs to render inside of the tutorial prose, you will want to use the `run: pause` command, described in the section above. Once you have put a `run: pause` at the spot in your prose where the diff needs to be captured, you can navigate to the `/dist/code/super-rentals/code` directory in your editor. In the app code, make whatever changes you need to make in whatever files need to be modified.
97 |
98 | After you have made your code changes, navigate back to `/dist/code/super-rentals/code` in the terminal. First, check that the diff is included in the current changes:
99 |
100 | ```
101 | ➜ code git:(your-branch-name) ✗ git diff
102 | ```
103 |
104 | Next, save your diff to some temporary file that you will open in a code editor in a moment (`my_awesome_patch` in the example below). You want to be sure to generate the diff specifically with only 1 line of context. You can do this by specifying `git diff --unified=1` or `git diff -U1`.
105 |
106 | ```
107 | ➜ code git:(your-branch-name) ✗ git diff -U1 > my_awesome_patch
108 | ➜ code git:(your-branch-name) ✗ code my_awesome_patch
109 | ```
110 |
111 | Once you have opened your temporary file containing your diff, edit it so that the only thing surrounding the diff is the line number additions subtractions.
112 |
113 | ```
114 | @@ -9,2 +9,3 @@
115 | Router.map(function() {
116 | + this.route('about');
117 | });
118 | ```
119 |
120 | Finally, wrap the newly-modified diff content in the `run:file:patch` command. Be sure to specify the language of the file, the filename, and the `cwd=super-rentals` option so that the patch is applied in the app's directory.
121 |
122 | For example:
123 |
124 | ```run:file:patch lang=js cwd=super-rentals filename=app/router.js
125 | @@ -9,2 +9,3 @@
126 | Router.map(function() {
127 | + this.route('about');
128 | });
129 | ```
130 |
131 | In both cases, be sure your editor is not set to trim trailing whitespace on lines. This will cause the diff to be error with `error: corrupt patch at line`.
132 |
133 | Once you have added the diff to the prose using the `run:file:patch` command, you can remove the `run:pause` command. In order to test that your patch was successfully applied, be sure to:
134 |
135 | 1. Run `git checkout` any changes in the working directory of `/dist/code/super-rentals/code`.
136 | 2. Regenerate your modified chapter (`yarn generate src/markdown/00-your-chapter-to-edit-here.md`).
137 |
138 | Finally, you can look at your modified chapter in `/dist/markdown` and make sure that your patch renders as you expect in the markdown!
139 |
140 | #### Adding screenshots
141 |
142 | TODO: add content!
143 |
144 | #### Adding GIFs
145 |
146 | TODO: add content!
147 |
148 | #### Saving a chapter
149 |
150 | Once a chapter is ready to be published and files, diffs, and screenshots have all been added as necessary, the last step is to commit the code changes at the end of the chapter. This is done using the `run:checkpoint` command.
151 |
152 | For example:
153 |
154 | ```run:checkpoint cwd=super-rentals
155 | Chapter 3
156 | ```
157 |
158 | This should always be the last thing at the end of every chapter, as it will lint and run tests against the code generated in the chapter. The commit message will be whatever content is in the code block. We prefer the chapter title as the content of the commit.
159 |
160 | Before opening a pull request, be sure to check the `/dist/markdown` to make sure that the markdown generated by your changes to a chapter still looks correct. Also lint the markdown by running the `lint:md` script.
161 |
162 | #### Linting
163 |
164 | Linting checks the markdown for spelling mistakes, repeated words, and markdown consistency. The linter also enforces brand and product name consistency. i.e `Ember` not `ember` or `GitHub`, not `Github`.
165 |
166 | The `super-rentals-tutorial` uses the same linting rules as the [guides](https://github.com/ember-learn/guides-source). If linting passes here it will pass when merged with the guides. Most of the linting errors, except some spelling errors, will be self-explanatory.
167 |
168 | #### Spelling Errors
169 |
170 | The spelling [dictionary](https://github.com/maxwondercorn/ember-dictionary) used by the markdown linter contains many words common to the Ember ecosystem and general technology. It won't contain every possible word that could be used in the tutorial. Some real words or acronyms will be flagged as misspelled words.
171 |
172 | Correctly spelled words flagged as misspelt can be added to the local dictionary file `.local.dic`. This will eliminate the spelling errors. Words added to the local dictionary should be in alphabetical order and all word cases added as necessary. i.e. both `params` and `Params` would need to be added if there both used in the tutorial.
173 |
174 | Finally, the tutorial will be linted using the `guides` markdown linter when the PR is created. Since the linter will use the `guides` dictionary the words in `.local.dic` must also be in the guides `.local.dic` file for linting to pass.
175 |
176 | When new words are added to the local dictionary, a separate PR must be opened on the guides to add them to the guides dictionary. If there are words that you think belong to the [Ember dictionary](https://github.com/maxwondercorn/ember-dictionary), please open a PR in that repo.
177 |
178 | #### Pull Request Merging
179 |
180 | After the pull request is merged to the `main` branch, the generated markdown and code output will be pushed to a branch on the markdown/code output will be pushed to a branch on the [guides](https://github.com/ember-learn/guides-source) and [super rentals repos](https://github.com/ember-learn/guides-source/tree/super-rentals-tutorial) so that they can be further reviewed by their maintainers before integrating into the tutorial code/app and markdown.
181 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ember Learning Team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "super-rentals-tutorial",
3 | "version": "0.0.0",
4 | "description": "Runnable super-rentals tutorial",
5 | "main": "index.js",
6 | "author": "Godfrey Chan",
7 | "license": "MIT",
8 | "private": true,
9 | "devDependencies": {
10 | "@actions/core": "^1.2.6",
11 | "@types/glob": "^8.0.0",
12 | "@types/image-size": "^0.8.0",
13 | "@types/mdast": "^3.0.2",
14 | "@types/mkdirp": "^1.0.2",
15 | "@types/ncp": "^2.0.1",
16 | "@types/node": "^18.11.18",
17 | "@types/unist": "^2.0.3",
18 | "@types/vfile": "^4.0.0",
19 | "chalk": "^2.4.2",
20 | "ember-cli": "*",
21 | "ember-dictionary": "^0.2.3",
22 | "image-size": "^1.0.2",
23 | "mkdirp": "^1.0.4",
24 | "ncp": "^2.0.0",
25 | "node-glob": "^1.2.0",
26 | "puppeteer": "~24.4.0",
27 | "rehype-stringify": "^6.0.0",
28 | "remark": "^11.0.1",
29 | "remark-cli": "^7.0.0",
30 | "remark-frontmatter": "^1.3.2",
31 | "remark-lint": "^6.0.5",
32 | "remark-lint-list-item-indent": "^1.0.4",
33 | "remark-parse": "^7.0.1",
34 | "remark-parse-yaml": "^0.0.3",
35 | "remark-preset-lint-consistent": "^2.0.3",
36 | "remark-preset-lint-recommended": "^3.0.3",
37 | "remark-rehype": "^5.0.0",
38 | "remark-retext": "^3.1.3",
39 | "remark-stringify": "^7.0.2",
40 | "retext-contractions": "^3.0.0",
41 | "retext-english": "^3.0.3",
42 | "retext-indefinite-article": "^1.1.7",
43 | "retext-repeated-words": "^2.0.0",
44 | "retext-spell": "^3.0.0",
45 | "retext-syntax-urls": "^1.0.2",
46 | "shx": "^0.3.2",
47 | "ts-std": "^0.7.0",
48 | "tslint": "^6.1.3",
49 | "tslint-config-prettier": "^1.18.0",
50 | "typescript": "^4.9.4",
51 | "unified": "^8.4.0"
52 | },
53 | "scripts": {
54 | "clean": "shx rm -rf dist",
55 | "lint": "yarn lint:ts && yarn lint:md",
56 | "lint:ts": "tslint --project .",
57 | "lint:md": "remark . --frail",
58 | "lint:md:src": "yarn lint:md --ignore-pattern dist",
59 | "lint:md:dist": "yarn lint:md --ignore-pattern src",
60 | "precompile": "yarn lint:ts",
61 | "compile": "tsc",
62 | "generate": "node ./dist/bin/generate.js",
63 | "prebuild": "yarn clean && yarn compile && yarn lint:md:src",
64 | "build": "yarn generate",
65 | "postbuild": "yarn lint:md:dist"
66 | },
67 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
68 | }
69 |
--------------------------------------------------------------------------------
/src/assets/downloads/data/api/rentals.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "type": "rental",
5 | "id": "grand-old-mansion",
6 | "attributes": {
7 | "title": "Grand Old Mansion",
8 | "owner": "Veruca Salt",
9 | "city": "San Francisco",
10 | "location": {
11 | "lat": 37.7749,
12 | "lng": -122.4194
13 | },
14 | "category": "Estate",
15 | "bedrooms": 15,
16 | "image": "https://upload.wikimedia.org/wikipedia/commons/c/cb/Crane_estate_(5).jpg",
17 | "description": "This grand old mansion sits on over 100 acres of rolling hills and dense redwood forests."
18 | }
19 | },
20 | {
21 | "type": "rental",
22 | "id": "urban-living",
23 | "attributes": {
24 | "title": "Urban Living",
25 | "owner": "Mike Teavee",
26 | "city": "Seattle",
27 | "location": {
28 | "lat": 47.6062,
29 | "lng": -122.3321
30 | },
31 | "category": "Condo",
32 | "bedrooms": 1,
33 | "image": "https://upload.wikimedia.org/wikipedia/commons/2/20/Seattle_-_Barnes_and_Bell_Buildings.jpg",
34 | "description": "A commuters dream. This rental is within walking distance of 2 bus stops and the Metro."
35 | }
36 | },
37 | {
38 | "type": "rental",
39 | "id": "downtown-charm",
40 | "attributes": {
41 | "title": "Downtown Charm",
42 | "owner": "Violet Beauregarde",
43 | "city": "Portland",
44 | "location": {
45 | "lat": 45.5175,
46 | "lng": -122.6801
47 | },
48 | "category": "Apartment",
49 | "bedrooms": 3,
50 | "image": "https://upload.wikimedia.org/wikipedia/commons/f/f7/Wheeldon_Apartment_Building_-_Portland_Oregon.jpg",
51 | "description": "Convenience is at your doorstep with this charming downtown rental. Great restaurants and active night life are within a few feet."
52 | }
53 | }
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/src/assets/downloads/data/api/rentals/downtown-charm.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "type": "rental",
4 | "id": "downtown-charm",
5 | "attributes": {
6 | "title": "Downtown Charm",
7 | "owner": "Violet Beauregarde",
8 | "city": "Portland",
9 | "location": {
10 | "lat": 45.5175,
11 | "lng": -122.6801
12 | },
13 | "category": "Apartment",
14 | "bedrooms": 3,
15 | "image": "https://upload.wikimedia.org/wikipedia/commons/f/f7/Wheeldon_Apartment_Building_-_Portland_Oregon.jpg",
16 | "description": "Convenience is at your doorstep with this charming downtown rental. Great restaurants and active night life are within a few feet."
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/assets/downloads/data/api/rentals/grand-old-mansion.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "type": "rental",
4 | "id": "grand-old-mansion",
5 | "attributes": {
6 | "title": "Grand Old Mansion",
7 | "owner": "Veruca Salt",
8 | "city": "San Francisco",
9 | "location": {
10 | "lat": 37.7749,
11 | "lng": -122.4194
12 | },
13 | "category": "Estate",
14 | "bedrooms": 15,
15 | "image": "https://upload.wikimedia.org/wikipedia/commons/c/cb/Crane_estate_(5).jpg",
16 | "description": "This grand old mansion sits on over 100 acres of rolling hills and dense redwood forests."
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/assets/downloads/data/api/rentals/urban-living.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "type": "rental",
4 | "id": "urban-living",
5 | "attributes": {
6 | "title": "Urban Living",
7 | "owner": "Mike Teavee",
8 | "city": "Seattle",
9 | "location": {
10 | "lat": 47.6062,
11 | "lng": -122.3321
12 | },
13 | "category": "Condo",
14 | "bedrooms": 1,
15 | "image": "https://upload.wikimedia.org/wikipedia/commons/2/20/Seattle_-_Barnes_and_Bell_Buildings.jpg",
16 | "description": "A commuters dream. This rental is within walking distance of 2 bus stops and the Metro."
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/assets/downloads/style.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable no-descending-specificity, media-feature-range-notation */
2 | @import url("https://fonts.googleapis.com/css?family=Lato:300,300italic,400,700,700italic");
3 |
4 | /**
5 | * Base Elements
6 | */
7 |
8 | * {
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
13 | body,
14 | h1,
15 | h2,
16 | h3,
17 | h4,
18 | h5,
19 | h6,
20 | p,
21 | div,
22 | span,
23 | a,
24 | button {
25 | font-family:
26 | Lato, "Open Sans", "Helvetica Neue", "Segoe UI", Helvetica, Arial,
27 | sans-serif;
28 | line-height: 1.5;
29 | }
30 |
31 | body {
32 | background: #f3f3f3;
33 | }
34 |
35 | a {
36 | color: #016aba;
37 | text-decoration: none;
38 | }
39 |
40 | button {
41 | font-size: 100%;
42 | }
43 |
44 | p {
45 | line-height: 1.5;
46 | margin-bottom: 15px;
47 | }
48 |
49 | /**
50 | * Button
51 | */
52 |
53 | .button {
54 | padding: 10px 30px;
55 | text-decoration: none;
56 | color: #fff;
57 | background: #016aba;
58 | border-radius: 5px;
59 | border: none;
60 | font-size: 20px;
61 | font-weight: bold;
62 | opacity: 0.9;
63 | display: inline-block;
64 | }
65 |
66 | .button:hover {
67 | opacity: 1;
68 | }
69 |
70 | /**
71 | * Body Container
72 | */
73 |
74 | .container {
75 | max-width: 1024px;
76 | min-height: 100vh;
77 | background: #f9f9f9;
78 | margin: 0 auto;
79 | }
80 |
81 | /**
82 | * Top Navigation
83 | */
84 |
85 | .menu {
86 | height: 4em;
87 | background-color: #e46855;
88 | }
89 |
90 | .menu h1 {
91 | position: relative;
92 | padding: 5px 0 0 8px;
93 | color: #f9f9f9;
94 | font-size: 1.8em;
95 | font-style: italic;
96 | }
97 |
98 | .menu a,
99 | .menu .links {
100 | display: inline-block;
101 | }
102 |
103 | .menu a {
104 | text-decoration: none;
105 | padding: 0 15px;
106 | color: #fff;
107 | font-size: 20px;
108 | font-weight: bold;
109 | }
110 |
111 | .menu a:hover,
112 | .menu a.active {
113 | opacity: 1;
114 | }
115 |
116 | .menu .links {
117 | padding: 0 21px;
118 | }
119 |
120 | .menu .links a {
121 | position: relative;
122 | bottom: 5px;
123 | }
124 |
125 | .rentals label span {
126 | font-size: 140%;
127 | margin: 50px auto 20px;
128 | display: block;
129 | text-align: center;
130 | font-style: italic;
131 | }
132 |
133 | .rentals form p {
134 | font-size: 80%;
135 | display: block;
136 | text-align: center;
137 | }
138 |
139 | .rentals input {
140 | padding: 11px;
141 | font-size: 18px;
142 | width: 500px;
143 | margin: 20px auto 50px;
144 | background-color: rgb(255 255 255 / 75%);
145 | border: solid 1px lightgray;
146 | display: block;
147 | }
148 |
149 | .menu input:focus {
150 | background-color: #f9f9f9;
151 | outline: none;
152 | }
153 |
154 | .menu button {
155 | margin-right: 15px;
156 | position: relative;
157 | top: -1px;
158 | left: -5px;
159 | border-top-left-radius: 0;
160 | border-bottom-left-radius: 0;
161 | background-color: #262626;
162 | cursor: pointer;
163 | opacity: 1;
164 | }
165 |
166 | .menu button:hover {
167 | background-color: #111;
168 | opacity: 1;
169 | }
170 |
171 | .menu .results {
172 | display: none;
173 | position: absolute;
174 | width: 215px;
175 | top: 54px;
176 | left: 10px;
177 | background-color: #f6f6f6;
178 | border-right: 1px solid rgb(0 0 0 / 5%);
179 | border-bottom: 1px solid rgb(0 0 0 / 5%);
180 | }
181 |
182 | .results {
183 | margin-top: -10px;
184 | }
185 |
186 | .results li {
187 | list-style: none;
188 | padding: 10px 15px;
189 | }
190 |
191 | .menu .results li:hover {
192 | background: #f3f3f3;
193 | }
194 |
195 | /**
196 | * Content Area
197 | */
198 |
199 | .body {
200 | padding: 15px;
201 | }
202 |
203 | /**
204 | * Similar to Jumbotron
205 | */
206 |
207 | .jumbo {
208 | padding: 50px;
209 | background: #f6f6f6;
210 | }
211 |
212 | .jumbo:hover {
213 | background-color: #f3f3f3;
214 | }
215 |
216 | .jumbo h2 {
217 | font-size: 3.2em;
218 | margin-top: -25px;
219 | }
220 |
221 | .jumbo p,
222 | .jumbo address {
223 | margin-bottom: 25px;
224 | }
225 |
226 | .jumbo img {
227 | height: 200px;
228 | position: relative;
229 | top: -25px;
230 | right: -20px;
231 | }
232 |
233 | /**
234 | * Individual Rental Listing
235 | */
236 |
237 | .rental {
238 | margin-top: 15px;
239 | background-color: #f6f6f6;
240 | padding: 20px 25px;
241 | display: flex;
242 | justify-content: space-between;
243 | align-items: center;
244 | flex-wrap: wrap;
245 | }
246 |
247 | .rental:hover {
248 | background-color: #f3f3f3;
249 | }
250 |
251 | .rental img {
252 | border-radius: 5px;
253 | }
254 |
255 | .rental .image {
256 | flex-grow: 0;
257 | flex-basis: 150px;
258 | margin: 20px 25px;
259 | text-align: center;
260 | }
261 |
262 | .rental button.image {
263 | position: relative;
264 | cursor: pointer;
265 | border: none;
266 | background: transparent;
267 | z-index: 1;
268 | }
269 |
270 | .rental button.image:focus {
271 | outline: none;
272 | }
273 |
274 | .rental button.image::after {
275 | content: "";
276 | position: absolute;
277 | top: 0;
278 | left: 0;
279 | width: 100%;
280 | height: 100%;
281 | z-index: -1;
282 | margin: -20px;
283 | padding: 20px;
284 | border-radius: 5px;
285 | background: #016aba;
286 | opacity: 0;
287 | transition: opacity 0.25s ease-in-out;
288 | }
289 |
290 | .rental button.image:focus::after,
291 | .rental button.image:hover::after {
292 | opacity: 0.1;
293 | }
294 |
295 | .rental .image img {
296 | max-width: 100%;
297 | }
298 |
299 | .rental .image.large {
300 | margin: 30px 25px 50px;
301 | flex-basis: 100%;
302 | }
303 |
304 | .rental .image small {
305 | display: block;
306 | margin-top: 5px;
307 | margin-bottom: -15px;
308 | text-align: center;
309 | color: #016aba;
310 |
311 | /* This is needed to fix a safari clipping issue */
312 | position: relative;
313 | }
314 |
315 | .rental .image.large small {
316 | margin-top: 10px;
317 | margin-bottom: 0;
318 | font-size: 110%;
319 | }
320 |
321 | .rental .details {
322 | flex-basis: 50%;
323 | flex-grow: 2;
324 | display: flex;
325 | height: 150px;
326 | margin: 20px 25px;
327 | place-content: space-around space-between;
328 | flex-wrap: wrap;
329 | }
330 |
331 | .rental h3 {
332 | flex-basis: 100%;
333 | }
334 |
335 | .rental h3 a {
336 | display: inline;
337 | }
338 |
339 | .rental .detail {
340 | flex-basis: 50%;
341 | font-weight: 300;
342 | font-style: italic;
343 | white-space: nowrap;
344 | }
345 |
346 | .rental .detail span {
347 | font-weight: 400;
348 | font-style: normal;
349 | }
350 |
351 | .rental .map {
352 | flex-grow: 0;
353 | flex-basis: 150px;
354 | font-size: 0;
355 | margin: 0 25px;
356 | }
357 |
358 | .rental .map img {
359 | width: 150px;
360 | height: 150px;
361 | }
362 |
363 | .rental.detailed {
364 | background: none;
365 | align-items: flex-start;
366 | }
367 |
368 | .rental.detailed .image {
369 | flex-basis: 320px;
370 | }
371 |
372 | .rental.detailed .image.large {
373 | margin: 30px 25px 50px;
374 | flex-basis: 100%;
375 | }
376 |
377 | .rental.detailed .details {
378 | height: auto;
379 | }
380 |
381 | .rental.detailed h3 {
382 | font-size: 200%;
383 | margin-bottom: 10px;
384 | }
385 |
386 | .rental.detailed .detail {
387 | margin: 5px 0;
388 | flex-basis: 100%;
389 | flex-shrink: 2;
390 | }
391 |
392 | .rental.detailed .description {
393 | white-space: normal;
394 | flex-basis: 100%;
395 | flex-shrink: 1;
396 | }
397 |
398 | .rental.detailed .map {
399 | flex-basis: 100%;
400 | margin: 50px 25px 25px;
401 | }
402 |
403 | .rental.detailed .map img {
404 | width: 100%;
405 | height: auto;
406 | }
407 |
408 | @media only screen and (max-width: 919px) {
409 | .rental.detailed .image,
410 | .rental.detailed .image.large {
411 | margin: 30px 25px 25px;
412 | flex-basis: 100%;
413 | cursor: default;
414 | }
415 |
416 | .rental.detailed .image:hover {
417 | flex-basis: 100%;
418 | cursor: default;
419 | }
420 |
421 | .rental.detailed .image small {
422 | display: none;
423 | }
424 |
425 | .rental.detailed button.image:hover::after {
426 | opacity: 0;
427 | }
428 |
429 | .rental.detailed button.image:focus::after {
430 | opacity: 0.1;
431 | }
432 |
433 | .rental.detailed .map {
434 | margin-top: 25px;
435 | }
436 | }
437 |
438 | /**
439 | * Utilities
440 | */
441 |
442 | .light {
443 | font-weight: 300;
444 | }
445 |
446 | .left {
447 | float: left;
448 | }
449 |
450 | .right {
451 | float: right;
452 | }
453 |
454 | .hidden {
455 | display: none;
456 | }
457 |
458 | .relative {
459 | position: relative;
460 | }
461 |
462 | .tomster {
463 | background: url("../assets/images/teaching-tomster.png");
464 | background-size: contain;
465 | background-repeat: no-repeat;
466 | height: 200px;
467 | width: 200px;
468 | position: relative;
469 | top: -25px;
470 | }
471 |
472 | .screen-reader {
473 | position: absolute;
474 | overflow: hidden;
475 | clip: rect(0 0 0 0);
476 | height: 1px;
477 | width: 1px;
478 | margin: -1px;
479 | padding: 0;
480 | border: 0;
481 | }
482 |
483 | /* stylelint-enable no-descending-specificity, media-feature-range-notation */
484 |
--------------------------------------------------------------------------------
/src/assets/downloads/teaching-tomster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ember-learn/super-rentals-tutorial/43f1a8e6605a13775ebce3422e6ca3b5109170eb/src/assets/downloads/teaching-tomster.png
--------------------------------------------------------------------------------
/src/bin/generate.ts:
--------------------------------------------------------------------------------
1 | import * as github from '@actions/core';
2 | import chalk from 'chalk';
3 | import {
4 | readFile as _readFile,
5 | writeFile as _writeFile
6 | } from 'fs';
7 | import _glob from 'glob';
8 | import mkdirp from 'mkdirp';
9 | import { ncp as _ncp } from 'ncp';
10 | import { basename, dirname, join, relative, sep } from 'path';
11 | import frontmatter from 'remark-frontmatter';
12 | import markdown from 'remark-parse';
13 | import yaml from 'remark-parse-yaml';
14 | import stringify from 'remark-stringify';
15 | import unified, { Processor } from 'unified';
16 | import { promisify } from 'util';
17 | import { VFileOptions } from 'vfile';
18 |
19 | import { doNotEdit, retinaImages, runCodeBlocks, todoLinks, zoeySays } from '../lib';
20 |
21 | const glob = promisify(_glob);
22 | const readFile = promisify(_readFile);
23 | const writeFile = promisify(_writeFile);
24 | const ncp = promisify(_ncp);
25 |
26 | // 01-orientation.md -> orientation.md
27 | function unprefix(path: string): string {
28 | let parts = path.split(sep);
29 | parts = parts.map(p => p.replace(/^[0-9]+-/, ''));
30 | return join(...parts);
31 | }
32 |
33 | // Group a related section of the logs. On CI this makes the section foldable.
34 | async function group(name: string, callback: () => Promise): Promise {
35 | if (process.env.CI) {
36 | return github.group(name, callback);
37 | } else {
38 | console.log(chalk.yellow(name));
39 | return callback();
40 | }
41 | }
42 |
43 | async function run(processor: Processor, inputPath: string, outputPath: string, options: VFileOptions): Promise {
44 | let contents = await readFile(inputPath, { encoding: 'utf8' });
45 | let result = await processor.process({ ...options, contents });
46 | await writeFile(outputPath, result.toString(), { encoding: 'utf8' });
47 | }
48 |
49 | async function main() {
50 | let project = process.cwd();
51 | let assetsDir = join(project, 'dist', 'assets');
52 | let srcDir = join(project, 'src', 'markdown')
53 | let outDir = join(project, 'dist', 'markdown');
54 | let codeDir = join(project, 'dist', 'code');
55 |
56 | await ncp(join(project, 'src', 'assets'), assetsDir);
57 | await mkdirp(outDir, {});
58 | await mkdirp(codeDir, {});
59 |
60 | let pattern = process.argv[2] || join('src', 'markdown', '**', '*.md');
61 |
62 | let inputPaths = await glob(pattern);
63 |
64 | let processor = unified()
65 | .use(markdown)
66 | .use(frontmatter)
67 | .use(runCodeBlocks, { cfg: process.env.CI ? ['ci'] : [], cwd: codeDir, assets: assetsDir })
68 | .use(todoLinks)
69 | .use(zoeySays)
70 | .use(doNotEdit, { repo: 'ember-learn/super-rentals-tutorial' })
71 | .use(stringify, { fences: true, listItemIndent: '1' })
72 | .use(yaml);
73 |
74 | let outputPaths: Map = new Map();
75 |
76 | for (let inputPath of inputPaths) {
77 | await group(`Processing ${inputPath}`, async () => {
78 | let dir = unprefix(relative(srcDir, dirname(inputPath)));
79 | let name = unprefix(basename(inputPath));
80 |
81 | await mkdirp(join(outDir, dir), {});
82 |
83 | let outputPath = join(outDir, dir, name);
84 |
85 | let options: VFileOptions = {
86 | path: join(dir, name),
87 | cwd: outDir,
88 | data: { originalPath: inputPath }
89 | }
90 |
91 | outputPaths.set(outputPath, options);
92 |
93 | await run(processor, inputPath, outputPath, options);
94 | });
95 | }
96 |
97 | let postProcessor = unified()
98 | .use(markdown)
99 | .use(frontmatter)
100 | .use(retinaImages, { assets: assetsDir })
101 | .use(stringify, { fences: true, listItemIndent: '1' })
102 | .use(yaml);
103 |
104 | for (let [outputPath, options] of outputPaths) {
105 | await group(`Post-processing ${relative(project, outputPath)}`, async () =>
106 | await run(postProcessor, outputPath, outputPath, options)
107 | );
108 | }
109 | }
110 |
111 | main().catch(error => {
112 | console.error(error);
113 | process.exit(1);
114 | });
115 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { default as doNotEdit } from './plugins/do-not-edit';
2 | export { default as retinaImages } from './plugins/retina-images';
3 | export { default as runCodeBlocks } from './plugins/run-code-blocks';
4 | export { default as todoLinks } from './plugins/todo-links';
5 | export { default as zoeySays } from './plugins/zoey-says';
6 |
--------------------------------------------------------------------------------
/src/lib/plugins/do-not-edit/index.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, Transformer } from 'unified';
2 | import { Node } from 'unist';
3 | import { VFile } from 'vfile';
4 | import Options from './options';
5 | import Walker from './walker';
6 |
7 | function attacher(options: Options): Transformer {
8 | async function transform(node: Node, file: VFile): Promise {
9 | return new Walker(options, file).walk(node);
10 | }
11 |
12 | return transform;
13 | }
14 |
15 | const plugin: Plugin<[Options]> = attacher;
16 |
17 | export default plugin;
18 |
--------------------------------------------------------------------------------
/src/lib/plugins/do-not-edit/options.ts:
--------------------------------------------------------------------------------
1 | export default interface Options {
2 | repo: string;
3 | branch?: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/lib/plugins/do-not-edit/walker.ts:
--------------------------------------------------------------------------------
1 | import { HTML, Root } from 'mdast';
2 | import { Option } from 'ts-std';
3 | import { VFile } from 'vfile';
4 |
5 | import BaseWalker from '../../walker';
6 | import Options from './options';
7 |
8 | function makeComment(repo: string, branch?: string, path?: string): HTML {
9 | let url = `https://github.com/${repo}`;
10 |
11 | if (path) {
12 | branch = branch || 'master';
13 | url = `${url}/blob/${branch}/${path}`;
14 | }
15 |
16 | let message = `Heads up! This is a generated file, do not edit directly. You can find the source at ${url}`;
17 |
18 | return {
19 | type: 'html',
20 | value: ``
21 | };
22 | }
23 |
24 | export default class DoNotEditWalker extends BaseWalker {
25 | protected async root(node: Root): Promise {
26 | let { repo, branch } = this.options;
27 | let originalPath = originalPathFor(this.file);
28 |
29 | if (originalPath === null) {
30 | return node;
31 | }
32 |
33 | let comment = makeComment(repo, branch, originalPath);
34 |
35 | if (hasFrontMatter(node)) {
36 | let [frontMatter, ...rest] = node.children;
37 |
38 | return {
39 | ...node,
40 | children: [frontMatter, comment, ...rest]
41 | };
42 | } else {
43 | return {
44 | ...node,
45 | children: [comment, ...node.children]
46 | };
47 | }
48 | }
49 | }
50 |
51 | function hasData(file: VFile): file is VFile & { data: { [key: string]: any } } {
52 | return typeof file.data === 'object' && file.data !== null;
53 | }
54 |
55 | function originalPathFor(file: VFile): Option {
56 | if (hasData(file) && typeof file.data.originalPath === 'string') {
57 | return file.data.originalPath;
58 | } else {
59 | return null;
60 | }
61 | }
62 |
63 | function hasFrontMatter(node: Root): boolean {
64 | return node.children.length > 0 && node.children[0].type === 'yaml';
65 | }
66 |
--------------------------------------------------------------------------------
/src/lib/plugins/retina-images/index.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, Transformer } from 'unified';
2 | import { Node } from 'unist';
3 | import { VFile } from 'vfile';
4 | import Options from './options';
5 | import Walker from './walker';
6 |
7 | function attacher(options: Options): Transformer {
8 | async function transform(node: Node, file: VFile): Promise {
9 | return new Walker(options, file).walk(node);
10 | }
11 |
12 | return transform;
13 | }
14 |
15 | const plugin: Plugin<[Options]> = attacher;
16 |
17 | export default plugin;
18 |
--------------------------------------------------------------------------------
/src/lib/plugins/retina-images/options.ts:
--------------------------------------------------------------------------------
1 | export default interface Options {
2 | assets: string;
3 | }
4 |
--------------------------------------------------------------------------------
/src/lib/plugins/retina-images/walker.ts:
--------------------------------------------------------------------------------
1 | import { exists as _exists } from 'fs';
2 | import _imageSize from 'image-size';
3 | import { HTML, Image } from 'mdast';
4 | import { basename, resolve } from 'path';
5 | import { expect } from 'ts-std';
6 | import { promisify } from 'util';
7 | import BaseWalker from '../../walker';
8 | import Options from './options';
9 |
10 | const exists = promisify(_exists);
11 | const imageSize = promisify(_imageSize);
12 |
13 | function pathFor(src: string, options: Options): string {
14 | return resolve(options.assets, `.${src}`);
15 | }
16 |
17 | async function isRetinaImage(src: string, options: Options): Promise {
18 | return basename(src, '.png').endsWith('@2x') &&
19 | src.startsWith('/') &&
20 | await exists(pathFor(src, options));
21 | }
22 |
23 | function attr(v: unknown): string {
24 | let escaped = String(v).replace(/[<>"]/g, c => {
25 | switch (c) {
26 | case '<':
27 | return '<';
28 | case '>':
29 | return '>';
30 | case '"':
31 | return '"';
32 | default:
33 | throw new Error(`Unknown character: \`${c}\``);
34 | }
35 | });
36 |
37 | return JSON.stringify(escaped);
38 | }
39 |
40 |
41 | async function toImgTag(node: Image, options: Options): Promise {
42 | let sizeOfImage = await imageSize(pathFor(node.url, options));
43 |
44 | let size = expect(sizeOfImage, 'size should be present');
45 | let width = expect(size.width, 'width should be present');
46 | let height = expect(size.height, 'height should be present');
47 |
48 | let widthAttr = Math.floor(width / 2);
49 | let heightAttr = Math.floor(height / 2);
50 |
51 | let attrs = [];
52 |
53 | attrs.push(`src=${attr(node.url)}`);
54 |
55 | if (node.alt) {
56 | attrs.push(`alt=${attr(node.alt)}`);
57 | }
58 |
59 | if (node.title) {
60 | attrs.push(`title=${attr(node.title)}`);
61 | }
62 |
63 | attrs.push(`width=${attr(widthAttr)}`);
64 | attrs.push(`height=${attr(heightAttr)}`);
65 |
66 | return {
67 | type: 'html',
68 | value: `
`,
69 | position: node.position,
70 | data: node.data
71 | };
72 | }
73 |
74 | export default class RetinaImages extends BaseWalker {
75 | protected async image(node: Image): Promise {
76 | if (await isRetinaImage(node.url, this.options)) {
77 | return toImgTag(node, this.options);
78 | } else {
79 | return node;
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/lib/plugins/run-code-blocks/cfg.ts:
--------------------------------------------------------------------------------
1 | import { Option } from 'ts-std';
2 | import ParseError from './parse-error';
3 |
4 | export interface ConfigurationPredicate {
5 | eval(cfg: string[]): boolean;
6 | }
7 |
8 | class ConfigurationOption implements ConfigurationPredicate {
9 | static parse(input: string, lineno: Option): ConfigurationOption {
10 | if (input === '' || /\(|\)|,/.test(input)) {
11 | throw new ParseError('expecting a single cfg predicate', lineno);
12 | }
13 |
14 | return new this(input);
15 | }
16 |
17 | private constructor(private option: string) {}
18 |
19 | eval(cfg: string[]): boolean {
20 | return cfg.includes(this.option);
21 | }
22 | }
23 |
24 | class ConfigurationAll implements ConfigurationPredicate {
25 | static parse(input: string, lineno: Option): Option {
26 | let match = /^all\((.+)\)$/.exec(input);
27 |
28 | if (match) {
29 | return new this(parsePredicates(match[1], lineno));
30 | } else {
31 | return null;
32 | }
33 | }
34 |
35 | private constructor(private predicates: ConfigurationPredicate[]) {}
36 |
37 | eval(cfg: string[]): boolean {
38 | return this.predicates.every(p => p.eval(cfg));
39 | }
40 | }
41 |
42 | class ConfigurationAny implements ConfigurationPredicate {
43 | static parse(input: string, lineno: Option): Option {
44 | let match = /^any\((.+)\)$/.exec(input);
45 |
46 | if (match) {
47 | return new this(parsePredicates(match[1], lineno));
48 | } else {
49 | return null;
50 | }
51 | }
52 |
53 | private constructor(private predicates: ConfigurationPredicate[]) {}
54 |
55 | eval(cfg: string[]): boolean {
56 | return this.predicates.some(p => p.eval(cfg));
57 | }
58 | }
59 |
60 | class ConfigurationNot implements ConfigurationPredicate {
61 | static parse(input: string, lineno: Option): Option {
62 | let match = /^not\((.+)\)$/.exec(input);
63 |
64 | if (match) {
65 | return new this(parse(match[1], lineno));
66 | } else {
67 | return null;
68 | }
69 | }
70 |
71 | private constructor(private predicate: ConfigurationPredicate) {}
72 |
73 | eval(cfg: string[]): boolean {
74 | return !this.predicate.eval(cfg);
75 | }
76 | }
77 |
78 | export default function parse(input: string, lineno: Option): ConfigurationPredicate {
79 | return ConfigurationAll.parse(input, lineno) ||
80 | ConfigurationAny.parse(input, lineno) ||
81 | ConfigurationNot.parse(input, lineno) ||
82 | ConfigurationOption.parse(input, lineno);
83 | }
84 |
85 | function parsePredicates(input: string, lineno: Option): ConfigurationPredicate[] {
86 | let items = input.split(',').map(item => item.trim());
87 |
88 | // Trailing comma
89 | if (items.length > 0 && items[items.length - 1] === '') {
90 | items.pop();
91 | }
92 |
93 | if (items.length === 0) {
94 | throw new ParseError('expecting at least one cfg predicate', lineno);
95 | }
96 |
97 | return items.map(item => parse(item, lineno));
98 | }
99 |
--------------------------------------------------------------------------------
/src/lib/plugins/run-code-blocks/commands.ts:
--------------------------------------------------------------------------------
1 | import { Option } from 'ts-std';
2 | import parseCfg, { ConfigurationPredicate } from './cfg';
3 | import LineNo, { end, offset } from './lineno';
4 | import ParseError from './parse-error';
5 |
6 | export class Command {
7 | constructor(readonly command: string, readonly display: string) {}
8 | }
9 |
10 | interface PartialCommand {
11 | cfg?: ConfigurationPredicate;
12 | display?: string;
13 | command?: string;
14 | }
15 |
16 | export function parseCommands(input: string, cfg: string[], lineno: LineNo): Command[] {
17 | let commands: Command[] = [];
18 |
19 | let current: Option = null;
20 |
21 | let initialize = (): PartialCommand => {
22 | if (current === null) {
23 | current = {};
24 | }
25 |
26 | return current;
27 | };
28 |
29 | let finalize = (ln: LineNo) => {
30 | if (!current) {
31 | return;
32 | }
33 |
34 | if (current.command === undefined) {
35 | throw new ParseError('expecting command', ln);
36 | }
37 |
38 | if (current.command.trimRight().endsWith('\\')) {
39 | throw new ParseError('expecting command (invalid line-continuation)', ln);
40 | }
41 |
42 | if (current.cfg && !current.cfg.eval(cfg)) {
43 | current = null;
44 | return;
45 | }
46 |
47 | commands.push(new Command(current.command, current.display || current.command));
48 | current = null;
49 | };
50 |
51 | let lines = input.split('\n');
52 |
53 | for (let [i, line] of lines.entries()) {
54 | let ln = offset(lineno, i + 1);
55 |
56 | if (line.trim() === '') {
57 | finalize(ln);
58 | continue;
59 | }
60 |
61 | let config = /^#\[cfg\((.+)\)\]$/.exec(line);
62 |
63 | if (config) {
64 | let partial = initialize();
65 |
66 | if (partial.cfg !== undefined) {
67 | throw new ParseError('only one `#[cfg(...)]` allowed per command', ln);
68 | }
69 |
70 | partial.cfg = parseCfg(config[1], ln);
71 |
72 | continue;
73 | }
74 |
75 | let display = /^#\[display\((.+)\)\]$/.exec(line);
76 |
77 | if (display) {
78 | let partial = initialize();
79 |
80 | if (partial.display !== undefined) {
81 | throw new ParseError('only one `#[display(...)]` allowed for each command', ln);
82 | }
83 |
84 | partial.display = display[1];
85 |
86 | continue;
87 | }
88 |
89 | let unknown = /^#\[.+\]$/.exec(line);
90 |
91 | if (unknown) {
92 | throw new ParseError(`invalid directive \`${line}\``, ln);
93 | }
94 |
95 | if (line.startsWith('#')) {
96 | finalize(ln);
97 | continue;
98 | }
99 |
100 | {
101 | let partial = initialize();
102 |
103 | if (partial.command) {
104 | partial.command = `${partial.command}\n${line}`;
105 | } else {
106 | partial.command = line;
107 | }
108 |
109 | if (!partial.command.trimRight().endsWith('\\')) {
110 | finalize(ln);
111 | }
112 | }
113 | }
114 |
115 | finalize(lines.length);
116 |
117 | return commands;
118 | }
119 |
120 | export function parseCommand(input: string, cfg: string[], lineno: LineNo): Command {
121 | let commands = parseCommands(input, cfg, lineno);
122 |
123 | if (commands.length === 1) {
124 | return commands[0];
125 | } else {
126 | throw new ParseError(`expecting exactly 1 command, got ${commands.length}`, end(lineno));
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/lib/plugins/run-code-blocks/directives/checkpoint.ts:
--------------------------------------------------------------------------------
1 | import { exec as _exec } from 'child_process';
2 | import { Code } from 'mdast';
3 | import { join } from 'path';
4 | import { assert } from 'ts-std';
5 | import { promisify } from 'util';
6 | import Options from '../options';
7 | import parseArgs, { ToBool, optional } from '../parse-args';
8 |
9 | const exec = promisify(_exec);
10 |
11 | interface Args {
12 | cwd?: string;
13 | commit?: boolean;
14 | }
15 |
16 | export default async function checkpoint(node: Code, options: Options): Promise {
17 | let args = parseArgs(node, [
18 | optional('cwd', String),
19 | optional('commit', ToBool, true)
20 | ]);
21 |
22 | let message = node.value;
23 | let [title] = message.split('\n');
24 |
25 | let { cwd } = options;
26 |
27 | if (args.cwd) {
28 | cwd = join(cwd, args.cwd);
29 | }
30 |
31 | console.log(`$ yarn test`);
32 |
33 | await exec('yarn test', { cwd });
34 |
35 | if (args.commit) {
36 | console.log(`$ git commit -m ${JSON.stringify(title)}`);
37 |
38 | let promise = exec('git commit -F -', { cwd });
39 |
40 | if (!message.endsWith('\n')) {
41 | message = `${message}\n`;
42 | }
43 |
44 | promise.child.stdin!.end(message, 'utf8');
45 |
46 | await promise;
47 | }
48 |
49 | let { stdout } = await exec('git status --short', { cwd });
50 |
51 | assert(stdout === '', `Unexpected dirty files!\n\n${stdout}`);
52 |
53 | return null;
54 | }
55 |
--------------------------------------------------------------------------------
/src/lib/plugins/run-code-blocks/directives/command.ts:
--------------------------------------------------------------------------------
1 | import { exec as _exec } from 'child_process';
2 | import { Code } from 'mdast';
3 | import { join } from 'path';
4 | import { Option, assert } from 'ts-std';
5 | import { promisify } from 'util';
6 | import { parseCommands } from '../commands';
7 | import Options from '../options';
8 | import parseArgs, { ToBool, optional } from '../parse-args';
9 |
10 | const exec = promisify(_exec);
11 |
12 | interface Args {
13 | lang?: string;
14 | hidden?: boolean;
15 | cwd?: string;
16 | captureCommand?: boolean;
17 | captureOutput?: boolean;
18 | }
19 |
20 | export default async function command(node: Code, options: Options): Promise
145 | + Contact Us
146 |
147 | ```
148 |
149 | ```run:file:patch lang=handlebars cwd=super-rentals filename=app/templates/contact.hbs
150 | @@ -16,2 +16,3 @@
151 |
152 | + About
153 |
154 | ```
155 |
156 | There is quite a bit going on here, so let's break it down.
157 |
158 | `` is an example of a *[component](../../../components/introducing-components/)* in Ember—you can tell them apart from regular HTML tags because they start with an uppercase letter. Along with regular HTML tags, components are a key building block that we can use to build up an app's user interface.
159 |
160 | We have a lot more to say about components later, but for now, you can think of them as a way to provide *[custom tags][TODO: link to custom tags]* to supplement the built-in ones that came with the browser.
161 |
162 | The `@route=...` part is how we pass *[arguments](../../../components/component-arguments-and-html-attributes/)* into the component. Here, we use this argument to specify *which* route we want to link to. (Note that this should be the *name* of the route, not the path, which is why we specified `"about"` instead of `"/about"`, and `"contact"` instead of `"/getting-in-touch"`.)
163 |
164 | In addition to arguments, components can also take the usual HTML attributes as well. In our example, we added a `"button"` class for styling purposes, but we could also specify other attributes as we see fit, such as the [ARIA](https://webaim.org/techniques/aria/) [`role` attribute](https://developer.mozilla.org/docs/Web/Accessibility/ARIA/Roles). These are passed without the `@` symbol (`class=...` as opposed to `@class=...`), so that Ember will know they are just regular HTML attributes.
165 |
166 | Under the hood, the `` component generates a regular `` tag for us with the appropriate `href` for the specific route. This `` tag works just fine with *[screen readers](https://webaim.org/projects/screenreadersurvey/)*, as well as allowing our users to bookmark the link or open it in a new tab.
167 |
168 | However, when clicking on one of these special links, Ember will intercept the click, render the content for the new page, and update the URL—all performed locally without having to wait for the server, thus avoiding a full page refresh.
169 |
170 |
171 |
172 | ```run:screenshot width=1024 retina=true filename=index-with-link.png alt="Index page after adding the link"
173 | visit http://localhost:4200/?deterministic
174 | ```
175 |
176 | ```run:screenshot width=1024 retina=true filename=about-with-link.png alt="About page after adding the link"
177 | visit http://localhost:4200/about?deterministic
178 | ```
179 |
180 | ```run:screenshot width=1024 retina=true filename=contact-with-link.png alt="Contact page after adding the link"
181 | visit http://localhost:4200/getting-in-touch?deterministic
182 | ```
183 |
184 | ```run:command hidden=true cwd=super-rentals
185 | git add app/templates/index.hbs
186 | git add app/templates/about.hbs
187 | git add app/templates/contact.hbs
188 | ```
189 |
190 | We will learn more about how all of this works soon. In the meantime, go ahead and click on the link in the browser. Did you notice how snappy that was?
191 |
192 | Congratulations, you are well on your way to becoming a master page-crafter!
193 |
194 | ```run:server:stop
195 | npm start
196 | ```
197 |
198 | ```run:checkpoint cwd=super-rentals
199 | Chapter 2
200 | ```
201 |
--------------------------------------------------------------------------------
/src/markdown/tutorial/part-1/03-automated-testing.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ```run:server:start hidden=true cwd=super-rentals expect="Serving on http://localhost:4200/"
4 | npm start
5 | ```
6 |
7 | In this chapter, you will use Ember's built-in testing framework to write some automated tests for your app. By the end of this chapter, we will have an automated test suite that we can run to ensure our app is working correctly:
8 |
9 | 
10 |
11 | In the process, you will learn about:
12 |
13 | * The purpose of automated testing
14 | * Writing acceptance tests
15 | * Using generators in Ember CLI
16 | * Testing with the QUnit test framework
17 | * Working with Ember's test helpers
18 | * Practicing the testing workflow
19 |
20 | ## The Purpose of Automated Testing
21 |
22 | We accomplished a lot in the last few chapters! Let's recap—we started with a blank canvas, added a few pages of content, styled everything to look pretty, dropped in a picture of Tomster, added links between our pages and amazingly, everything worked together flawlessly!
23 |
24 | But do we *really* know that everything is actually working? Sure, we clicked around a bit to confirm that things look as expected. But do we feel confident that we checked *every* page after the most recent change that we made?
25 |
26 | After all, most of us have experienced (or heard horror stories about) making a Small Tweak™ in one area of the app that inadvertently broke *everything else* when we weren't looking.
27 |
28 | Maybe we can write a checklist somewhere of all the things to check after making changes to our site. But surely, this will get out of hand as we add more features to our app. It is also going to get old really quickly—repetitive tasks like that are best left to robots.
29 |
30 | Hmm, robots. That's an idea. What if we can write this checklist and just get the computer to check everything for us? I think we just invented the idea of *[automated testing](../../../testing/)*! Okay, maybe we were not the first to come up with the concept, but we independently discovered it so we still deserve some credit.
31 |
32 | ## Adding Acceptance Tests with Generators
33 |
34 | Once we are done patting ourselves on the back, go ahead and run the following command in the terminal:
35 |
36 | ```run:command cwd=super-rentals
37 | ember generate acceptance-test super-rentals
38 | ```
39 |
40 | This is called a *[generator](https://cli.emberjs.com/release/basic-use/cli-commands/#generatemorefiles)* command in Ember CLI. Generators automatically create files for us based on Ember's conventions and populate them with the appropriate boilerplate content, similar to how `ember new` initially created a skeleton app for us. It typically follows the pattern `ember generate `, where `` is the kind of thing we are generating, and `` is what we want to call it.
41 |
42 | In this case, we generated an *[acceptance test](../../../testing/test-types/#toc_application-tests)* located at `tests/acceptance/super-rentals-test.js`.
43 |
44 | ```run:command hidden=true cwd=super-rentals
45 | git add tests/acceptance/super-rentals-test.js
46 | ```
47 |
48 | Generators aren't required; we *could* have created the file ourselves which would have accomplished the exact same thing. But, generators certainly save us a lot of typing. Go ahead and take a peek at the acceptance test file and see for yourself.
49 |
50 | > Zoey says...
51 | >
52 | > Want to save even more typing? `ember generate ...` can be shortened into `ember g ...`. That's 7 fewer characters!
53 |
54 | ## Writing Acceptance Tests
55 |
56 | Acceptance tests, also known as *application tests*, are one of a few types of automated testing at our disposal in Ember. We will learn about the other types later, but what makes acceptance tests unique is that they test our app from the user's perspective—they are an automated version of the "click around and see if it works" testing we did earlier, which is exactly what we need.
57 |
58 | Let's open the generated test file and replace the boilerplate test with our own:
59 |
60 | ```run:file:patch lang=js cwd=super-rentals filename=tests/acceptance/super-rentals-test.js
61 | @@ -1,3 +1,3 @@
62 | import { module, test } from 'qunit';
63 | -import { visit, currentURL } from '@ember/test-helpers';
64 | +import { click, visit, currentURL } from '@ember/test-helpers';
65 | import { setupApplicationTest } from 'super-rentals/tests/helpers';
66 | @@ -7,6 +7,12 @@
67 |
68 | - test('visiting /super-rentals', async function (assert) {
69 | - await visit('/super-rentals');
70 | -
71 | - assert.strictEqual(currentURL(), '/super-rentals');
72 | + test('visiting /', async function (assert) {
73 | + await visit('/');
74 | +
75 | + assert.strictEqual(currentURL(), '/');
76 | + assert.dom('h2').hasText('Welcome to Super Rentals!');
77 | +
78 | + assert.dom('.jumbo a.button').hasText('About Us');
79 | + await click('.jumbo a.button');
80 | +
81 | + assert.strictEqual(currentURL(), '/about');
82 | });
83 | ```
84 |
85 | First, we instruct the test robot to navigate to the `/` URL of our app by using the `visit` *test helper* provided by Ember. This is akin to us typing `http://localhost:4200/` in the browser's address bar and hitting the `enter` key.
86 |
87 | Because the page is going to take some time to load, this is known as an *[async](https://developer.mozilla.org/docs/Learn/JavaScript/Asynchronous/Concepts)* (short for *asynchronous*) step, so we will need to tell the test robot to wait by using JavaScript's `await` keyword. That way, it will wait until the page completely finishes loading before moving on to the next step.
88 |
89 | This is almost always the behavior we want, so we will almost always use `await` and `visit` as a pair. This applies to other kinds of simulated interaction too, such as clicking on a button or a link, as they all take time to complete. Even though sometimes these actions may seem imperceptibly fast to us, we have to remember that our test robot has really, really fast hands, as we will see in a moment.
90 |
91 | After navigating to the `/` URL and waiting for things to settle, we check that the current URL matches the URL that we expect (`/`). We can use the `currentURL` test helper here, as well as `equal` *[assertion](https://github.com/emberjs/ember-test-helpers/blob/master/API.md)*. This is how we encode our "checklist" into code—by specifying, or asserting how things *should* behave, we will be alerted if our app does *not* behave in the way that we expect.
92 |
93 | Next, we confirmed that the page has an `