diff --git a/.dockerignore b/.dockerignore index fb31961..e2b0b51 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,7 +15,6 @@ !sample/sample.sqlite3 !viewer/**/*.py -!viewer/static/download-files !viewer/static_src !viewer/templates diff --git a/.gitignore b/.gitignore index e138b05..7d208c6 100644 --- a/.gitignore +++ b/.gitignore @@ -63,8 +63,6 @@ venv *.egg-info .installed.cfg viewer/static/** -!viewer/static/download-files -!viewer/static/download-files/* # Unit test / coverage reports ################# diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..dc462ec --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +.github +.tox + +htmlcov +sample +venv +viewer/static diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..edd6acc --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Django runserver", + "type": "debugpy", + "request": "launch", + "args": ["runserver"], + "django": true, + "autoStartBrowser": false, + "program": "${workspaceFolder}/manage.py" + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec64336..17c1144 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,11 +5,10 @@ > feature request, you are agreeing to comply with this waiver of copyright interest. > Details can be found in our [TERMS](TERMS.md) and [LICENSE](LICENSE). - There are two primary ways to help: - - Using the issue tracker, and - - Changing the code-base. +- Using the issue tracker, and +- Changing the code-base. ## Using the issue tracker @@ -21,7 +20,6 @@ Use the issue tracker to find ways to contribute. Find a bug or a feature, menti the issue that you will take on that effort, then follow the _Changing the code-base_ guidance below. - ## Changing the code-base Generally speaking, you should fork this repository, make changes in your @@ -31,7 +29,6 @@ Additionally, the code should follow any stylistic and architectural guidelines prescribed by the project. In the absence of such guidelines, mimic the styles and patterns in the existing code-base. - ## Browser support We configure our build chain tools diff --git a/README.md b/README.md index ccd924b..8dc1f99 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,20 @@ sqlite> SELECT url FROM crawler_page WHERE html LIKE "%%" ORDER BY URL asc; ## Running the viewer application -From the repo's root, compile front-end assets: +From the repo's root, compile frontend assets: ``` yarn yarn build ``` +Alternatively, to continuously watch the frontend assets and rebuild as necessary: + +``` +yarn +yarn watch +``` + Create a Python virtual environment and install required packages: ``` @@ -205,7 +212,7 @@ yarn prettier You can fix any problems by running: ``` -yarn fix +yarn prettier:fix ``` ### Sample test data @@ -291,9 +298,10 @@ The `deploy` command: See [fabfile.py](fabfile.py) for additional detail. ----- +--- ## Open source licensing info + 1. [TERMS](TERMS.md) 2. [LICENSE](LICENSE) 3. [CFPB Source Code Policy](https://github.com/cfpb/source-code-policy/) diff --git a/TERMS.md b/TERMS.md index f64c133..a0268df 100644 --- a/TERMS.md +++ b/TERMS.md @@ -1,7 +1,7 @@ As a work of the United States Government, this package (excluding any exceptions listed below) is in the public domain within the United States. Additionally, we waive copyright and related rights in the work worldwide -through the [CC0 1.0 Universal public domain dedication][CC0]. +through the [CC0 1.0 Universal public domain dedication][cc0]. Software source code previously released under an open source license and then modified by CFPB staff or its contractors is considered a "joint work" @@ -14,10 +14,9 @@ rights for that work are waived through the CC0 1.0 Universal dedication. For further details, please see the CFPB [Source Code Policy][policy]. - ## CC0 1.0 Universal Summary -This is a human-readable summary of the [Legal Code (read the full text)][CC0]. +This is a human-readable summary of the [Legal Code (read the full text)][cc0]. ### No Copyright @@ -26,7 +25,7 @@ the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. -You can copy, modify, distribute and perform the work, even for commercial +You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. See Other Information below. ### Other Information @@ -42,8 +41,7 @@ When using or citing the work, you should not imply endorsement by the author or the affirmer. [policy]: https://github.com/cfpb/source-code-policy/ -[CC0]: http://creativecommons.org/publicdomain/zero/1.0/legalcode - +[cc0]: http://creativecommons.org/publicdomain/zero/1.0/legalcode ## Exceptions diff --git a/esbuild.mjs b/esbuild.mjs new file mode 100644 index 0000000..a0acd49 --- /dev/null +++ b/esbuild.mjs @@ -0,0 +1,89 @@ +import { context } from "esbuild"; +import { copy } from "esbuild-plugin-copy"; +import autoprefixer from "autoprefixer"; +import { promises, readdirSync } from "fs"; +import { dirname } from "path"; +import postcss from "postcss"; +import less from "less"; + +const modules = "./node_modules"; + +// Copied from https://github.com/cfpb/consumerfinance.gov/blob/fcb4eab638db45003cac57432b89c0a249bc2aff/esbuild/plugins/postcss.js +const postCSSPlugin = ({ plugins = [], lessOptions = {} }) => ({ + name: "less-and-postcss", + setup(build) { + build.onLoad({ filter: /.\.less$/ }, async (args) => { + const fileContent = await promises.readFile(args.path, { + encoding: "utf-8", + }); + const lessResult = await less.render(fileContent, { + ...lessOptions, + filename: args.path, + rootpath: dirname(args.path), + }); + const result = await postcss(plugins).process(lessResult.css, { + from: args.path, + }); + + return { + contents: result.css, + loader: "css", + watchFiles: lessResult.imports, + }; + }); + }, +}); + +(async function () { + const watch = process.argv.slice(2)[0] === "--watch"; + + const ctx = await context({ + entryPoints: [ + "viewer/static_src/js/main.js", + "viewer/static_src/css/main.less", + ], + bundle: true, + sourcemap: true, + minify: true, + logLevel: "debug", + outbase: "viewer/static_src", + outdir: "viewer/static", + external: ["*.png", "*.woff", "*.woff2", "*.gif"], + loader: { + ".svg": "text", + }, + plugins: [ + copy({ + assets: [ + { + from: ["viewer/static_src/query-string-filtering.docx"], + to: ["."], + }, + { + from: [`${modules}/@cfpb/cfpb-icons/src/icons/*`], + to: ["./icons"], + }, + ], + }), + postCSSPlugin({ + plugins: [autoprefixer], + lessOptions: { + math: "always", + paths: [ + ...readdirSync(`${modules}/@cfpb`).map( + (v) => `${modules}/@cfpb/${v}/src` + ), + `${modules}`, + ], + }, + }), + ], + }); + + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + return await ctx.dispose(); + } +})(); diff --git a/package.json b/package.json index 848bfd1..2f3db68 100644 --- a/package.json +++ b/package.json @@ -2,36 +2,36 @@ "name": "website-indexer", "version": "1.1.0", "license": "CC0-1.0", - "type": "module", "engines": { "node": ">=16.x" }, "scripts": { - "prettier": "prettier --check 'viewer/static_src/*.{css,js}' 'viewer/**/*.html'", - "fix": "npm run prettier -- --write", - "fonts": "cp -r viewer/static_src/fonts viewer/static/fonts", - "styles": "curl -o viewer/static/cfgov.css https://www.consumerfinance.gov/static/css/main.css && curl https://www.consumerfinance.gov/static/apps/regulations3k/css/main.css >> viewer/static/cfgov.css && cp viewer/static_src/main.css viewer/static/main.css", - "scripts": "esbuild viewer/static_src/main.js --bundle --outfile=viewer/static/main.js", - "build": "yarn fonts && yarn styles && yarn scripts" + "build": "node ./esbuild.mjs", + "clean": "rm -rf ./viewer/static/", + "prettier": "prettier . --check", + "prettier:fix": "prettier . --write", + "watch": "yarn build --watch" }, "dependencies": { - "@cfpb/cfpb-expandables": "0.32.0", - "esbuild": "^0.14.38" + "@cfpb/cfpb-atomic-component": "^2.0.1", + "@cfpb/cfpb-buttons": "^2.0.0", + "@cfpb/cfpb-core": "^2.0.0", + "@cfpb/cfpb-expandables": "^2.0.1", + "@cfpb/cfpb-forms": "^2.0.0", + "@cfpb/cfpb-grid": "^2.0.0", + "@cfpb/cfpb-icons": "^2.0.0", + "@cfpb/cfpb-layout": "^2.0.0", + "@cfpb/cfpb-notifications": "^2.0.1", + "@cfpb/cfpb-pagination": "^2.0.0", + "@cfpb/cfpb-typography": "^2.0.1", + "autoprefixer": "^10.4.19", + "esbuild": "^0.23.0", + "esbuild-plugin-copy": "^2.1.1", + "less": "^4.2.0", + "postcss": "^8.4.39", + "postcss-less": "^6.0.0" }, "devDependencies": { "prettier": "^2.7.1" - }, - "prettier": { - "bracketSameLine": true, - "htmlWhitespaceSensitivity": "strict", - "tabWidth": 2, - "overrides": [ - { - "files": "*.css", - "options": { - "tabWidth": 4 - } - } - ] } } diff --git a/settings.py b/settings.py index 51bf03e..d780e7d 100644 --- a/settings.py +++ b/settings.py @@ -57,6 +57,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "viewer/static/icons"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ diff --git a/viewer/static/.gitkeep b/viewer/static/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/viewer/static_src/css/breadcrumbs.less b/viewer/static_src/css/breadcrumbs.less new file mode 100644 index 0000000..6ff5778 --- /dev/null +++ b/viewer/static_src/css/breadcrumbs.less @@ -0,0 +1,29 @@ +// Copied from https://github.com/cfpb/consumerfinance.gov/blob/f19227f9d13d90f19c06dd1dbd78e9233d1d3432/cfgov/unprocessed/css/breadcrumbs.less +.m-breadcrumbs { + // Mobile size. + position: relative; + display: flex; + gap: unit(10px / @base-font-size-px, rem); + align-items: center; + flex-wrap: wrap; + + font-size: unit(14px / @base-font-size-px, rem); + padding-top: unit(15px / @base-font-size-px, rem); + padding-bottom: unit(15px / @base-font-size-px, rem); + min-height: 33px; + + // Desktop and above. + .respond-to-min(@bp-med-min, { + padding-top: unit(30px / @base-font-size-px, rem); + // Bottom is zero because main content area may or may not have a breadcrumb, + // so the main content needs to set it s own spacing. + padding-bottom: 0; + }); +} + +// Hide on print. +.respond-to-print({ + .m-breadcrumbs { + display: none; + } +}); diff --git a/viewer/static_src/css/crawler.less b/viewer/static_src/css/crawler.less new file mode 100644 index 0000000..d612c87 --- /dev/null +++ b/viewer/static_src/css/crawler.less @@ -0,0 +1,65 @@ +.search-options { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + + div { + flex: 50%; + } + + label { + font-weight: bold; + + > span { + display: block; + margin-top: 3px; + margin-bottom: 15px; + font-weight: normal; + } + } +} + +.results-list { + li { + list-style-type: none; + } + + .results-list__item { + padding: 24px 0 !important; + + + .results-list__item { + border-top: 1px solid var(--gray-40); + } + } +} + +.u-truncate { + max-width: none !important; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + list-style-position: inside; + margin-bottom: 15px; +} + +.crawl-date { + display: flex; + + &__text { + font-weight: bold; + padding-right: 5px; + margin-bottom: 15px; + } +} + +.help-text { + margin-top: 15px; + margin-bottom: 30px; +} + +code.language-html { + max-height: 500px; + overflow: scroll; + display: block; +} diff --git a/viewer/static_src/css/footer.less b/viewer/static_src/css/footer.less new file mode 100644 index 0000000..32e49cb --- /dev/null +++ b/viewer/static_src/css/footer.less @@ -0,0 +1,22 @@ +// Copied from https://github.com/cfpb/consumerfinance.gov/blob/9fd820092b80fe7272454e6a93cef2c81f351877/cfgov/unprocessed/css/organisms/footer.less +.u-upper-hover-line { + position: absolute; + top: -1px; + content: ""; + display: block; + height: 1px; + width: 100%; + border-top: 1px solid currentcolor; +} + +.o-footer { + font-weight: 500; + // Adding an extra 5px to the top to account for the absolute positioned + // social media icons. + padding-top: unit(50px / @base-font-size-px, em); + // There is a 10px margin-bottom on the last .o-footer__list li's, plus the + // 50px bottom padding = 60px of total padding at the bottom of the footer. + padding-bottom: unit(50px / @base-font-size-px, em); + border-top: 5px solid var(--green); + background: var(--gray-5); +} diff --git a/viewer/static_src/css/layout.less b/viewer/static_src/css/layout.less new file mode 100644 index 0000000..ccca686 --- /dev/null +++ b/viewer/static_src/css/layout.less @@ -0,0 +1,126 @@ +// Copied from https://github.com/cfpb/consumerfinance.gov/blob/9fd820092b80fe7272454e6a93cef2c81f351877/cfgov/unprocessed/css/enhancements/layout-base.less +@layer layout-base { + // Establish the wrapper inside the grid container as the grid element. + .u-layout-grid { + --layout-max-width: 1170px; + + &__wrapper { + display: grid; + max-width: var(--layout-max-width); + margin: 0 auto; + padding-left: unit(15px / @base-font-size-px, rem); + padding-right: unit(15px / @base-font-size-px, rem); + grid-template-areas: + "c-breadcrumbs" + "c-secondary-nav" + "c-main" + "c-sidebar" + "c-prefooter"; + grid-auto-columns: 100%; + } + + // Name all possible content area elements (appearing inside ). + &__breadcrumbs { + grid-area: c-breadcrumbs; + } + + &__main { + grid-area: c-main; + padding-top: unit((30px / @base-font-size-px), rem); + } + + &__sidebar { + grid-area: c-sidebar; + } + + &__secondary-nav { + grid-area: c-secondary-nav; + } + + &__prefooter { + grid-area: c-prefooter; + } + + // Tablet and below. + .respond-to-max(@bp-sm-max, { + &__breadcrumbs { + padding-left: unit(30px / @base-font-size-px, rem); + padding-right: unit(30px / @base-font-size-px, rem); + margin-left: unit(-30px / @base-font-size-px, rem); + margin-right: unit(-30px / @base-font-size-px, rem); + background: var(--gray-5); + border-bottom: 1px solid var(--gray-40); + } + }); + + // Tablet only. + .respond-to-range(@bp-sm-min, @bp-sm-max, { + &__breadcrumbs { + padding-left: unit(15px / @base-font-size-px, rem); + padding-right: unit(15px / @base-font-size-px, rem); + } + }); + + // Tablet and above. + .respond-to-min(@bp-sm-min, { + &__wrapper { + // This handles collapsing of the breadcrumbs space if they are absent. + grid-template-rows: min-content 1fr; + padding-left: unit(30px / @base-font-size-px, rem); + padding-right: unit(30px / @base-font-size-px, rem); + } + + // If we do not have breadcrumbs, the gap above the content is larger… + &__main, + &__secondary-nav { + margin-top: unit((15px / @base-font-size-px), rem); + } + + // …than if we do have breadcrumbs + &__breadcrumbs ~ &__main, + &__breadcrumbs ~ &__secondary-nav { + margin-top: 0; + } + }); + + // Desktop and above. + .respond-to-min(@bp-med-min, { + &__secondary-nav { + padding-top: unit((30px / @base-font-size-px), rem); + } + }); + } + + // Modifier to remove max-width value. Used on CCDB, for example. + .u-layout-grid--full-width { + .u-layout-grid__wrapper { + max-width: initial; + } + } + + // Set default line width. + .u-layout-grid__main, + .u-layout-grid__content-intro { + dd, + dt, + h3, + h4, + h5, + h6, + li, + p, + label { + max-width: 41.875rem; + } + } + + // "Breakout" container that bleeds out of the grid to the screen edge. + .u-layout-grid__breakout { + width: 100vw; + position: relative; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; + } +} diff --git a/viewer/static_src/css/main.less b/viewer/static_src/css/main.less new file mode 100644 index 0000000..35155cf --- /dev/null +++ b/viewer/static_src/css/main.less @@ -0,0 +1,24 @@ +// CFPB Design System +@import (less) "cfpb-atomic-component.less"; +@import (less) "cfpb-core.less"; +@import (less) "cfpb-buttons.less"; +@import (less) "cfpb-expandables.less"; +@import (less) "cfpb-forms.less"; +@import (less) "cfpb-grid.less"; +@import (less) "cfpb-icons.less"; +@import (less) "cfpb-layout.less"; +@import (less) "cfpb-notifications.less"; +@import (less) "cfpb-pagination.less"; +@import (less) "cfpb-typography.less"; + +// Inspired by consumerfinance.gov +@import (less) "breadcrumbs.less"; +@import (less) "footer.less"; +@import (less) "skip-nav.less"; +@import (less) "layout.less"; + +// Crawler-specific +@import (less) "crawler.less"; + +@font-stack: Arial, sans-serif; +@font-face-path: ""; diff --git a/viewer/static_src/css/skip-nav.less b/viewer/static_src/css/skip-nav.less new file mode 100644 index 0000000..599fbef --- /dev/null +++ b/viewer/static_src/css/skip-nav.less @@ -0,0 +1,35 @@ +// Copied from https://github.com/cfpb/consumerfinance.gov/blob/fcb4eab638db45003cac57432b89c0a249bc2aff/cfgov/unprocessed/css/skip-nav.less +.skip-nav { + position: relative; + + &__link { + position: absolute; + top: auto; + left: -10000px; + height: 1px; + width: 1px; + overflow: hidden; + background: transparent; + transition: transform 1s ease, background 0.5s linear; + z-index: 11; + + &:focus { + .a-btn(); + // Adjustments to button to make it appear like a super button. + padding: unit(11px / 18px, em) unit(29px / 18px, em); + font-size: unit(18px / @base-font-size-px, em); + + top: 15px; + left: 15px; + height: auto; + width: auto; + overflow: visible; + outline: 0; + transition: transform 0.1s ease, background 0.2s linear; + } + + &--flush-left:focus { + left: 0; + } + } +} diff --git a/viewer/static_src/fonts/1e9892c0-6927-4412-9874-1b82801ba47a.woff b/viewer/static_src/fonts/1e9892c0-6927-4412-9874-1b82801ba47a.woff deleted file mode 100755 index ee6bd30..0000000 Binary files a/viewer/static_src/fonts/1e9892c0-6927-4412-9874-1b82801ba47a.woff and /dev/null differ diff --git a/viewer/static_src/fonts/2cd55546-ec00-4af9-aeca-4a3cd186da53.woff2 b/viewer/static_src/fonts/2cd55546-ec00-4af9-aeca-4a3cd186da53.woff2 deleted file mode 100755 index 3273d7d..0000000 Binary files a/viewer/static_src/fonts/2cd55546-ec00-4af9-aeca-4a3cd186da53.woff2 and /dev/null differ diff --git a/viewer/static_src/fonts/627fbb5a-3bae-4cd9-b617-2f923e29d55e.woff2 b/viewer/static_src/fonts/627fbb5a-3bae-4cd9-b617-2f923e29d55e.woff2 deleted file mode 100755 index abb3d33..0000000 Binary files a/viewer/static_src/fonts/627fbb5a-3bae-4cd9-b617-2f923e29d55e.woff2 and /dev/null differ diff --git a/viewer/static_src/fonts/f26faddb-86cc-4477-a253-1e1287684336.woff b/viewer/static_src/fonts/f26faddb-86cc-4477-a253-1e1287684336.woff deleted file mode 100755 index fdf59ed..0000000 Binary files a/viewer/static_src/fonts/f26faddb-86cc-4477-a253-1e1287684336.woff and /dev/null differ diff --git a/viewer/static_src/js/main.js b/viewer/static_src/js/main.js new file mode 100644 index 0000000..8d6af9e --- /dev/null +++ b/viewer/static_src/js/main.js @@ -0,0 +1,9 @@ +import { Expandable } from "@cfpb/cfpb-expandables"; + +const docElement = document.documentElement; +docElement.className = docElement.className.replace( + /(^|\s)no-js(\s|$)/, + "$1$2" +); + +Expandable.init(); diff --git a/viewer/static_src/main.css b/viewer/static_src/main.css deleted file mode 100644 index abd5b7a..0000000 --- a/viewer/static_src/main.css +++ /dev/null @@ -1,126 +0,0 @@ -.content_main-alt { - border-top: 0 !important; - width: 100% !important; -} - -.search_results .results_count { - display: grid; - grid-template-columns: repeat(auto-fit, 1000px); - grid-template-rows: auto auto; - grid-auto-rows: 0px; - overflow: hidden; - margin: 0 auto; - - padding-left: 1.875em; - padding-bottom: 15px; - margin-top: 30px; - border: 1px solid #20aa3f; -} - -.results_item h4 { - padding-top: 30px !important; -} - -.search_results .results_list { - padding-top: 0; - padding-left: 0 !important; -} - -.search_results .results_list ul { - padding-left: 0; -} - -.search_results .results_item { - margin: 0; -} - -.u-truncate { - max-width: none !important; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - list-style-position: inside; - margin-bottom: 15px; -} - -code.language-html { - max-height: 500px; - overflow: scroll; - display: block; -} - -.breadcrumbs { - border: 0; -} - -.o-radio_buttons { - display: flex; - flex-direction: row; - flex-wrap: wrap; - width: 100%; -} - -.o-radio_buttons div { - flex: 50%; -} - -.o-radio_buttons label > span { - display: block; - margin-top: 3px; - margin-bottom: 15px; - font-weight: normal; -} - -.o-radio_buttons label { - font-weight: bold; -} - -.o-form__input-w-btn-alt { - border-left-width: 0; - margin-bottom: 30px; -} - -.a-btn-alt { - margin-top: 15px; -} - -.a-link_text { - color: #0061c1; -} - -.u-bold_text { - font-weight: bold; - margin-bottom: 15px; -} - -.m-list_item { - max-width: 60em !important; -} - -.o-header_alt { - margin-top: 30px; - margin-bottom: 15px; -} - -.u-crawl_text { - font-weight: bold; - padding-right: 5px; -} - -.u-crawl_date { - display: flex; -} - -.o-well_alt { - margin-top: 60px; -} - -.lead-paragraph { - margin-top: 0; - margin-bottom: 15px; -} - -.u-help_text { - margin-top: 15px; - margin-bottom: 30px; -} diff --git a/viewer/static_src/main.js b/viewer/static_src/main.js deleted file mode 100644 index a58ad41..0000000 --- a/viewer/static_src/main.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Expandable } from "@cfpb/cfpb-expandables"; - -Expandable.init(); diff --git a/viewer/static/download-files/query-string-filtering.docx b/viewer/static_src/query-string-filtering.docx similarity index 100% rename from viewer/static/download-files/query-string-filtering.docx rename to viewer/static_src/query-string-filtering.docx diff --git a/viewer/templates/viewer/base.html b/viewer/templates/viewer/base.html index 3f33cea..f4b3823 100644 --- a/viewer/templates/viewer/base.html +++ b/viewer/templates/viewer/base.html @@ -7,30 +7,35 @@ - - {% block title %}Consumerfinance.gov web page index{% endblock %} - - - - + content="width=device-width, initial-scale=1, minimum-scale=1" + /> + + {% block title %}Consumerfinance.gov web page index{% endblock %} + + + - Skip to main content + Skip to main content - - - - - {% block content %}{% endblock content %} - + + + {% block breadcrumbs %}{% endblock breadcrumbs %} + + {% block content %}{% endblock content %}