├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cli ├── build.js ├── checkout.js ├── host-lint.js ├── minifier.js └── size-lint.js ├── docs └── joinExperimentLocal.md ├── hosts ├── en.wikipedia.org │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── style.scss │ └── wikipedia-wordmark-en.svg ├── iz.ru │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── README.md │ ├── iz-logo-full.svg │ └── style.css ├── livejournal.com │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── logo.svg │ ├── menu.svg │ └── style.css ├── meduza.io │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── clock.svg │ ├── logo.svg │ ├── menu.svg │ └── style.scss ├── pikabu.ru │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── README.md │ ├── images │ │ ├── logo.png │ │ ├── logo.svg │ │ └── menu.svg │ └── style.scss ├── rozhdestvenskiy.ru │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ └── style.css ├── ru.wikipedia.org │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── style.scss │ └── wikipedia-wordmark-ru.svg ├── test.skolznev.ru │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ └── style.css ├── vedomosti.ru │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── images │ │ └── logo.svg │ └── style.css ├── wikipedia.org │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ └── style.scss ├── www.drive.ru │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── menu_trigger.svg │ └── style.css ├── www.drive2.ru │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── icon-nav.svg │ ├── logo.svg │ └── style.css └── www.rbc.ru │ ├── HOSTS.yaml │ ├── OWNERS.yaml │ ├── images │ └── rbc_mob.svg │ └── style.css ├── lib ├── clean.js ├── gzip.js ├── hosts.js ├── postcss.js ├── server │ ├── cssOrScss.js │ ├── get-host-style.js │ ├── inject-livereload.js │ ├── inject-postcss-runtime.js │ ├── inject-prebuild.js │ ├── inject.js │ ├── lr.js │ ├── remove-csp.js │ ├── remove-custom-css.js │ ├── replace-custom-css.js │ └── style-middleware.js └── size.js ├── nodemon.json ├── package-lock.json ├── package.json ├── pm2.json ├── postcss.config.js ├── public ├── favicon.ico ├── frame-morda.html ├── frame.html ├── index.html └── vendors │ └── livereload.js ├── screencast.gif ├── server.js ├── stylelint.config.js ├── test ├── mocha.opts ├── remove-csp.spec.js └── replace-custom-css.spec.js └── webmaster-host.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | trim_trailing_whitespace = true 9 | 10 | [*.{yaml,json}] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | checkout 2 | public/vendors/** 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 2018 8 | }, 9 | "rules": { 10 | "no-unused-vars": [ 11 | 2, 12 | { 13 | "vars": "all", 14 | "args": "none" 15 | } 16 | ], 17 | "max-params": [2, 5], 18 | "max-depth": [2, 4], 19 | "no-eq-null": 0, 20 | "no-unused-expressions": 0, 21 | "dot-notation": 0, 22 | "use-isnan": 2, 23 | "indent": [ 24 | "error", 25 | 4 26 | ], 27 | "linebreak-style": [ 28 | "error", 29 | "unix" 30 | ], 31 | "quotes": [ 32 | "error", 33 | "single" 34 | ], 35 | "semi": [ 36 | "error", 37 | "always" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | *.css text eol=lf 3 | *.yaml text eol=lf 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Примеры турбо-страниц вашего сайта 2 | - https://yandex.ru/turbo?text=[URL] 3 | 4 | 7 | 8 | ### Изменения проверены в следующих браузерах 9 | - [ ] Android 4+ 10 | - [ ] iOS 9+ 11 | - [ ] IE 11 12 | 13 | Мы рекомендуем проверять изменения в перечисленных браузерах. Это не является блокером к принятию изменений, однако, помните, что вы несёте ответственность за итоговый результат. 14 | 15 | ### Изменения проверены внутри iframe 16 | - [ ] Да 17 | 18 | Турбо-страницы чаще всего отображаются внутри iframe. Мы рекомендуем проверять изменения стилей внутри iframe, особенно если проводились изменения с сеткой страницы. 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .history 3 | .vscode 4 | 5 | checkout 6 | node_modules 7 | hosts/**/*.min.css 8 | hosts/**/*.gz 9 | !hosts/**/hosts.json 10 | !hosts/**/style.css 11 | !hosts/**/OWNERS.yaml 12 | checkout 13 | build.json 14 | .env 15 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.11.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8.11.1 4 | cache: yarn 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## 16-07-2017 6 | ### Изменено 7 | Турбо-страница становится "резиновой". Избавляемся от `media query` и фиксированной ширины сетки. Теперь сетка тянется с фиксированным отступом от края страницы для всех расширений. 8 | 9 | 10 | ## 02-07-2017 11 | ### Изменено 12 | Из верстки страниц пропадает сущность `.markup`, избавляемся от лишней вложенности блоков. Также пропадают селекторы `.page__*` и им на смену приходят новые селекторы `.unit`, которые будут стоять у каждого блока и могут использоваться для вертикального выравнивания. 13 | Также базовый шрифт задается у всей страницы и не переопределяется в текстовых блоках. 14 | 15 | #### Запрещенные селекторы 16 | * `.markup`, `.markup__*` 17 | * `.page__*` 18 | * `.typo`, `.typo_*` 19 | * `.grid`, `.grid_*` 20 | 21 | #### Новые селекторы 22 | * `.unit`, `.unit_rect`, `.unit_text_xl`, `.unit_text_l`, `.unit_text_m` 23 | 24 | #### Пример изменения верстки страницы 25 | Было: 26 | 27 | ```html 28 |
29 | ... 30 |
31 |
32 |

33 |
34 |
35 |

36 |

37 | ... ... 38 |

39 |
40 |
41 |
42 | 43 |
44 | ... 45 |
46 | ``` 47 | 48 | Стало: 49 | 50 | ```html 51 |
52 | ... 53 |
54 |
55 |

56 |
57 |

58 |

59 | ... ... 60 |

61 |
62 |
63 | 64 |
65 | ... 66 |
67 | ``` 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TurboExtensions 2 | Расширенные возможности стилизации средствами CSS. 3 | 4 | [![Build Status](https://travis-ci.com/turboext/css.svg?branch=master)](https://travis-ci.com/turboext/css) 5 | 6 | # Инструменты разработчика 7 | ## DevServer 8 | Пререквизиты — [node.js 8.x](https://nodejs.org/en/) или [nvm](https://github.com/creationix/nvm). 9 | 10 | ### Установка: 11 | ``` 12 | git clone https://github.com/turboext/css.git 13 | cd css 14 | npm install 15 | npm start 16 | ``` 17 | 18 | Локальный запуск тестов — `npm test`. 19 | 20 | ### Возможности: 21 | * автоматическое применение стилей без необходимости перезагружать страницу 22 | * возможность открыть локальную бету на мобильном телефоне 23 | * написание стилей в синтаксисе CSS или SCSS с использованием препроцессора postcss. 24 | 25 | ### Использование: 26 | Допустим, вы хотите поменя стили для сайта `https://rozhdestvenskiy.ru`. Порядок действий следующий: 27 | * создать директорию `hosts/rozhdestvenskiy.ru` (дальше все действия будут происходить внутри этой директории); 28 | * внутри этой директории создать файл `HOSTS.yaml` 29 | * указать там название домен сайта, который используется в турбо-страницах: 30 | ```yaml 31 | - https://rozhdestvenskiy.ru 32 | ``` 33 | * создать файл `style.css` или `style.scss` 34 | * запустить dev-server 35 | * открыть http://localhost:3000 и выбрать адрес вашей турбо-страницы, например: `https://rozhdestvenskiy.ru/`, или `https://yandex.ru/turbo?text=https://rozhdestvenskiy.ru/`; 36 | * внести изменения в файл стилей; 37 | * изменения стилей будут применены автоматически. 38 | 39 | #### Дополнительные возможности DevServer 40 | * `TURBO_HOST=https://some-host.yandex.ru npm start` — возможность ходить за данными на отличный от `https://yandex.ru` сервер; 41 | * `&hostname=https://example.com` — форсировать применение стилей для выбранного хоста, даже если url турбо-страницы отличается от него; 42 | * `&disable=1` — отключить подмену CSS (может быть удобно для тестирования страницы в iframe). 43 | 44 | ### Livereload 45 | По-умолчанию включён livereload режим, в котором изменения применяются автоматически без обновления страницы. Если с ним возникают сложности, нужно запустить сервер с переменной окружения `LIVERELOAD=false`: 46 | * mac os x / linux — `LIVERELOAD=false npm start` 47 | * windows — `set LIVERELOAD=false && npm start` 48 | 49 | ### Отладка на мобильном телефоне 50 | Существует два варианта: 51 | * дождаться сборки автоматической беты в PR и использовать ссылки из описания PR 52 | * для доступа к локальной бете с мобильного телефона можно использовать встроенное тунеллирование на базе ngrok: 53 | * mac os x / linux — `PUBLIC=true npm start` или `npm run public` 54 | * windows — `set PUBLIC=true && npm start` или `npm run public` 55 | 56 | ### IFrame 57 | Турбо-страницы могут отображаться внутри iframe, например в Поиске, поэтому рекомендуется смотреть на результат изменения стилей в iframe. В случае изменения каркаса страницы, могут возникнуть проблемы наличием горизонтального скролла. Убрать его можно следующим образом: 58 | ```css 59 | .ua_frame_yes .page__container { 60 | overflow-x: hidden; 61 | } 62 | ``` 63 | 64 | Для тестирования страницы внутри iframe — нужно использовать адрес `/frame`, `/frame-morda`, вместо `/turbo`. Ссылки на iframe будут доступны в PR. 65 | 66 | ## Прототипирование стилей в браузере 67 | 1. Установить расширенение [live editor for CSS](https://webextensions.org/) (или любое другое с похожими возможностями); 68 | 2. Зайти в браузере на турбо-страницу; 69 | 3. Написать CSS для нужных компонентов. 70 | 71 | ![](screencast.gif) 72 | 73 | # Ограничения 74 | Ограничения указаны в [конфиге](stylelint.config.js) [stylelint](https://stylelint.io/): 75 | * нельзя использовать пользовательские шрифты; 76 | * нельзя использовать низкопроизводительную анимацию; 77 | * нельзя использовать селекторы по имени тэга; 78 | * нельзя использовать любые внешние ресурсы; 79 | * размер CSS в gzip ограничен 21KB. 80 | 81 | ## Deprecated 82 | * Селекторы на `.markup` и его производные в скором времени перестанут работать 83 | 84 | На code review могут быть указаны дополнительные требования к коду. 85 | 86 | **Важно:** список ограничений может меняться со временем. 87 | 88 | # Технические детали сборки 89 | * CSS код будет обработан с помощью [postcss](https://github.com/postcss/postcss). Настройки можно посмотреть в [конфигурационном файле](postcss.config.js). 90 | * Минимальные версии поддерживаемых браузеров 91 | * android 4 92 | * iOS 9 93 | * IE 11 94 | * Векторные изображения предпочтительнее растровых. Размер всех изображений должен быть оптимизирован до приемлемого уровня качества. 95 | 96 | Для оптимизации SVG рекомендуется использовать [svgo](https://www.npmjs.com/package/svgo) со следующими опциями: `svgo -p 2 --multipass --enable=removeDesc --enable=removeTitle --enable=sortAttrs --enable=removeViewBox --enable=removeStyleElement -i [PATH_TO_SVG]`. 97 | 98 | # Создание PR 99 | 1. Сделать fork репозитория; 100 | 1. Создать директорию `hosts/example.com`; 101 | 1. В директории должно быть несколько обязательных файлов — `HOSTS.yaml, OWNERS.yaml, style.css`. Возможен вариант `style.scss` для использования синтаксиса SCSS. 102 | * `HOSTS.yaml` должен быть указан домен для которых будут применяться текущие стили, например: 103 | ```yaml 104 | - https://rozhdestvenskiy.ru 105 | ``` 106 | Протокол (http, https) обязателен. Фактически, это адрес сайта из https://webmaster.yandex.ru 107 | ![webmaster](webmaster-host.png) 108 | * `OWNERS.yaml` должен содержать список логинов на https://github.com кому разрешено править стили для текущего домена, например: 109 | ```yaml 110 | - sbmaxx 111 | ``` 112 | 1. Закоммитить изменения и создать PR в основной репозиторий. Желательно в тексте коммита указывать адрес сайта; 113 | 1. В описании PR добавьте несколько ссылок на ваши турбо-страницы; 114 | 1. Дождитесь создания автоматической беты. Ссылка на неё появится в описании PR через некоторое время после создания PR, проверьте на ней свои изменения; 115 | 1. Дождитесь прохождения линтеров; 116 | 1. Дождитесь прохождения ревью; 117 | 118 | # Когда я увижу это в production? 119 | Изменения будут доступны в production в течение 30 минут после вливания PR в master ветку. В случае возникновения каких-либо проблем, пишите в slack, мы сможем помочь оперативно откатить изменения или внести правки. 120 | 121 | # Внесение изменений в инфраструктурную часть 122 | 1. [Создать issue](https://github.com/turboext/css/issues/new) с описанием сути изменений; 123 | 1. Сделать branch в формате `issues/<номер_issue>`; 124 | 1. Закоммитить изменения и создать PR в основной репозиторий; 125 | 1. Связать PR и issue (например, c помощью комментария). 126 | 127 | # Поддержка разработчиков 128 | https://turbosupport.slack.com, доступ по [приглашениям](https://yandex.ru/turbo?text=turbosupport-slack-access). 129 | -------------------------------------------------------------------------------- /cli/build.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | const fs = require('fs-extra'); 3 | const { basename, join } = require('path'); 4 | const postcss = require('../lib/postcss'); 5 | const glob = require('glob'); 6 | 7 | const pullRequestDir = process.argv[2] ? join('checkout', process.argv[2].replace('/', '-')) : '.'; 8 | 9 | const css = {}; 10 | const hosts = {}; 11 | 12 | (async () => { 13 | try { 14 | if (!fs.existsSync(pullRequestDir)) { 15 | console.error(`Pull request directory "${pullRequestDir}" doesn't exists.`); 16 | process.exit(1); 17 | } 18 | 19 | const promises = glob.sync(`${pullRequestDir}/hosts/*/`).map(async dir => { 20 | console.log(`Processing ${basename(dir)}`); 21 | const key = basename(dir); 22 | 23 | const cssFile = join(dir, 'style.css'); 24 | const scssFile = join(dir, 'style.scss'); 25 | const style = fs.existsSync(scssFile) ? scssFile : fs.existsSync(cssFile) ? cssFile : ''; 26 | 27 | css[key] = await postcss(style); 28 | 29 | yaml.safeLoad(fs.readFileSync(join(dir, 'HOSTS.yaml'), 'utf8')) 30 | .forEach(host => hosts[host] = key); 31 | }); 32 | 33 | await Promise.all(promises); 34 | 35 | const output = join(pullRequestDir, 'build.json'); 36 | fs.writeFileSync(output, JSON.stringify({ css, hosts }, null, 4)); 37 | 38 | console.log(`Build was saved to ${output}`); 39 | } catch(e) { 40 | console.error(e); 41 | process.exit(1); 42 | } 43 | })(); 44 | -------------------------------------------------------------------------------- /cli/checkout.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const { resolve } = require('path'); 3 | const spawn = require('child_process').spawnSync; 4 | 5 | const [ pullRequest = 'master' ] = process.argv.slice(2); 6 | const checkoutDirectory = getCheckoutDirectory(pullRequest); 7 | 8 | const cwd = process.cwd(); 9 | 10 | function getCheckoutDirectory(pull) { 11 | return resolve('checkout', pull.replace('/', '-')); 12 | } 13 | 14 | function exec(cmd, cwd = checkoutDirectory) { 15 | console.log(`${cwd} $ ${cmd}`); 16 | return spawn(cmd, { stdio: 'inherit', shell: true, cwd }); 17 | } 18 | 19 | function clone() { 20 | const remotes = ` 21 | [remote "origin"] 22 | fetch = +refs/heads/*:refs/remotes/origin/* 23 | fetch = +refs/pull/*/head:refs/remotes/origin/pull/* 24 | `.trim(); 25 | 26 | exec(`git clone https://github.com/turboext/css.git ${checkoutDirectory}`, cwd); 27 | exec(`echo '${remotes}' >> .git/config`); 28 | exec('git fetch --all'); 29 | } 30 | 31 | function checkout(pullRequest) { 32 | exec(`git checkout ${pullRequest}`); 33 | } 34 | 35 | if (fs.existsSync(checkoutDirectory)) { 36 | exec(`rm -rf ${checkoutDirectory}`); 37 | } 38 | 39 | clone(); 40 | checkout(pullRequest); 41 | -------------------------------------------------------------------------------- /cli/host-lint.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const { basename } = require('path'); 3 | const { list, validator } = require('../lib/hosts'); 4 | 5 | const errors = list() 6 | .map(host => ({ host: basename(host), validation: validator(host) })) 7 | .filter(res => res.validation.length); 8 | 9 | errors.forEach(res => { 10 | const validation = res.validation.map(v => `— ${v.error} ${chalk.gray(`[${v.rule}]`)}`).join('\n— '); 11 | console.error(`${chalk.red.bold(res.host)} has errors:\n${validation}`); 12 | }); 13 | 14 | if (errors.length) { 15 | process.exit(1); 16 | } 17 | -------------------------------------------------------------------------------- /cli/minifier.js: -------------------------------------------------------------------------------- 1 | const postcss = require('../lib/postcss'); 2 | const glob = require('glob'); 3 | const fs = require('fs'); 4 | const async = require('async'); 5 | const path = require('path'); 6 | const files = findTargets(process.argv[2]); 7 | 8 | const queue = async.queue(async ({ from, to }, callback) => { 9 | process.stdout.write(`Minify ${from}...`); 10 | 11 | try { 12 | const result = await postcss(from, to); 13 | fs.writeFileSync(to, result); 14 | process.stdout.write('ok'); 15 | } catch(e) { 16 | process.stdout.write('failed'); 17 | console.log(e); 18 | } 19 | console.log(''); 20 | 21 | callback(); 22 | }, 1); 23 | 24 | if (files.length !== 1) { 25 | queue.drain = () => console.log('All items has been minified'); 26 | } 27 | 28 | queue.push(files.map(from => { 29 | const to = from.replace(/\.s?css$/, '.min.css'); 30 | return { from, to }; 31 | })); 32 | 33 | function findTargets(where) { 34 | if (where && where.endsWith('css')) { 35 | return where; 36 | } 37 | 38 | let search; 39 | 40 | if (where) { 41 | search = path.join(where, '*css'); 42 | } else { 43 | search = path.resolve(__dirname, '../hosts/**/*css'); 44 | } 45 | 46 | return glob.sync(search) 47 | .filter(file => where ? file.includes(where) : true) 48 | .filter(file => file.endsWith('css') && !file.endsWith('min.css')); 49 | } 50 | -------------------------------------------------------------------------------- /cli/size-lint.js: -------------------------------------------------------------------------------- 1 | const size = require('../lib/size'); 2 | const gzip = require('../lib/gzip'); 3 | const clean = require('../lib/clean'); 4 | const chalk = require('chalk'); 5 | 6 | const BYTES = 21 * 1024; // 21 KB 7 | 8 | function toKB(bytes, precision = 2) { 9 | return (bytes / 1024).toFixed(precision); 10 | } 11 | 12 | function toKBStr(bytes, precision) { 13 | return toKB(bytes, precision) + ' KB'; 14 | } 15 | 16 | async function run() { 17 | await gzip('hosts/**/*.min.css'); 18 | const fileSizes = await size('hosts/**/*.gz'); 19 | 20 | await clean('hosts/**/*.min.css'); 21 | await clean('hosts/**/*.gz'); 22 | 23 | const bigFiles = fileSizes.filter(file => file.size > BYTES); 24 | 25 | if (!bigFiles.length) { 26 | process.exit(0); 27 | } 28 | 29 | console.info(chalk.blue('[INFO]'), 'MaxSize:', toKBStr(BYTES)); 30 | 31 | bigFiles.forEach(({ file, size }) => { 32 | console.error( 33 | chalk.red.bold('[ERROR]'), 34 | file, '—', toKBStr(size), 35 | chalk.red('(+' + toKBStr(size - BYTES) + ')') 36 | ); 37 | }); 38 | 39 | process.exit(1); 40 | } 41 | 42 | run(); 43 | -------------------------------------------------------------------------------- /docs/joinExperimentLocal.md: -------------------------------------------------------------------------------- 1 | ## Включить экспериментальную верстку Турбо-страниц при локальной разработке. 2 | 3 | ### Для включения эксперимента локально надо сделать несколько шагов: 4 | 5 | 1. Перейти по ссылке, включающей экспериментальную верстку, которая придет в письме. Например, она выглядит так https://yandex.ru/ecoo/safe/redirect?key=t79735.e1529793723.T42000.r2849369039.s1482BDA75603064E8FCBBC252F40A70E&to=https://yandex.ru/search/touch. Лучше делать это в режиме инкогнито, чтобы потом не пришлось чистить куки браузера. 6 | 2. После перехода по ссылке на странице Яндекса открыть средства разработчика и в консоли выполнить: 7 | ``` 8 | var cookie = ''; 9 | document.cookie.split('; ').forEach(function (c) { 10 | if (c.indexOf('yexp') === 0) cookie = c; 11 | }); 12 | cookie.split('=')[1]; 13 | ``` 14 | , что выведет значение куки `yexp`. Также значение куки можно посмотреть на https://yandex.ru/internet в разделе 'Cookie вашего браузера'. Ее значение будет похоже на: 15 | ``` 16 | t84403.e1532431671.s696DEA29A5EA77692C398594EE4B5686 17 | ``` 18 | 3. При открытии локальной беты http://localhost:3000 открыть средства разработчика и в консоли выполнить: 19 | ``` 20 | document.cookie='yexp=myCookie;' 21 | ``` 22 | , где `myCookie` - строка, которую скопировали в предыдущем пункте. 23 | 24 | 4. После перезагрузки страницы будет включена экспериментальная верстка. 25 | 26 | ### Для выключения эксперимента: 27 | 28 | Если все действия выше делать не в режиме инкогнито, то после разработки экспериментальная верстка останется включенной по-умолчанию. Для того, чтобы ее выключить, надо почистить куки как на http://localhost:3000, так и на http://yandex.ru. 29 | -------------------------------------------------------------------------------- /hosts/en.wikipedia.org/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://en.wikipedia.org 2 | - https://uk.wikipedia.org 3 | - https://tr.wikipedia.org 4 | - https://be.wikipedia.org 5 | - https://kk.wikipedia.org 6 | - https://tt.wikipedia.org 7 | - https://uz.wikipedia.org 8 | -------------------------------------------------------------------------------- /hosts/en.wikipedia.org/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - sbmaxx 2 | - tenorok 3 | -------------------------------------------------------------------------------- /hosts/en.wikipedia.org/style.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable-next-line */ 2 | @import "../wikipedia.org/style"; 3 | 4 | .page .header > .link { 5 | background: transparent url('./wikipedia-wordmark-en.svg') no-repeat 0 50%; 6 | background-size: 116px 18px; 7 | width: 116px; 8 | } 9 | -------------------------------------------------------------------------------- /hosts/en.wikipedia.org/wikipedia-wordmark-en.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/iz.ru/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://iz.ru 2 | -------------------------------------------------------------------------------- /hosts/iz.ru/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - ifirsaev 2 | - i.firsaev -------------------------------------------------------------------------------- /hosts/iz.ru/README.md: -------------------------------------------------------------------------------- 1 | Стилизация страницы поиска Yandex.Turbo сайта iz.ru 2 | ---------------------------------------------------- 3 | 4 | Стилизация проводилась на основе типовой страницы результатов поиска: 5 | https://www.yandex.ru/search/touch/?text=%D0%B4%D0%B2%D0%BE%D1%80%D0%BA%D0%BE%D0%B2%D0%B8%D1%87%20%D0%B2%D1%8B%D0%B4%D0%B2%D0%B8%D0%BD%D1%83%D1%82%20%D0%B2%20%D1%81%D0%BE%D0%B2%D0%B5%D1%82%20%D0%B4%D0%B8%D1%80%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BE%D0%B2%20%D1%80%D0%B6%D0%B4%20iz.ru&lr=213&mda=0 6 | 7 | ##### Верхняя панель 8 | Добавлен логотип iz.ru, лежит рядом с CSS-файлом. 9 | Добавлен градиентный фон и стилизована кнопка меню. 10 | 11 | 12 | ##### Всплывающее меню 13 | Стилизована панель всплывающего меню - изменен фон, 14 | цвет и базовое семейство шрифтов. 15 | 16 | ##### Контентная область 17 | Изменен размер заголовка, цвет и размер шрифта "автора статьи", размер шрифта, 18 | межстрочное расстояние параграфов контентной области, отступы. 19 | 20 | ##### Контентная область 21 | На примере тестовой площадки https://yandex.ru/turbo?text=custom-css-iz.ru 22 | были применены стили к блоку "Вам может быть интересно". 23 | 24 | ##### Футер 25 | Был стилизован футер, относительно тестового шаблона https://yandex.ru/turbo?text=custom-css-iz.ru 26 | Цвет, размер, межстрочное расстояние шрифтов, цвет фона и отступы. -------------------------------------------------------------------------------- /hosts/iz.ru/iz-logo-full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/iz.ru/style.css: -------------------------------------------------------------------------------- 1 | .page { 2 | font-family: "Fira Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 3 | } 4 | .modal .link { 5 | text-transform: uppercase; 6 | font-size: 16px; 7 | color: #fff; 8 | } 9 | .header.header { 10 | position: absolute; 11 | left: 0; 12 | width: 100%; 13 | border-width: 2px 0px; 14 | box-sizing: border-box; 15 | border-style: solid; 16 | padding: 9px 0; 17 | border-color: #9e4bbb transparent #2b143e; 18 | background: linear-gradient(180deg,#69389f,#351c58); 19 | } 20 | .header.header:before, 21 | .header.header:after { 22 | content: ''; 23 | position: absolute; 24 | left: 0; 25 | height: 50%; 26 | width: 100%; 27 | } 28 | .header.header:before { 29 | background: linear-gradient(180deg,#78329d,#5c2a7f); 30 | top: 0; 31 | } 32 | .header.header:after { 33 | background: linear-gradient(180deg,#562875,#3a1955); 34 | border-top: 1px solid #5c2a7f; 35 | top: 50%; 36 | } 37 | .header_host .header-title { 38 | text-indent: -1000px; 39 | min-height: 28px; 40 | padding-top: 0; 41 | } 42 | .header .header__aside { 43 | z-index: 1; 44 | } 45 | .header.header > .link { 46 | background: transparent url('./iz-logo-full.svg') no-repeat 50%; 47 | background-size: 109px 25px; 48 | position: relative; 49 | z-index: 1; 50 | } 51 | .header__aside .sandwich-menu { 52 | width: 60px; 53 | margin-left: 0; 54 | } 55 | .header__aside .header-turbo { 56 | top: 11px; 57 | margin-right: 10px; 58 | } 59 | .header-turbo .header-turbo-icon { 60 | fill: #c0abce; 61 | } 62 | .header__aside .modal-handler { 63 | background: transparent; 64 | border-top: 0px none; 65 | position: relative; 66 | width: 60px; 67 | height: 46px; 68 | margin: 0 auto; 69 | } 70 | .sandwich-menu:before, 71 | .header__aside .modal-handler:before, 72 | .header__aside .modal-handler:after { 73 | content: ''; 74 | position: absolute; 75 | background: white; 76 | width: 20px; 77 | margin: 0px 20px; 78 | height: 2px; 79 | left: 0; 80 | } 81 | .sandwich-menu:before { 82 | top: 16px; 83 | left: auto; 84 | right: 0; 85 | } 86 | .header__aside .modal-handler:before { 87 | top: 21px; 88 | } 89 | .header__aside .modal-handler:after { 90 | top: 26px; 91 | } 92 | .cover__content > .divider { 93 | padding-top: 52px; 94 | } 95 | .cover .title { 96 | font-size: 20px; 97 | line-height: normal; 98 | color: #000; 99 | } 100 | .cover .divier + .title { 101 | margin-top: 22px; 102 | margin-bottom: 22px; 103 | } 104 | .cover__content > .description { 105 | font-size: 14px; 106 | line-height: 1.1; 107 | vertical-align: top; 108 | } 109 | .cover .title+.description { 110 | margin-top: 10px; 111 | } 112 | .cover .description.description { 113 | color: #999; 114 | } 115 | .paragraph { 116 | font-size: 16px; 117 | line-height: 23px; 118 | margin-bottom: 15px; 119 | } 120 | .source .button_theme_default { 121 | background-color: #452963; 122 | color: #fff; 123 | border-radius: 0; 124 | } 125 | .table .table__cell { 126 | line-height: 1.1; 127 | } 128 | .table_style_divided .table__cell:last-child { 129 | padding-left: 0; 130 | } 131 | .modal .table__row+.table__row .table__cell { /* stylelint-disable-line */ 132 | border-top: 1px solid #ababa1; 133 | } 134 | .image-simple { 135 | display: block; 136 | position: relative; 137 | background-repeat: no-repeat; 138 | background-position: 50% 50%; 139 | background-size: cover; 140 | max-width: 100%; 141 | max-height: 100%; 142 | overflow: hidden; 143 | } 144 | .image-simple img { /* stylelint-disable-line */ 145 | opacity: 0; 146 | max-width: 100%; 147 | max-height: 100%; 148 | } 149 | .image-simple_preloaded .loader { 150 | opacity: 0.75; 151 | } 152 | .image-simple_loaded .loader { 153 | display: none; 154 | } 155 | .image-simple_ratio_1x1 img { /* stylelint-disable-line */ 156 | position: absolute; 157 | } 158 | .image-simple_ratio_1x1:after { 159 | content: ''; 160 | display: block; 161 | padding: 100% 0 0; 162 | } 163 | .modal .modal__container { 164 | background: #878679; 165 | margin-left: auto; 166 | margin-right: auto; 167 | width: auto; 168 | padding: 0 0 20px; 169 | } 170 | .modal .modal__title > .modal__title-text { 171 | text-transform: uppercase; 172 | font-size: 16px; 173 | color: #ababa1; 174 | font-weight: bold; 175 | } 176 | .modal .modal__title { 177 | border-bottom: 1px solid #ababa1; 178 | } 179 | .modal .modal__landscape-close { 180 | float: right; 181 | font-size: 16px; 182 | font-weight: normal; 183 | color: #fff; 184 | } 185 | .modal .button { 186 | background-color: #ababa1; 187 | color: #fff; 188 | border-radius: 0; 189 | border: 0px none; 190 | box-sizing: border-box; 191 | width: 100%; 192 | max-width: 100%; 193 | } 194 | .footer.footer { 195 | background: #2c2c2c; 196 | text-align: center; 197 | padding-top: 10px; 198 | padding-bottom: 10px; 199 | } 200 | .footer .footer__links.footer__links { 201 | border: 0px none; 202 | padding: 0px; 203 | margin: 0; 204 | } 205 | .footer .footer__link, 206 | .footer .footer__divider { 207 | color: #fff; 208 | } 209 | .footer .paragraph.paragraph { 210 | font-size: 16px; 211 | line-height: 24px; 212 | color: #aaa; 213 | margin-bottom: 0px; 214 | } 215 | .footer { 216 | margin: 0 -14px; 217 | } 218 | -------------------------------------------------------------------------------- /hosts/livejournal.com/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://livejournal.com 2 | -------------------------------------------------------------------------------- /hosts/livejournal.com/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - sbmaxx 2 | -------------------------------------------------------------------------------- /hosts/livejournal.com/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hosts/livejournal.com/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hosts/livejournal.com/style.css: -------------------------------------------------------------------------------- 1 | /* Разделитель больше не нужен */ 2 | .divider { 3 | display: none; 4 | } 5 | 6 | /* Убираем текст *.livejournal */ 7 | .header_host .header-title { 8 | text-indent: -1000px; 9 | min-height: 28px; 10 | padding-top: 0; 11 | } 12 | 13 | .page .header > .link { 14 | position: relative; 15 | z-index: 1; 16 | background: transparent url('./logo.svg') no-repeat 0 50%; 17 | background-size: 165px 30px; 18 | text-align: left; 19 | } 20 | 21 | /* Добавим заливку для шапки на всю ширину*/ 22 | .cover:before { 23 | content: ''; 24 | position: absolute; 25 | left: 0; 26 | top: 0; 27 | width: 100%; 28 | height: 52px; 29 | background-color: #004359; 30 | } 31 | 32 | /* Цвет турбо-иконки */ 33 | .header-turbo .header-turbo-icon { 34 | fill: #fff; 35 | } 36 | 37 | /* Через CSS поменять цвет меню сложно, меняем иконку целиком */ 38 | .sandwich-menu__handler { 39 | background-image: url('menu.svg'); 40 | } 41 | 42 | .header_host { 43 | padding: 12px 0; 44 | } 45 | 46 | /* Иначе меню будет некликабельным */ 47 | .header__aside { 48 | z-index: 1; 49 | } 50 | 51 | .cover .title + .description { 52 | font-size: 14px; 53 | margin-top: 0; 54 | color: #8C969B; 55 | } 56 | 57 | .cover + .unit { 58 | margin-top: 10px; 59 | } 60 | 61 | /* Уберём шапку из релеватных публикаций */ 62 | .autoload__content .cover .header { 63 | display: none; 64 | } 65 | -------------------------------------------------------------------------------- /hosts/meduza.io/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://meduza.io 2 | -------------------------------------------------------------------------------- /hosts/meduza.io/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - sbmaxx 2 | -------------------------------------------------------------------------------- /hosts/meduza.io/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /hosts/meduza.io/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hosts/meduza.io/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hosts/meduza.io/style.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | font-family: Arial,Helvetica Neue,sans-serif; 3 | } 4 | 5 | /* Разделитель больше не нужен */ 6 | .divider { 7 | display: none; 8 | } 9 | 10 | /* Убираем текст *.meduza */ 11 | .header .header-title { 12 | text-indent: -1000px; 13 | min-height: 28px; 14 | padding-top: 0; 15 | } 16 | 17 | /* Фирменный цвет ссылок, но не в подвале */ 18 | .paragraph .link { 19 | text-decoration: none; 20 | color: inherit; 21 | box-shadow: inset 0 -1px #b88b58; 22 | } 23 | 24 | .footer .link { 25 | box-shadow: inherit; 26 | } 27 | 28 | .page .header > .link { 29 | position: relative; 30 | z-index: 1; 31 | background: transparent url('./logo.svg') no-repeat 50% 50%; 32 | background-size: 100px 21px; 33 | text-align: center; 34 | fill: #b88b58; 35 | } 36 | 37 | .header .image_type_logo { 38 | display: none; 39 | } 40 | 41 | /* Добавим заливку для шапки на всю ширину*/ 42 | .cover:before { 43 | content: ''; 44 | position: absolute; 45 | left: 0; 46 | top: 0; 47 | width: 100%; 48 | height: 52px; 49 | background-color: #262626; 50 | } 51 | 52 | /* Цвет турбо-иконки */ 53 | .header-turbo .header-turbo-icon { 54 | fill: #fff; 55 | } 56 | 57 | /* Через CSS поменять цвет меню сложно, меняем иконку целиком */ 58 | .sandwich-menu__handler { 59 | background-image: url('menu.svg'); 60 | } 61 | 62 | .header_host { 63 | padding: 12px 0; 64 | } 65 | 66 | /* Иначе меню будет некликабельным */ 67 | .header__aside { 68 | position: static; 69 | } 70 | 71 | .header-turbo.header__aside-item { 72 | position: absolute; 73 | left: 0; 74 | top: 14px; 75 | } 76 | 77 | .sandwich-menu.header__aside-item { 78 | position: absolute; 79 | right: 0; 80 | top: 0; 81 | z-index: 1; 82 | } 83 | 84 | .cover .title { 85 | font-family: Georgia,serif; 86 | font-size: 25px; 87 | line-height: 27px; 88 | font-weight: 400; 89 | } 90 | 91 | .cover .title + .description { 92 | font-size: 12px; 93 | line-height: 14px; 94 | margin-top: 12px; 95 | color: gray; 96 | } 97 | 98 | .cover .description .date { 99 | display: inline-block; 100 | padding-left: 16px; 101 | background: url('./clock.svg') no-repeat 0 0; 102 | background-size: 12px 14px; 103 | } 104 | 105 | .cover + .unit { 106 | margin-top: 10px; 107 | } 108 | 109 | .paragraph { 110 | font-family: Georgia,serif; 111 | } 112 | 113 | .footer .paragraph { 114 | font-family: inherit; 115 | } 116 | 117 | .footer__about { 118 | padding-top: 16px; 119 | padding-bottom: 16px; 120 | } 121 | .footer_view_default { 122 | padding-bottom: 16px; 123 | } 124 | 125 | .button_theme_default { 126 | background-color: #b88b58; 127 | text-shadow: 0 1px 1px rgba(0,0,0,.3); 128 | color: #fff; 129 | } 130 | 131 | /* Уберём шапку из релеватных публикаций */ 132 | .autoload__content .cover:before, 133 | .autoload__content .cover .header { 134 | display: none; 135 | } 136 | -------------------------------------------------------------------------------- /hosts/pikabu.ru/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://pikabu.ru 2 | -------------------------------------------------------------------------------- /hosts/pikabu.ru/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - lomadurov -------------------------------------------------------------------------------- /hosts/pikabu.ru/README.md: -------------------------------------------------------------------------------- 1 | # pikabu.ru 2 | 3 | Стилизация турбо-страниц https://pikabu.ru 4 | 5 |

6 | 7 |

8 | 9 | ## Что изменили 10 | 11 | Ход работ по изменению внешнего вида Turbo страницы. 12 | 13 | ### Основное 14 | 15 | - [x] Перекрасить в фирменные цвета: основной текст `#454648`, ссылки `#83b65b`, цитаты `#8c8681`, кнопки `#8ac858`; 16 | - [x] Отступы между основными элементами `20px`; 17 | - [x] Растянуть изображения на всю ширину экрана; 18 | - [x] Отступы от текста до изображения `10px`, от изображения до текста `15px`, между изображениями `5px`. 19 | 20 | ### Шапка 21 | 22 | - [x] Высота шапки `45px`; 23 | - [x] Зелёная подложка на всю ширину; 24 | - [x] Временно заменим логотип кекса для экранов с DPR > 1. Удалить, как только Turbo будут отдавать 2x logo 😄; 25 | - [x] Текст "Пикабу" в logo заменим SVG; 26 | - [x] Иконку Турбо перекрасить в белый, а сэндвич в цвет текста; 27 | - [x] Убирать шапку у релеватных публикаций. 28 | 29 | ### Подвал 30 | 31 | - [x] Отцентровать ссылки; 32 | - [x] Уменьшить размер ссылок до `14px`. 33 | 34 | ### Комментарии 35 | 36 | - [x] Выравнить аватрки с именем пользователя на одну прямую; 37 | - [x] Изменили размер аватарок до `20px 20px`; 38 | - [x] Добавить разделитель c подписью "Популярные комментарии" между текстом публикации и комментариями; 39 | - [x] Перекрасить кнопку написания комментариев. 40 | -------------------------------------------------------------------------------- /hosts/pikabu.ru/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turboext/css/b359413a462133efdd7bef3deb61eda2acb5c3bd/hosts/pikabu.ru/images/logo.png -------------------------------------------------------------------------------- /hosts/pikabu.ru/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/pikabu.ru/images/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/pikabu.ru/style.scss: -------------------------------------------------------------------------------- 1 | $color--green: #83b65b; 2 | $color--text: #454648; 3 | $size--space_between: 20px; 4 | 5 | $size--title: 20px; 6 | $size--body-1: 16px; 7 | $size--body-0: 14px; 8 | $size--caption: 13px; 9 | 10 | $size--avatar: 20px; 11 | 12 | /* --------------------------------------------------------------------------------------------------------------------- 13 | * Основное 14 | * - Перекрасить в фирменные цвета: основной текст `#454648`, ссылки `#83b65b`, цитаты, кнопки 15 | * - Отступы между основными элементами `20px` 16 | * - Растянуть изображения на всю ширину .page 17 | * - Изменить семейство шрифтов на фирменные 18 | * - Отступы от текста до изображения `10px`, от изображения до текста `15px`, между изображениями `5px` 19 | * - Отцентровать ссылки в подвале и уменьшить размер ссылок до `14px` 20 | * --------------------------------------------------------------------------------------------------------------------- 21 | */ 22 | 23 | .link.link { 24 | color: $color--green; 25 | } 26 | 27 | .page { 28 | overflow-x: hidden; 29 | color: $color--text; 30 | .source { 31 | margin-top: $size--space_between; 32 | } 33 | } 34 | 35 | /* Шрифт */ 36 | .page, 37 | .button, 38 | .b, 39 | .i { 40 | font-family: Roboto, "Open Sans", -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; 41 | } 42 | 43 | .button { 44 | border-radius: 3px; 45 | color: #fff; 46 | background: #8ac858; 47 | } 48 | 49 | .author { 50 | font-weight: bold; 51 | color: $color--green; 52 | } 53 | 54 | .blockquote.blockquote { 55 | padding: 5px; 56 | border-left: 0; 57 | border-radius: 2px; 58 | color: #8c8681; 59 | background: #faf9f4; 60 | } 61 | 62 | .cover .title { 63 | font-size: $size--title; 64 | } 65 | 66 | .image { 67 | /* Уберём отступы у изображений */ 68 | &_type_edge { 69 | max-width: inherit; 70 | margin-left: -14px; 71 | margin-right: -14px; 72 | max-height: inherit !important; 73 | } 74 | /* Отступы между изображениями 5px */ 75 | & + & { 76 | margin-top: 5px; 77 | } 78 | } 79 | 80 | .paragraph { 81 | .paragraph { 82 | line-height: 22px; 83 | color: $color--text; 84 | } 85 | /* Отступ от параграфа до изображения */ 86 | & + .image { 87 | margin-top: 10px; 88 | } 89 | } 90 | 91 | /* Отступ от изображения до параграфа */ 92 | .image + .paragraph { 93 | margin-top: 15px; 94 | } 95 | 96 | .footer { 97 | font-size: $size--body-0; 98 | text-align: center; 99 | && { 100 | margin-top: $size--space_between; 101 | } 102 | } 103 | 104 | /* --------------------------------------------------------------------------------------------------------------------- 105 | * Шапка 106 | * - Высота шапки 45px 107 | * - Зелёная подложка на всю ширину 108 | * - Временно заменим логотип кекса для экранов с DPR > 1. Удалить, как только Turbo будут отдавать 2x logo 😄 109 | * - Текст "Пикабу" в logo заменим SVG 110 | * - Иконки перекрасим в белый 111 | * --------------------------------------------------------------------------------------------------------------------- 112 | */ 113 | .cover { 114 | position: relative; 115 | 116 | & + .paragraph { 117 | margin-top: 10px; 118 | } 119 | 120 | /* Добавим заливку для шапки на всю ширину*/ 121 | &:before { 122 | content: ''; 123 | position: absolute; 124 | left: 0; 125 | top: 0; 126 | width: 100%; 127 | height: 45px; 128 | background-color: #89c957; 129 | } 130 | 131 | .description { 132 | font-size: 13px; 133 | } 134 | 135 | .title + .description { 136 | margin-top: 0; 137 | } 138 | 139 | .divider { 140 | display: none; 141 | } 142 | 143 | .header { 144 | .image_type_logo { 145 | background-image: url('images/logo.png') !important; /* переопределение inline стилей */ 146 | } 147 | 148 | &__title-link { 149 | position: relative; 150 | margin-right: 50px; 151 | z-index: 1; 152 | } 153 | 154 | &_logo-host { 155 | padding: 6px 0; 156 | } 157 | } 158 | 159 | .header-turbo { 160 | top: 10px; 161 | 162 | .header-turbo-icon { 163 | fill: #fff; 164 | } 165 | } 166 | .header-title { 167 | position: relative; 168 | margin-left: 10px; 169 | padding-top: 0; 170 | color: transparent; 171 | 172 | &:after { 173 | content: ''; 174 | display: block; 175 | position: absolute; 176 | left: 0; 177 | top: 5px; 178 | width: 75px; 179 | height: 24px; 180 | background: url('./images/logo.svg') no-repeat; 181 | } 182 | } 183 | 184 | .sandwich-menu__handler { 185 | height: 45px; 186 | background-image: url('./images/menu.svg'); 187 | } 188 | } 189 | 190 | /* Уберём шапку из релеватных публикаций */ 191 | .autoload__content { 192 | .cover { 193 | &:before, .header, .divider { 194 | display: none; 195 | } 196 | } 197 | } 198 | 199 | /* --------------------------------------------------------------------------------------------------------------------- 200 | * Комментарий 201 | * - Выравнить аватрки с именем пользователя на одну прямую 202 | * - Добавить разделитель c подписью "Популярные комментарии" между текстом публикации и комментариями 203 | * - Изменили размер аватарок до `20px 20px` 204 | * - Изменить размер: текста комментария 15px 205 | * --------------------------------------------------------------------------------------------------------------------- 206 | */ 207 | .comments { 208 | && { 209 | font-size: 15px; 210 | line-height: 18px; 211 | margin-top: $size--space_between; 212 | } 213 | 214 | &:before { 215 | display: block; 216 | content: 'Популярные комментарии'; 217 | margin-bottom: 15px; 218 | padding-bottom: 10px; 219 | font-weight: 700; 220 | font-size: $size--body-1; 221 | width: 100%; 222 | border-bottom: 1px solid #eff2ec; 223 | } 224 | 225 | &__author, &__info { 226 | display: flex; 227 | } 228 | 229 | /* Отступ от автора комментария до текста*/ 230 | &__author + &__comment-text { 231 | margin-top: 2px; 232 | } 233 | 234 | &__avatar&__avatar&__avatar { 235 | height: $size--avatar; 236 | width: $size--avatar; 237 | min-width: $size--avatar; 238 | flex-basis: $size--avatar; 239 | } 240 | 241 | &__title&__title { 242 | font-size: $size--body-0; 243 | color: #777; 244 | line-height: $size--avatar; 245 | } 246 | 247 | &__subtitle&__subtitle { 248 | margin-left: 5px; 249 | font-size: $size--body-0; 250 | color: #a6aca1; 251 | line-height: $size--avatar; 252 | } 253 | 254 | &__title, &__title + &__subtitle { 255 | margin-top: 0; 256 | &:after, &:before { 257 | display: none; 258 | } 259 | } 260 | 261 | &__comment + &__comment { 262 | margin-top: $size--space_between; 263 | padding-top: 0; 264 | border-top: 0; 265 | } 266 | 267 | .blockquote { 268 | margin-bottom: 5px; 269 | } 270 | 271 | /* Для развёрнутого комментария добавим отступы параграфа */ 272 | .fold__hidden { 273 | &:before, &:after { 274 | display: block; 275 | height: 2px; 276 | content: ""; 277 | } 278 | } 279 | 280 | .button { 281 | color: $color--text; 282 | background-color: #eff2ec; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /hosts/rozhdestvenskiy.ru/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://rozhdestvenskiy.ru 2 | -------------------------------------------------------------------------------- /hosts/rozhdestvenskiy.ru/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - sbmaxx -------------------------------------------------------------------------------- /hosts/rozhdestvenskiy.ru/style.css: -------------------------------------------------------------------------------- 1 | .cover__title { 2 | font-weight: normal; 3 | } 4 | 5 | .page__cover + .page__markup { /* stylelint-disable-line */ 6 | margin-top: 10px; 7 | } 8 | 9 | .page__unit + .page__footer { /* stylelint-disable-line */ 10 | margin-top: 20px; 11 | } 12 | 13 | .page__source { /* stylelint-disable-line */ 14 | display: none; 15 | } 16 | 17 | .footer { 18 | background: #fff; 19 | border-top: 1px solid rgba(0, 0, 0, 0.1) 20 | } 21 | 22 | .footer__about { 23 | padding-top: 16px; 24 | padding-bottom: 16px; 25 | } 26 | 27 | .footer_theme_dark .footer__link.link { 28 | color: #777; 29 | } 30 | 31 | .footer_view_default { 32 | padding-bottom: 0; 33 | } 34 | -------------------------------------------------------------------------------- /hosts/ru.wikipedia.org/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://ru.wikipedia.org 2 | -------------------------------------------------------------------------------- /hosts/ru.wikipedia.org/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - sbmaxx 2 | - tenorok 3 | -------------------------------------------------------------------------------- /hosts/ru.wikipedia.org/style.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable-next-line */ 2 | @import "../wikipedia.org/style"; 3 | 4 | .page .header > .link { 5 | background: transparent url('./wikipedia-wordmark-ru.svg') no-repeat 0 50%; 6 | background-size: 126px 20px; 7 | width: 126px; 8 | } 9 | -------------------------------------------------------------------------------- /hosts/ru.wikipedia.org/wikipedia-wordmark-ru.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/test.skolznev.ru/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - http://test.skolznev.ru 2 | -------------------------------------------------------------------------------- /hosts/test.skolznev.ru/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - sklznv 2 | -------------------------------------------------------------------------------- /hosts/test.skolznev.ru/style.css: -------------------------------------------------------------------------------- 1 | .page { 2 | background: #fff6f1; 3 | } 4 | -------------------------------------------------------------------------------- /hosts/vedomosti.ru/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://vedomosti.ru 2 | - https://www.vedomosti.ru 3 | -------------------------------------------------------------------------------- /hosts/vedomosti.ru/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - fapspirit 2 | - ilya-vasilyev 3 | - rogodec 4 | -------------------------------------------------------------------------------- /hosts/vedomosti.ru/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/vedomosti.ru/style.css: -------------------------------------------------------------------------------- 1 | .page { 2 | background-color: #fff6f1; 3 | font-family: Georgia, serif; 4 | } 5 | 6 | .i { 7 | font-family: HelveticaNeue,arial,sans-serif; 8 | } 9 | 10 | .header_wide-logo .image { 11 | background-position-x: center; 12 | background-image: url('images/logo.svg') !important; 13 | } 14 | 15 | .header.header_size_s { 16 | padding: 14px 0 8px; 17 | } 18 | 19 | .header.header_size_s .image { 20 | display: block; 21 | margin: 0 auto; 22 | height: 32px; 23 | } 24 | 25 | .header__title-link { 26 | text-align: center; 27 | } 28 | 29 | .title, 30 | .cover .title { 31 | font-family: "Helvetica Neue", Arial, sans-serif; 32 | font-size: 27px; 33 | font-weight: 400; 34 | } 35 | 36 | .cover .description.description { 37 | color: #857871; 38 | font-family: "Helvetica Neue", Arial, sans-serif; 39 | font-size: 15px; 40 | margin-top: 20px; 41 | } 42 | 43 | .cover .image_cover { 44 | margin-top: 20px; 45 | } 46 | 47 | .cover { 48 | margin-bottom: 20px 49 | } 50 | 51 | .paragraph.paragraph { 52 | font-size: 17px; 53 | line-height: 22px; 54 | color: #423e3d; 55 | margin-bottom: 15px; 56 | } 57 | 58 | .related { 59 | margin-top: 52px; 60 | } 61 | 62 | .related.autoload { 63 | margin: 0; 64 | } 65 | 66 | .related__title { 67 | font-size: 24px; 68 | line-height: 34px; 69 | font-family: "Helvetica Neue", Arial, sans-serif; 70 | color: #2e6f6f; 71 | font-weight: 400; 72 | } 73 | 74 | .link, .footer .link { 75 | color: #2e6f6f 76 | } 77 | 78 | .snippet__title-link { 79 | font-size: 19px; 80 | color: #332f2e; 81 | } 82 | 83 | .snippet__footer .meta__item { 84 | color: #857871; 85 | } 86 | 87 | .source .button { 88 | background-color: transparent; 89 | border: 1px solid #e3d1c7; 90 | } 91 | 92 | .footer .paragraph { 93 | margin: 0; 94 | font-size: 16px; 95 | line-height: 18px; 96 | } 97 | -------------------------------------------------------------------------------- /hosts/wikipedia.org/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://wikipedia.org 2 | -------------------------------------------------------------------------------- /hosts/wikipedia.org/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - sbmaxx 2 | - tenorok 3 | - Peccansy -------------------------------------------------------------------------------- /hosts/wikipedia.org/style.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | 3 | .infobox { 4 | position: relative; 5 | display: flex; 6 | flex: 1 1 100%; 7 | flex-flow: column nowrap; 8 | margin-bottom: 20px; 9 | width: 100% !important; 10 | border: 1px solid #eaecf0; 11 | background-color: #f8f9fa; 12 | text-align: left; 13 | font-size: 14px; 14 | line-height: 23px; 15 | 16 | div, span, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, 17 | acronym, address, big, cite, code, del, ins, em, img, small, 18 | strike, strong, sub, sup, tt, b, u, i, center, dl, dt, dd, ol, 19 | ul, li, fieldset, form, label, legend, input, textarea, 20 | button, select, audio, video { 21 | margin: 0; 22 | padding: 0; 23 | border: 0; 24 | background: none; 25 | vertical-align: baseline; 26 | font: inherit; 27 | font-size: 100%; 28 | } 29 | 30 | > tbody { 31 | display: block; 32 | } 33 | 34 | table td { 35 | padding: initial; 36 | width: initial; 37 | } 38 | 39 | > tbody > tr { 40 | display: flex; 41 | flex-flow: row nowrap; 42 | min-width: 100%; 43 | } 44 | 45 | th, 46 | td { 47 | padding: 7px 10px; 48 | border: 0; 49 | vertical-align: top; 50 | } 51 | 52 | &.t-geoinfobox td:only-child, 53 | &.t-geoinfobox th:only-child { 54 | text-align: center 55 | } 56 | 57 | &.t-geoinfobox-nickname { 58 | font-style: italic 59 | } 60 | 61 | &.t-geoinfobox-cave th:only-child, 62 | &.t-geoinfobox-mount th:only-child { 63 | background: #e7dcc3 64 | } 65 | 66 | &.t-geoinfobox-surface th:only-child { 67 | background: #ffe4c4 68 | } 69 | 70 | &.t-geoinfobox-blue th:only-child, 71 | &.t-geoinfobox-water th:only-child { 72 | background: #c0daff 73 | } 74 | 75 | &.t-geoinfobox-underwater th:only-child { 76 | background: #b0e0e6 77 | } 78 | 79 | &.t-geoinfobox-green th:only-child { 80 | background: #d0f0c0 81 | } 82 | 83 | &.t-geoinfobox-yellow th:only-child { 84 | background: #fdeaa8 85 | } 86 | 87 | &.t-geoinfobox-ny th:only-child { 88 | background: #cbd5c4; 89 | border: 1px solid #aaaaaa 90 | } 91 | 92 | &.t-geoinfobox-ny th:first-child:not(:only-child) { 93 | background: #cbd5c4; 94 | text-align: right; 95 | padding: 0 0.5em 96 | } 97 | 98 | &.t-geoinfobox-grey th:only-child { 99 | background: #dcdcdc 100 | } 101 | 102 | &.t-geoinfobox-crater th:only-child { 103 | background: #fcafa7 104 | } 105 | 106 | &.t-geoinfobox td:only-child, 107 | &.t-geoinfobox th:only-child { 108 | text-align: center; 109 | } 110 | 111 | td:only-child, 112 | th:only-child { 113 | width: 100%; 114 | } 115 | 116 | tbody > tr > td, 117 | tbody > tr > th { 118 | flex: 1 0; 119 | } 120 | 121 | td p { 122 | margin: 0 !important; 123 | } 124 | 125 | th > ul, 126 | td > ul, 127 | [data-wikidata-property-id] > ul { 128 | margin: 0; 129 | padding: 0; 130 | list-style-type: none; 131 | list-style-image: none; 132 | } 133 | 134 | th > ol, 135 | td > ol, 136 | [data-wikidata-property-id] > ol { 137 | margin: 0 0 0 2em; 138 | line-height: 1.1em; 139 | } 140 | 141 | ul li:only-child { 142 | list-style-type: none; 143 | } 144 | 145 | .hlist dd, 146 | .hlist dt, 147 | .hlist li { 148 | display: inline; 149 | } 150 | 151 | .hlist dd:after, 152 | .hlist li:after { 153 | content: " · "; 154 | font-weight: bold; 155 | } 156 | 157 | .hlist dd:last-child:after, 158 | .hlist li:last-child:after { 159 | content: ""; 160 | } 161 | 162 | .NavHead small { 163 | margin-bottom: 0 !important; 164 | } 165 | 166 | .center { 167 | width: 100%; 168 | text-align: center; 169 | } 170 | 171 | a { 172 | text-decoration: none; 173 | } 174 | 175 | a > img { 176 | max-width: 100% !important; 177 | height: auto !important; 178 | } 179 | 180 | caption { 181 | text-align: left; 182 | display: flex; 183 | flex-flow: column nowrap; 184 | font-weight: bold; 185 | } 186 | 187 | [class*=mw-customtoggle-] { 188 | display: none; 189 | } 190 | } 191 | 192 | .infobox-above { 193 | font-size: 120%; 194 | text-align: center 195 | } 196 | 197 | .infobox-image { 198 | padding-left: 0; 199 | padding-right: 0; 200 | text-align: center 201 | } 202 | 203 | table.infobox, 204 | .infobox table { 205 | display: block; 206 | overflow-x: auto; 207 | overflow-y: hidden; 208 | margin: 1em 0; 209 | width: 100% !important; 210 | } 211 | 212 | a { 213 | /* Чтобы ссылки не сливались на синем фоне. 214 | Ставим одинаковый цвет ссылок на всей страницы. */ 215 | color: #04b; 216 | } 217 | 218 | /* Добавим заливку для шапки на всю ширину*/ 219 | .cover:before { 220 | content: ''; 221 | position: absolute; 222 | left: 0; 223 | top: 0; 224 | width: 100%; 225 | height: 52px; 226 | background-color: #eaecf0; 227 | } 228 | 229 | /* Разделитель больше не нужен */ 230 | .divider { 231 | display: none; 232 | } 233 | 234 | /* Убираем текст *.wikipedia.org */ 235 | .header_host .header-title { 236 | text-indent: -1000px; 237 | min-height: 28px; 238 | padding-top: 0; 239 | } 240 | 241 | .header_host { 242 | padding: 12px 0; 243 | } 244 | 245 | .header_sticky-fixed.header_host { 246 | padding: 12px 10px 12px 14px; 247 | background-color: #eaecf0; 248 | } 249 | 250 | .turbo-overlay-close-button .turbo-icon { 251 | fill: #666; 252 | } 253 | 254 | .page[data-customcss-gte~="3"] .header-turbo .header-turbo-icon { 255 | fill: #999; 256 | } 257 | -------------------------------------------------------------------------------- /hosts/www.drive.ru/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://www.drive.ru 2 | -------------------------------------------------------------------------------- /hosts/www.drive.ru/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - designeng 2 | - anwerso 3 | -------------------------------------------------------------------------------- /hosts/www.drive.ru/menu_trigger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/www.drive.ru/style.css: -------------------------------------------------------------------------------- 1 | .page { 2 | background-color: #e8e8e8; 3 | } 4 | 5 | .link { 6 | color: rgb(68, 84, 111); 7 | text-decoration: underline; 8 | } 9 | 10 | .header { 11 | position: absolute; 12 | left: 0; 13 | height: 30px; 14 | margin: 0; 15 | background: rgba(252, 42, 75, 1); 16 | text-align: center; 17 | width: 100%; 18 | } 19 | 20 | .header-title { 21 | /* явно задаем отступы чтобы центрировать лого */ 22 | padding-right: 80px; 23 | padding-left: 72px; /* 80px - 8px (margin-left) */ 24 | } 25 | 26 | .header__title-link { 27 | text-decoration: none; 28 | } 29 | 30 | .cover__content > .divider { 31 | padding-top: 52px; 32 | } 33 | 34 | .header_logo-host .header-title { 35 | text-transform: uppercase; 36 | color: #fff; 37 | font-family: "Helvetica Narrow", "Arial Narrow", Arial, sans-serif; 38 | font-size: 24px; 39 | } 40 | 41 | .cover .description { 42 | font: 12px/24px sans-serif; 43 | color: rgba(236, 236, 236, 0.7); 44 | text-shadow: none; 45 | } 46 | 47 | .header-turbo .header-turbo-icon { 48 | fill: #fff; 49 | } 50 | 51 | .image_type_logo { 52 | display: none; 53 | } 54 | 55 | .sandwich-menu__handler { 56 | position: relative; 57 | margin-right: 10px; 58 | width: 21px; 59 | height: 50px; 60 | background: url(menu_trigger.svg) center no-repeat; 61 | background-size: 100%; 62 | } 63 | 64 | .source, .footer { 65 | text-align: center; 66 | } 67 | 68 | .source .button { 69 | background-color: #fff; 70 | border-radius: 5px; 71 | width: 80%; 72 | } 73 | 74 | .share__more { 75 | background-color: #fff; 76 | border-radius: 5px; 77 | } 78 | 79 | .footer__link { 80 | font-size: 13px; 81 | color: #999; 82 | text-decoration: none; 83 | line-height: 18px; 84 | } 85 | -------------------------------------------------------------------------------- /hosts/www.drive2.ru/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://www.drive2.ru -------------------------------------------------------------------------------- /hosts/www.drive2.ru/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - anwerso -------------------------------------------------------------------------------- /hosts/www.drive2.ru/icon-nav.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/www.drive2.ru/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hosts/www.drive2.ru/style.css: -------------------------------------------------------------------------------- 1 | .cover { 2 | /* Внутри cover есть блоки с position: absolute. Минимизируем риски */ 3 | position: relative; 4 | } 5 | 6 | .page { 7 | background-color: #e8e8e8; 8 | } 9 | 10 | .link { 11 | text-decoration: underline; 12 | text-decoration-color: rgba(32,80,144,.35); 13 | color: #205090; 14 | } 15 | 16 | .button { 17 | background-color: #fff; 18 | } 19 | 20 | .cover__content:before { 21 | content: ''; 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | right: 0; 26 | height: 50px; 27 | background-color: rgba(0, 0, 0, .9); 28 | } 29 | 30 | .header.header { 31 | padding: 0; 32 | } 33 | 34 | .header__title-link { 35 | width: 120px; 36 | height: 50px; 37 | background: #c03 url(./logo.svg) no-repeat; 38 | } 39 | 40 | .cover .divider, 41 | .header_wide-logo .image { 42 | display: none; 43 | } 44 | 45 | .header-turbo .header-turbo-icon { 46 | fill: #ccc; 47 | } 48 | 49 | .sandwich-menu__handler { 50 | background-image: url(./icon-nav.svg); 51 | } 52 | -------------------------------------------------------------------------------- /hosts/www.rbc.ru/HOSTS.yaml: -------------------------------------------------------------------------------- 1 | - https://www.rbc.ru 2 | -------------------------------------------------------------------------------- /hosts/www.rbc.ru/OWNERS.yaml: -------------------------------------------------------------------------------- 1 | - olliva 2 | -------------------------------------------------------------------------------- /hosts/www.rbc.ru/images/rbc_mob.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hosts/www.rbc.ru/style.css: -------------------------------------------------------------------------------- 1 | .cover .title { 2 | margin: 0; 3 | font-size: 30px; 4 | line-height: 32px; 5 | color: #222; 6 | } 7 | 8 | .page .cover .description { 9 | font-size: 11px; 10 | text-transform: uppercase; 11 | color: #222; 12 | font-weight: 700; 13 | letter-spacing: 1px; 14 | margin-top: 12px; 15 | line-height: 15px; 16 | } 17 | 18 | .paragraph { 19 | margin: 0; 20 | font-family: Georgia, serif; 21 | font-size: 17px; 22 | color: #333; 23 | letter-spacing: 0.4px; 24 | line-height: 24px; 25 | } 26 | 27 | .link { 28 | background: linear-gradient(to bottom,rgba(0,0,0,0) 0,#16b67f 100%); 29 | background-repeat: repeat-x; 30 | background-position: 0 100%; 31 | background-size: 1px 1px; 32 | color: #333; 33 | text-decoration: none; 34 | } 35 | 36 | .footer__link { 37 | color: #9B9B9B; 38 | background: 0; 39 | font-family: HelveticaNeue,arial,sans-serif; 40 | font-size: 15px; 41 | } 42 | 43 | .footer__divider { 44 | color: #9B9B9B; 45 | background: 0; 46 | font-family: HelveticaNeue,arial,sans-serif; 47 | font-size: 15px; 48 | } 49 | 50 | .footer_view_inline .footer__links { 51 | border: none; 52 | } 53 | 54 | .autoload__divider { 55 | height: 0; 56 | } 57 | 58 | .cover .divider { 59 | border: none; 60 | } 61 | 62 | .header { 63 | position: relative; 64 | background-color: #222; 65 | margin-left: -14px; 66 | margin-right: -14px; 67 | } 68 | 69 | .header .link { 70 | background: 0; 71 | margin-left: 14px; 72 | } 73 | 74 | .header__aside { 75 | position: absolute; 76 | top: 0; 77 | right: 0; 78 | margin-right: 14px; 79 | } 80 | 81 | .header.header_size_s .image { 82 | height: 21px; 83 | background-image: url('images/rbc_mob.svg') !important; 84 | } 85 | 86 | .cover .divider+.title { 87 | margin-top: 28px; 88 | } 89 | 90 | .unit+.footer { 91 | margin-top: 40px; 92 | } 93 | -------------------------------------------------------------------------------- /lib/clean.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const glob = require('glob'); 3 | 4 | module.exports = function run(mask) { 5 | const files = glob.sync(mask); 6 | 7 | return Promise.all(files.map(f => fs.unlink(f))); 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /lib/gzip.js: -------------------------------------------------------------------------------- 1 | const zlib = require('zlib'); 2 | const fs = require('fs-extra'); 3 | const glob = require('glob'); 4 | 5 | module.exports = function run(mask) { 6 | const files = glob.sync(mask); 7 | 8 | const promises = files.map(from => { 9 | const to = from.replace(/\.css$/, '.css.gz'); 10 | const gzip = zlib.createGzip(); 11 | const rs = fs.createReadStream(from); 12 | const ws = fs.createWriteStream(to); 13 | 14 | rs.pipe(gzip).pipe(ws); 15 | 16 | return new Promise(resolve => ws.once('finish', resolve)); 17 | }); 18 | 19 | return Promise.all(promises); 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /lib/hosts.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | const Yaml = require('js-yaml'); 4 | const fs = require('fs'); 5 | const { URL } = require('url'); 6 | 7 | const rules = []; 8 | 9 | function styleFilesValidator(files) { 10 | if (files.includes('style.css') && files.includes('style.scss')) { 11 | return 'Should exists only one CSS entry point. Found both.'; 12 | } 13 | 14 | if (!files.includes('style.css') && !files.includes('style.scss')) { 15 | return 'Should exists CSS entry point (style.css or style.scss).'; 16 | } 17 | 18 | return true; 19 | } 20 | 21 | function ownersFilesValidator(files, dir) { 22 | const file = 'OWNERS.yaml'; 23 | 24 | if (!files.includes(file)) { 25 | return `Should exists ${file}`; 26 | } 27 | 28 | try { 29 | Yaml.safeLoad(fs.readFileSync(path.join(dir, file), 'utf8')); 30 | return true; 31 | } catch(e) { 32 | return e; 33 | } 34 | } 35 | 36 | function hostsFileValidator(files, dir) { 37 | const file = 'HOSTS.yaml'; 38 | 39 | if (!files.includes(file)) { 40 | return `Should exists ${file}`; 41 | } 42 | 43 | let hosts; 44 | try { 45 | hosts = Yaml.safeLoad(fs.readFileSync(path.join(dir, file), 'utf8')); 46 | } catch(e) { 47 | return e; 48 | } 49 | 50 | if (!Array.isArray(hosts)) { 51 | return `${file} should contains array of hostnames.'`; 52 | } 53 | 54 | const errs = validateHosts(hosts, dir); 55 | 56 | if (errs.length) { 57 | return errs.map(e => e.message).join(', '); 58 | } 59 | 60 | return true; 61 | } 62 | 63 | const uniqHost = new Map(); 64 | 65 | function validateHosts(hosts, dir) { 66 | return hosts.map(host => { 67 | try { 68 | const origin = new URL(host).origin; 69 | if (uniqHost.has(origin)) { 70 | throw new Error(`${origin} already exists in ${uniqHost.get(origin)}`); 71 | } 72 | uniqHost.set(origin, path.basename(dir)); 73 | return true; 74 | } catch(e) { 75 | return e; 76 | } 77 | }).filter(res => res !== true); 78 | } 79 | 80 | rules.push(styleFilesValidator, ownersFilesValidator, hostsFileValidator); 81 | 82 | function list() { 83 | return glob.sync('./hosts/*/'); 84 | } 85 | 86 | function validator(dir) { 87 | const files = glob.sync(path.join(dir, '*')).map(file => path.basename(file)); 88 | 89 | return rules.reduce((acc, rule) => { 90 | const result = rule(files, dir); 91 | 92 | if (result === true) { 93 | return acc; 94 | } 95 | 96 | return acc.concat({ rule: rule.name, error: result }); 97 | }, []); 98 | } 99 | 100 | module.exports = { 101 | list, 102 | validator 103 | }; 104 | -------------------------------------------------------------------------------- /lib/postcss.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const postcssScss = require('postcss-scss'); 3 | const postcssConfig = require('../postcss.config'); 4 | let postcss; 5 | 6 | module.exports = async function run(from, to) { 7 | postcss = postcss || (postcss = require('postcss')); 8 | 9 | const content = await fs.readFile(from, 'utf-8'); 10 | 11 | return postcss(postcssConfig.plugins) 12 | .process(content, { from, to, syntax: postcssScss }) 13 | .then(result => result.css); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/server/cssOrScss.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs'); 3 | 4 | /** 5 | * 6 | * @param {string} dir - директория хоста 7 | * @returns {string} 8 | */ 9 | function cssOrScss(dir) { 10 | const css = join(dir, 'style.css'); 11 | const scss = join(dir, 'style.scss'); 12 | 13 | const cssExists = fs.existsSync(css); 14 | const scssExists = fs.existsSync(scss); 15 | 16 | return scssExists ? scss : cssExists ? css : ''; 17 | } 18 | 19 | module.exports = cssOrScss; 20 | -------------------------------------------------------------------------------- /lib/server/get-host-style.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | const glob = require('glob'); 3 | const fs = require('fs'); 4 | const { dirname } = require('path'); 5 | const { URL } = require('url'); 6 | const cssOrScss = require('./cssOrScss'); 7 | 8 | module.exports = function findCustom(url) { 9 | let origin; 10 | 11 | try { 12 | origin = new URL(url).origin; 13 | } catch(e) { 14 | console.log(e); 15 | return ''; 16 | } 17 | 18 | const hosts = glob.sync('./hosts/*/HOSTS.yaml').map(file => { 19 | try { 20 | const styleFile = cssOrScss(dirname(file)); 21 | 22 | return { 23 | hosts: yaml.safeLoad(fs.readFileSync(file, 'utf8')), 24 | style: styleFile 25 | }; 26 | } catch(e) { 27 | console.log(e); 28 | } 29 | }).filter(data => data && data.style); 30 | 31 | const host = hosts.find(host => host.hosts.includes(origin)); 32 | return host ? host.style : ''; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/server/inject-livereload.js: -------------------------------------------------------------------------------- 1 | const replaceCustomCSS = require('./replace-custom-css'); 2 | 3 | const getHostStyle = require('./get-host-style'); 4 | 5 | module.exports = function injectLiveReload(req, html, host) { 6 | const style = getHostStyle(host); 7 | 8 | let lr = ''; 9 | 10 | if (!style) { 11 | lr = ``; 12 | } else { 13 | const min = style.replace(/\.s?css$/, '.min.css'); 14 | console.log(`Injected livreload style ${style}`); 15 | 16 | lr = [ 17 | ``, 18 | '' 19 | ].join(''); 20 | } 21 | 22 | return Promise.resolve(replaceCustomCSS(html, lr)); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/server/inject-postcss-runtime.js: -------------------------------------------------------------------------------- 1 | const getHostStyle = require('./get-host-style'); 2 | const postcss = require('../postcss'); 3 | const replaceCustomCSS = require('./replace-custom-css'); 4 | const removeCustomCSS = require('./remove-custom-css'); 5 | 6 | module.exports = function getHostCSS(req, html, origin) { 7 | const style = getHostStyle(origin); 8 | 9 | if (!style) { 10 | return Promise.resolve(removeCustomCSS(html)); 11 | } 12 | 13 | console.log(`Injected ${style}`); 14 | return postcss(style).then(style => { 15 | return replaceCustomCSS(html, ``); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/server/inject-prebuild.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const fs = require('fs-extra'); 3 | const replaceCustomCSS = require('./replace-custom-css'); 4 | 5 | async function getBuildResult(pullRequestDir) { 6 | const buildFile = join('checkout', pullRequestDir, 'build.json'); 7 | const isExists = await fs.exists(buildFile); 8 | 9 | if (!isExists) { 10 | return null; 11 | } 12 | 13 | return JSON.parse(await fs.readFile(buildFile, 'utf8')); 14 | } 15 | 16 | module.exports = function injectPrebuild({ ctx: { pullRequest } }, html, origin) { 17 | return getBuildResult(pullRequest).then(build => { 18 | const key = build.hosts[origin]; 19 | const css = build.css[key]; 20 | 21 | return replaceCustomCSS(html, ``); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/server/inject.js: -------------------------------------------------------------------------------- 1 | const injectLiveReload = require('./inject-livereload'); 2 | const injectPrebuild = require('./inject-prebuild'); 3 | const injectPostCSSRuntime = require('./inject-postcss-runtime'); 4 | 5 | if (process.env.LIVERELOAD === 'true' && process.env.PUBLIC !== 'true' && process.env.NODE_ENV === 'development') { 6 | module.exports = function inject(req, html, host) { 7 | return injectLiveReload(req, html, host); 8 | }; 9 | } else if (process.env.NODE_ENV === 'development') { 10 | module.exports = function inject(req, html, host) { 11 | return injectPostCSSRuntime(req, html, host); 12 | }; 13 | } else { 14 | module.exports = function inject(req, html, host) { 15 | return injectPrebuild(req, html, host); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /lib/server/lr.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar'); 2 | const postcss = require('../postcss'); 3 | const fs = require('fs'); 4 | const lr = require('tiny-lr'); 5 | 6 | const watcher = chokidar.watch(['./hosts/**/*.css', './hosts/**/*.scss'], { 7 | ignored: /\.min\.css$/, 8 | persistent: true 9 | }); 10 | 11 | const ready = () => { 12 | const update = file => { 13 | const build = file.replace(/style\.s?css$/, 'style.min.css'); 14 | 15 | postcss(file).then(style => { 16 | fs.writeFile(build, style, 'utf8', () => lr.changed(file)); 17 | }).catch(e => console.error(e)); 18 | }; 19 | 20 | const remove = file => { 21 | fs.unlink(file.replace(/\.s?css$/, '.min.css')); 22 | }; 23 | 24 | watcher 25 | .on('add', file => update(file)) 26 | .on('change', file => update(file)) 27 | .on('unlink', file => remove(file)); 28 | }; 29 | 30 | watcher.on('ready', ready); 31 | 32 | module.exports = lr; 33 | -------------------------------------------------------------------------------- /lib/server/remove-csp.js: -------------------------------------------------------------------------------- 1 | const meta = //; 2 | 3 | module.exports = function removeCSP(html) { 4 | return html.replace(meta, ''); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/server/remove-custom-css.js: -------------------------------------------------------------------------------- 1 | module.exports = function removeCustomCSS(html) { 2 | const customStyle = ' 165 | 166 | 167 |
168 |
169 |
170 |
172 | 186 |
187 |
188 |
189 | 190 | 191 | -------------------------------------------------------------------------------- /public/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Яндекс 6 | 7 | 8 | 9 | 144 | 145 | 146 |
147 |
148 |
149 | 166 |
167 |
168 |
170 | 171 |
172 |
173 |
174 |
175 | 176 | 177 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Яндекс 6 | 7 | 8 | 9 | 157 | 158 | 159 |
160 |
161 | 162 | 163 | 164 | 165 |
166 |
167 |
168 |
169 |
Например, https://rozhdestvenskiy.ru
170 |
171 | -------------------------------------------------------------------------------- /public/vendors/livereload.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o tag"); 326 | return; 327 | } 328 | } 329 | this.reloader = new Reloader(this.window, this.console, Timer); 330 | this.connector = new Connector(this.options, this.WebSocket, Timer, { 331 | connecting: (function(_this) { 332 | return function() {}; 333 | })(this), 334 | socketConnected: (function(_this) { 335 | return function() {}; 336 | })(this), 337 | connected: (function(_this) { 338 | return function(protocol) { 339 | var _base; 340 | if (typeof (_base = _this.listeners).connect === "function") { 341 | _base.connect(); 342 | } 343 | _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ")."); 344 | return _this.analyze(); 345 | }; 346 | })(this), 347 | error: (function(_this) { 348 | return function(e) { 349 | if (e instanceof ProtocolError) { 350 | if (typeof console !== "undefined" && console !== null) { 351 | return console.log("" + e.message + "."); 352 | } 353 | } else { 354 | if (typeof console !== "undefined" && console !== null) { 355 | return console.log("LiveReload internal error: " + e.message); 356 | } 357 | } 358 | }; 359 | })(this), 360 | disconnected: (function(_this) { 361 | return function(reason, nextDelay) { 362 | var _base; 363 | if (typeof (_base = _this.listeners).disconnect === "function") { 364 | _base.disconnect(); 365 | } 366 | switch (reason) { 367 | case 'cannot-connect': 368 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec."); 369 | case 'broken': 370 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec."); 371 | case 'handshake-timeout': 372 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec."); 373 | case 'handshake-failed': 374 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec."); 375 | case 'manual': 376 | break; 377 | case 'error': 378 | break; 379 | default: 380 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec."); 381 | } 382 | }; 383 | })(this), 384 | message: (function(_this) { 385 | return function(message) { 386 | switch (message.command) { 387 | case 'reload': 388 | return _this.performReload(message); 389 | case 'alert': 390 | return _this.performAlert(message); 391 | } 392 | }; 393 | })(this) 394 | }); 395 | this.initialized = true; 396 | } 397 | 398 | LiveReload.prototype.on = function(eventName, handler) { 399 | return this.listeners[eventName] = handler; 400 | }; 401 | 402 | LiveReload.prototype.log = function(message) { 403 | return this.console.log("" + message); 404 | }; 405 | 406 | LiveReload.prototype.performReload = function(message) { 407 | var _ref, _ref1, _ref2; 408 | this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2))); 409 | return this.reloader.reload(message.path, { 410 | liveCSS: (_ref = message.liveCSS) != null ? _ref : true, 411 | liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true, 412 | reloadMissingCSS: (_ref2 = message.reloadMissingCSS) != null ? _ref2 : true, 413 | originalPath: message.originalPath || '', 414 | overrideURL: message.overrideURL || '', 415 | serverURL: "http://" + this.options.host + ":" + this.options.port 416 | }); 417 | }; 418 | 419 | LiveReload.prototype.performAlert = function(message) { 420 | return alert(message.message); 421 | }; 422 | 423 | LiveReload.prototype.shutDown = function() { 424 | var _base; 425 | if (!this.initialized) { 426 | return; 427 | } 428 | this.connector.disconnect(); 429 | this.log("LiveReload disconnected."); 430 | return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0; 431 | }; 432 | 433 | LiveReload.prototype.hasPlugin = function(identifier) { 434 | return !!this.pluginIdentifiers[identifier]; 435 | }; 436 | 437 | LiveReload.prototype.addPlugin = function(pluginClass) { 438 | var plugin; 439 | if (!this.initialized) { 440 | return; 441 | } 442 | if (this.hasPlugin(pluginClass.identifier)) { 443 | return; 444 | } 445 | this.pluginIdentifiers[pluginClass.identifier] = true; 446 | plugin = new pluginClass(this.window, { 447 | _livereload: this, 448 | _reloader: this.reloader, 449 | _connector: this.connector, 450 | console: this.console, 451 | Timer: Timer, 452 | generateCacheBustUrl: (function(_this) { 453 | return function(url) { 454 | return _this.reloader.generateCacheBustUrl(url); 455 | }; 456 | })(this) 457 | }); 458 | this.plugins.push(plugin); 459 | this.reloader.addPlugin(plugin); 460 | }; 461 | 462 | LiveReload.prototype.analyze = function() { 463 | var plugin, pluginData, pluginsData, _i, _len, _ref; 464 | if (!this.initialized) { 465 | return; 466 | } 467 | if (!(this.connector.protocol >= 7)) { 468 | return; 469 | } 470 | pluginsData = {}; 471 | _ref = this.plugins; 472 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 473 | plugin = _ref[_i]; 474 | pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {}; 475 | pluginData.version = plugin.constructor.version; 476 | } 477 | this.connector.sendCommand({ 478 | command: 'info', 479 | plugins: pluginsData, 480 | url: this.window.location.href 481 | }); 482 | }; 483 | 484 | return LiveReload; 485 | 486 | })(); 487 | 488 | }).call(this); 489 | 490 | },{"./connector":1,"./options":5,"./protocol":6,"./reloader":7,"./timer":9}],5:[function(require,module,exports){ 491 | (function() { 492 | var Options; 493 | 494 | exports.Options = Options = (function() { 495 | function Options() { 496 | this.https = false; 497 | this.host = null; 498 | this.port = 35729; 499 | this.snipver = null; 500 | this.ext = null; 501 | this.extver = null; 502 | this.mindelay = 1000; 503 | this.maxdelay = 60000; 504 | this.handshake_timeout = 5000; 505 | } 506 | 507 | Options.prototype.set = function(name, value) { 508 | if (typeof value === 'undefined') { 509 | return; 510 | } 511 | if (!isNaN(+value)) { 512 | value = +value; 513 | } 514 | return this[name] = value; 515 | }; 516 | 517 | return Options; 518 | 519 | })(); 520 | 521 | Options.extract = function(document) { 522 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1; 523 | _ref = document.getElementsByTagName('script'); 524 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 525 | element = _ref[_i]; 526 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) { 527 | options = new Options(); 528 | options.https = src.indexOf("https") === 0; 529 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) { 530 | options.host = mm[1]; 531 | if (mm[2]) { 532 | options.port = parseInt(mm[2], 10); 533 | } 534 | } 535 | if (m[2]) { 536 | _ref1 = m[2].split('&'); 537 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 538 | pair = _ref1[_j]; 539 | if ((keyAndValue = pair.split('=')).length > 1) { 540 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('=')); 541 | } 542 | } 543 | } 544 | return options; 545 | } 546 | } 547 | return null; 548 | }; 549 | 550 | }).call(this); 551 | 552 | },{}],6:[function(require,module,exports){ 553 | (function() { 554 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError, 555 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 556 | 557 | exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6'; 558 | 559 | exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7'; 560 | 561 | exports.ProtocolError = ProtocolError = (function() { 562 | function ProtocolError(reason, data) { 563 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\"."; 564 | } 565 | 566 | return ProtocolError; 567 | 568 | })(); 569 | 570 | exports.Parser = Parser = (function() { 571 | function Parser(handlers) { 572 | this.handlers = handlers; 573 | this.reset(); 574 | } 575 | 576 | Parser.prototype.reset = function() { 577 | return this.protocol = null; 578 | }; 579 | 580 | Parser.prototype.process = function(data) { 581 | var command, e, message, options, _ref; 582 | try { 583 | if (this.protocol == null) { 584 | if (data.match(/^!!ver:([\d.]+)$/)) { 585 | this.protocol = 6; 586 | } else if (message = this._parseMessage(data, ['hello'])) { 587 | if (!message.protocols.length) { 588 | throw new ProtocolError("no protocols specified in handshake message"); 589 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) { 590 | this.protocol = 7; 591 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) { 592 | this.protocol = 6; 593 | } else { 594 | throw new ProtocolError("no supported protocols found"); 595 | } 596 | } 597 | return this.handlers.connected(this.protocol); 598 | } else if (this.protocol === 6) { 599 | message = JSON.parse(data); 600 | if (!message.length) { 601 | throw new ProtocolError("protocol 6 messages must be arrays"); 602 | } 603 | command = message[0], options = message[1]; 604 | if (command !== 'refresh') { 605 | throw new ProtocolError("unknown protocol 6 command"); 606 | } 607 | return this.handlers.message({ 608 | command: 'reload', 609 | path: options.path, 610 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true 611 | }); 612 | } else { 613 | message = this._parseMessage(data, ['reload', 'alert']); 614 | return this.handlers.message(message); 615 | } 616 | } catch (_error) { 617 | e = _error; 618 | if (e instanceof ProtocolError) { 619 | return this.handlers.error(e); 620 | } else { 621 | throw e; 622 | } 623 | } 624 | }; 625 | 626 | Parser.prototype._parseMessage = function(data, validCommands) { 627 | var e, message, _ref; 628 | try { 629 | message = JSON.parse(data); 630 | } catch (_error) { 631 | e = _error; 632 | throw new ProtocolError('unparsable JSON', data); 633 | } 634 | if (!message.command) { 635 | throw new ProtocolError('missing "command" key', data); 636 | } 637 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) { 638 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data); 639 | } 640 | return message; 641 | }; 642 | 643 | return Parser; 644 | 645 | })(); 646 | 647 | }).call(this); 648 | 649 | },{}],7:[function(require,module,exports){ 650 | (function() { 651 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl; 652 | 653 | splitUrl = function(url) { 654 | var comboSign, hash, index, params; 655 | if ((index = url.indexOf('#')) >= 0) { 656 | hash = url.slice(index); 657 | url = url.slice(0, index); 658 | } else { 659 | hash = ''; 660 | } 661 | comboSign = url.indexOf('??'); 662 | if (comboSign >= 0) { 663 | if (comboSign + 1 !== url.lastIndexOf('?')) { 664 | index = url.lastIndexOf('?'); 665 | } 666 | } else { 667 | index = url.indexOf('?'); 668 | } 669 | if (index >= 0) { 670 | params = url.slice(index); 671 | url = url.slice(0, index); 672 | } else { 673 | params = ''; 674 | } 675 | return { 676 | url: url, 677 | params: params, 678 | hash: hash 679 | }; 680 | }; 681 | 682 | pathFromUrl = function(url) { 683 | var path; 684 | url = splitUrl(url).url; 685 | if (url.indexOf('file://') === 0) { 686 | path = url.replace(/^file:\/\/(localhost)?/, ''); 687 | } else { 688 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/'); 689 | } 690 | return decodeURIComponent(path); 691 | }; 692 | 693 | pickBestMatch = function(path, objects, pathFunc) { 694 | var bestMatch, object, score, _i, _len; 695 | bestMatch = { 696 | score: 0 697 | }; 698 | for (_i = 0, _len = objects.length; _i < _len; _i++) { 699 | object = objects[_i]; 700 | score = numberOfMatchingSegments(path, pathFunc(object)); 701 | if (score > bestMatch.score) { 702 | bestMatch = { 703 | object: object, 704 | score: score 705 | }; 706 | } 707 | } 708 | if (bestMatch.score > 0) { 709 | return bestMatch; 710 | } else { 711 | return null; 712 | } 713 | }; 714 | 715 | numberOfMatchingSegments = function(path1, path2) { 716 | var comps1, comps2, eqCount, len; 717 | path1 = path1.replace(/^\/+/, '').toLowerCase(); 718 | path2 = path2.replace(/^\/+/, '').toLowerCase(); 719 | if (path1 === path2) { 720 | return 10000; 721 | } 722 | comps1 = path1.split('/').reverse(); 723 | comps2 = path2.split('/').reverse(); 724 | len = Math.min(comps1.length, comps2.length); 725 | eqCount = 0; 726 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) { 727 | ++eqCount; 728 | } 729 | return eqCount; 730 | }; 731 | 732 | pathsMatch = function(path1, path2) { 733 | return numberOfMatchingSegments(path1, path2) > 0; 734 | }; 735 | 736 | IMAGE_STYLES = [ 737 | { 738 | selector: 'background', 739 | styleNames: ['backgroundImage'] 740 | }, { 741 | selector: 'border', 742 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] 743 | } 744 | ]; 745 | 746 | exports.Reloader = Reloader = (function() { 747 | function Reloader(window, console, Timer) { 748 | this.window = window; 749 | this.console = console; 750 | this.Timer = Timer; 751 | this.document = this.window.document; 752 | this.importCacheWaitPeriod = 200; 753 | this.plugins = []; 754 | } 755 | 756 | Reloader.prototype.addPlugin = function(plugin) { 757 | return this.plugins.push(plugin); 758 | }; 759 | 760 | Reloader.prototype.analyze = function(callback) { 761 | return results; 762 | }; 763 | 764 | Reloader.prototype.reload = function(path, options) { 765 | var plugin, _base, _i, _len, _ref; 766 | this.options = options; 767 | if ((_base = this.options).stylesheetReloadTimeout == null) { 768 | _base.stylesheetReloadTimeout = 15000; 769 | } 770 | _ref = this.plugins; 771 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 772 | plugin = _ref[_i]; 773 | if (plugin.reload && plugin.reload(path, options)) { 774 | return; 775 | } 776 | } 777 | if (options.liveCSS && path.match(/\.css(?:\.map)?$/i)) { 778 | if (this.reloadStylesheet(path)) { 779 | return; 780 | } 781 | } 782 | if (options.liveImg && path.match(/\.(jpe?g|png|gif)$/i)) { 783 | this.reloadImages(path); 784 | return; 785 | } 786 | if (options.isChromeExtension) { 787 | this.reloadChromeExtension(); 788 | return; 789 | } 790 | return this.reloadPage(); 791 | }; 792 | 793 | Reloader.prototype.reloadPage = function() { 794 | return this.window.document.location.reload(); 795 | }; 796 | 797 | Reloader.prototype.reloadChromeExtension = function() { 798 | return this.window.chrome.runtime.reload(); 799 | }; 800 | 801 | Reloader.prototype.reloadImages = function(path) { 802 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results; 803 | expando = this.generateUniqueString(); 804 | _ref = this.document.images; 805 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 806 | img = _ref[_i]; 807 | if (pathsMatch(path, pathFromUrl(img.src))) { 808 | img.src = this.generateCacheBustUrl(img.src, expando); 809 | } 810 | } 811 | if (this.document.querySelectorAll) { 812 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 813 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames; 814 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]"); 815 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 816 | img = _ref2[_k]; 817 | this.reloadStyleImages(img.style, styleNames, path, expando); 818 | } 819 | } 820 | } 821 | if (this.document.styleSheets) { 822 | _ref3 = this.document.styleSheets; 823 | _results = []; 824 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 825 | styleSheet = _ref3[_l]; 826 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando)); 827 | } 828 | return _results; 829 | } 830 | }; 831 | 832 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) { 833 | var e, rule, rules, styleNames, _i, _j, _len, _len1; 834 | try { 835 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 836 | } catch (_error) { 837 | e = _error; 838 | } 839 | if (!rules) { 840 | return; 841 | } 842 | for (_i = 0, _len = rules.length; _i < _len; _i++) { 843 | rule = rules[_i]; 844 | switch (rule.type) { 845 | case CSSRule.IMPORT_RULE: 846 | this.reloadStylesheetImages(rule.styleSheet, path, expando); 847 | break; 848 | case CSSRule.STYLE_RULE: 849 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 850 | styleNames = IMAGE_STYLES[_j].styleNames; 851 | this.reloadStyleImages(rule.style, styleNames, path, expando); 852 | } 853 | break; 854 | case CSSRule.MEDIA_RULE: 855 | this.reloadStylesheetImages(rule, path, expando); 856 | } 857 | } 858 | }; 859 | 860 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) { 861 | var newValue, styleName, value, _i, _len; 862 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) { 863 | styleName = styleNames[_i]; 864 | value = style[styleName]; 865 | if (typeof value === 'string') { 866 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) { 867 | return function(match, src) { 868 | if (pathsMatch(path, pathFromUrl(src))) { 869 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")"; 870 | } else { 871 | return match; 872 | } 873 | }; 874 | })(this)); 875 | if (newValue !== value) { 876 | style[styleName] = newValue; 877 | } 878 | } 879 | } 880 | }; 881 | 882 | Reloader.prototype.reloadStylesheet = function(path) { 883 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1; 884 | links = (function() { 885 | var _i, _len, _ref, _results; 886 | _ref = this.document.getElementsByTagName('link'); 887 | _results = []; 888 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 889 | link = _ref[_i]; 890 | if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) { 891 | _results.push(link); 892 | } 893 | } 894 | return _results; 895 | }).call(this); 896 | imported = []; 897 | _ref = this.document.getElementsByTagName('style'); 898 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 899 | style = _ref[_i]; 900 | if (style.sheet) { 901 | this.collectImportedStylesheets(style, style.sheet, imported); 902 | } 903 | } 904 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) { 905 | link = links[_j]; 906 | this.collectImportedStylesheets(link, link.sheet, imported); 907 | } 908 | if (this.window.StyleFix && this.document.querySelectorAll) { 909 | _ref1 = this.document.querySelectorAll('style[data-href]'); 910 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { 911 | style = _ref1[_k]; 912 | links.push(style); 913 | } 914 | } 915 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets"); 916 | match = pickBestMatch(path, links.concat(imported), (function(_this) { 917 | return function(l) { 918 | return pathFromUrl(_this.linkHref(l)); 919 | }; 920 | })(this)); 921 | if (match) { 922 | if (match.object.rule) { 923 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href); 924 | this.reattachImportedRule(match.object); 925 | } else { 926 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object))); 927 | this.reattachStylesheetLink(match.object); 928 | } 929 | } else { 930 | if (this.options.reloadMissingCSS) { 931 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one. To disable this behavior, set 'options.reloadMissingCSS' to 'false'."); 932 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) { 933 | link = links[_l]; 934 | this.reattachStylesheetLink(link); 935 | } 936 | } else { 937 | this.console.log("LiveReload will not reload path '" + path + "' because the stylesheet was not found on the page and 'options.reloadMissingCSS' was set to 'false'."); 938 | } 939 | } 940 | return true; 941 | }; 942 | 943 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) { 944 | var e, index, rule, rules, _i, _len; 945 | try { 946 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 947 | } catch (_error) { 948 | e = _error; 949 | } 950 | if (rules && rules.length) { 951 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) { 952 | rule = rules[index]; 953 | switch (rule.type) { 954 | case CSSRule.CHARSET_RULE: 955 | continue; 956 | case CSSRule.IMPORT_RULE: 957 | result.push({ 958 | link: link, 959 | rule: rule, 960 | index: index, 961 | href: rule.href 962 | }); 963 | this.collectImportedStylesheets(link, rule.styleSheet, result); 964 | break; 965 | default: 966 | break; 967 | } 968 | } 969 | } 970 | }; 971 | 972 | Reloader.prototype.waitUntilCssLoads = function(clone, func) { 973 | var callbackExecuted, executeCallback, poll; 974 | callbackExecuted = false; 975 | executeCallback = (function(_this) { 976 | return function() { 977 | if (callbackExecuted) { 978 | return; 979 | } 980 | callbackExecuted = true; 981 | return func(); 982 | }; 983 | })(this); 984 | clone.onload = (function(_this) { 985 | return function() { 986 | _this.console.log("LiveReload: the new stylesheet has finished loading"); 987 | _this.knownToSupportCssOnLoad = true; 988 | return executeCallback(); 989 | }; 990 | })(this); 991 | if (!this.knownToSupportCssOnLoad) { 992 | (poll = (function(_this) { 993 | return function() { 994 | if (clone.sheet) { 995 | _this.console.log("LiveReload is polling until the new CSS finishes loading..."); 996 | return executeCallback(); 997 | } else { 998 | return _this.Timer.start(50, poll); 999 | } 1000 | }; 1001 | })(this))(); 1002 | } 1003 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback); 1004 | }; 1005 | 1006 | Reloader.prototype.linkHref = function(link) { 1007 | return link.href || link.getAttribute('data-href'); 1008 | }; 1009 | 1010 | Reloader.prototype.reattachStylesheetLink = function(link) { 1011 | var clone, parent; 1012 | if (link.__LiveReload_pendingRemoval) { 1013 | return; 1014 | } 1015 | link.__LiveReload_pendingRemoval = true; 1016 | if (link.tagName === 'STYLE') { 1017 | clone = this.document.createElement('link'); 1018 | clone.rel = 'stylesheet'; 1019 | clone.media = link.media; 1020 | clone.disabled = link.disabled; 1021 | } else { 1022 | clone = link.cloneNode(false); 1023 | } 1024 | clone.href = this.generateCacheBustUrl(this.linkHref(link)); 1025 | parent = link.parentNode; 1026 | if (parent.lastChild === link) { 1027 | parent.appendChild(clone); 1028 | } else { 1029 | parent.insertBefore(clone, link.nextSibling); 1030 | } 1031 | return this.waitUntilCssLoads(clone, (function(_this) { 1032 | return function() { 1033 | var additionalWaitingTime; 1034 | if (/AppleWebKit/.test(navigator.userAgent)) { 1035 | additionalWaitingTime = 5; 1036 | } else { 1037 | additionalWaitingTime = 200; 1038 | } 1039 | return _this.Timer.start(additionalWaitingTime, function() { 1040 | var _ref; 1041 | if (!link.parentNode) { 1042 | return; 1043 | } 1044 | link.parentNode.removeChild(link); 1045 | clone.onreadystatechange = null; 1046 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0; 1047 | }); 1048 | }; 1049 | })(this)); 1050 | }; 1051 | 1052 | Reloader.prototype.reattachImportedRule = function(_arg) { 1053 | var href, index, link, media, newRule, parent, rule, tempLink; 1054 | rule = _arg.rule, index = _arg.index, link = _arg.link; 1055 | parent = rule.parentStyleSheet; 1056 | href = this.generateCacheBustUrl(rule.href); 1057 | media = rule.media.length ? [].join.call(rule.media, ', ') : ''; 1058 | newRule = "@import url(\"" + href + "\") " + media + ";"; 1059 | rule.__LiveReload_newHref = href; 1060 | tempLink = this.document.createElement("link"); 1061 | tempLink.rel = 'stylesheet'; 1062 | tempLink.href = href; 1063 | tempLink.__LiveReload_pendingRemoval = true; 1064 | if (link.parentNode) { 1065 | link.parentNode.insertBefore(tempLink, link); 1066 | } 1067 | return this.Timer.start(this.importCacheWaitPeriod, (function(_this) { 1068 | return function() { 1069 | if (tempLink.parentNode) { 1070 | tempLink.parentNode.removeChild(tempLink); 1071 | } 1072 | if (rule.__LiveReload_newHref !== href) { 1073 | return; 1074 | } 1075 | parent.insertRule(newRule, index); 1076 | parent.deleteRule(index + 1); 1077 | rule = parent.cssRules[index]; 1078 | rule.__LiveReload_newHref = href; 1079 | return _this.Timer.start(_this.importCacheWaitPeriod, function() { 1080 | if (rule.__LiveReload_newHref !== href) { 1081 | return; 1082 | } 1083 | parent.insertRule(newRule, index); 1084 | return parent.deleteRule(index + 1); 1085 | }); 1086 | }; 1087 | })(this)); 1088 | }; 1089 | 1090 | Reloader.prototype.generateUniqueString = function() { 1091 | return 'livereload=' + Date.now(); 1092 | }; 1093 | 1094 | Reloader.prototype.generateCacheBustUrl = function(url, expando) { 1095 | var hash, oldParams, originalUrl, params, _ref; 1096 | if (expando == null) { 1097 | expando = this.generateUniqueString(); 1098 | } 1099 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params; 1100 | if (this.options.overrideURL) { 1101 | if (url.indexOf(this.options.serverURL) < 0) { 1102 | originalUrl = url; 1103 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url); 1104 | this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url); 1105 | } 1106 | } 1107 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) { 1108 | return "" + sep + expando; 1109 | }); 1110 | if (params === oldParams) { 1111 | if (oldParams.length === 0) { 1112 | params = "?" + expando; 1113 | } else { 1114 | params = "" + oldParams + "&" + expando; 1115 | } 1116 | } 1117 | return url + params + hash; 1118 | }; 1119 | 1120 | return Reloader; 1121 | 1122 | })(); 1123 | 1124 | }).call(this); 1125 | 1126 | },{}],8:[function(require,module,exports){ 1127 | (function() { 1128 | var CustomEvents, LiveReload, k; 1129 | 1130 | CustomEvents = require('./customevents'); 1131 | 1132 | LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window); 1133 | 1134 | for (k in window) { 1135 | if (k.match(/^LiveReloadPlugin/)) { 1136 | LiveReload.addPlugin(window[k]); 1137 | } 1138 | } 1139 | 1140 | LiveReload.addPlugin(require('./less')); 1141 | 1142 | LiveReload.on('shutdown', function() { 1143 | return delete window.LiveReload; 1144 | }); 1145 | 1146 | LiveReload.on('connect', function() { 1147 | return CustomEvents.fire(document, 'LiveReloadConnect'); 1148 | }); 1149 | 1150 | LiveReload.on('disconnect', function() { 1151 | return CustomEvents.fire(document, 'LiveReloadDisconnect'); 1152 | }); 1153 | 1154 | CustomEvents.bind(document, 'LiveReloadShutDown', function() { 1155 | return LiveReload.shutDown(); 1156 | }); 1157 | 1158 | }).call(this); 1159 | 1160 | },{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){ 1161 | (function() { 1162 | var Timer; 1163 | 1164 | exports.Timer = Timer = (function() { 1165 | function Timer(func) { 1166 | this.func = func; 1167 | this.running = false; 1168 | this.id = null; 1169 | this._handler = (function(_this) { 1170 | return function() { 1171 | _this.running = false; 1172 | _this.id = null; 1173 | return _this.func(); 1174 | }; 1175 | })(this); 1176 | } 1177 | 1178 | Timer.prototype.start = function(timeout) { 1179 | if (this.running) { 1180 | clearTimeout(this.id); 1181 | } 1182 | this.id = setTimeout(this._handler, timeout); 1183 | return this.running = true; 1184 | }; 1185 | 1186 | Timer.prototype.stop = function() { 1187 | if (this.running) { 1188 | clearTimeout(this.id); 1189 | this.running = false; 1190 | return this.id = null; 1191 | } 1192 | }; 1193 | 1194 | return Timer; 1195 | 1196 | })(); 1197 | 1198 | Timer.start = function(timeout, func) { 1199 | return setTimeout(func, timeout); 1200 | }; 1201 | 1202 | }).call(this); 1203 | 1204 | },{}]},{},[8]); 1205 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turboext/css/b359413a462133efdd7bef3deb61eda2acb5c3bd/screencast.gif -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | if (!process.env.NODE_ENV) { 2 | process.env.NODE_ENV = 'development'; 3 | } 4 | 5 | if (!process.env.LIVERELOAD) { 6 | process.env.LIVERELOAD = 'true'; 7 | } 8 | 9 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; 10 | 11 | const express = require('express'); 12 | const app = module.exports = express(); 13 | const rp = require('request-promise'); 14 | const { URL } = require('url'); 15 | const chalk = require('chalk'); 16 | const fs = require('fs'); 17 | const path = require('path'); 18 | const compression = require('compression'); 19 | 20 | const removeCSP = require('./lib/server/remove-csp'); 21 | const inject = require('./lib/server/inject'); 22 | 23 | const styleMiddlware = require('./lib/server/style-middleware'); 24 | 25 | if (process.env.NODE_ENV !== 'development') { 26 | app.use((req, res, next) => { 27 | const match = req.hostname.match(/^(.+)\.turboext\.net$/); 28 | let pullRequest; 29 | 30 | if (match && fs.existsSync(path.join('checkout', match[1]))) { 31 | pullRequest = match[1]; 32 | } else { 33 | return res.redirect('https://master.turboext.net'); 34 | } 35 | 36 | req.ctx = req.ctx || {}; 37 | req.ctx.pullRequest = pullRequest; 38 | 39 | next(); 40 | }); 41 | } 42 | 43 | app.use(styleMiddlware); 44 | app.use(express.static('public')); 45 | app.use('/hosts', express.static('hosts')); 46 | 47 | app.use(compression()); 48 | 49 | app.use((req, res, next) => { 50 | const url = normalize(req.query.text); 51 | 52 | req.ctx = req.ctx || {}; 53 | req.ctx.url = url; 54 | req.ctx.hostname = req.query.hostname || getHostname(url); 55 | 56 | next(); 57 | }); 58 | 59 | app.get('/turbo', (req, res, next) => { 60 | const { 61 | url, 62 | hostname 63 | } = req.ctx; 64 | 65 | const params = cleanupParams(req.query); 66 | 67 | if (!hostname || req.query.disable) { 68 | return getTurbo(req, url, params).then(html => res.send(html)).catch(e => next(e)); 69 | } 70 | 71 | if (params.ajax_type) { 72 | return getTurbo(req, url, params).then(json => { 73 | res.set('Content-Type', 'application/json; charset=utf-8'); 74 | res.send(json); 75 | return res.end(); 76 | }).catch(e => next(e)); 77 | } 78 | 79 | getTurbo(req, url, params) 80 | .then(html => inject(req, html, hostname)) 81 | .then(html => res.send(html)) 82 | .catch(e => next(e)); 83 | }); 84 | 85 | app.get('/frame', (req, res, next) => { 86 | fs.readFile('public/frame.html', 'utf8', (e, html) => { 87 | if (e) { 88 | return next(e); 89 | } 90 | 91 | res.send(html.replace( 92 | '/turbo?placeholder=1', 93 | req.originalUrl.replace('/frame', '/turbo') 94 | )); 95 | }); 96 | }); 97 | 98 | app.get('/frame-morda', (req, res, next) => { 99 | fs.readFile('public/frame-morda.html', 'utf8', (e, html) => { 100 | if (e) { 101 | return next(e); 102 | } 103 | 104 | res.send(html.replace( 105 | '/turbo?placeholder=1', 106 | req.originalUrl.replace('/frame-morda', '/turbo') 107 | )); 108 | }); 109 | }); 110 | 111 | app.use((req, res) => { 112 | res.status(404); 113 | res.end('Unknown route'); 114 | }); 115 | 116 | app.use((err, req, res) => { 117 | res.status(500); 118 | res.end(err); 119 | }); 120 | 121 | const DEV_SERVER_PORT = 3000; 122 | const LIVRELOAD_PORT = 35729; 123 | const PUBLIC = process.env.PUBLIC === 'true'; 124 | 125 | const debug = ['NODE_ENV', 'LIVERELOAD', 'PUBLIC'] 126 | .filter(v => process.env[v]) 127 | .map(v => `${v}=${process.env[v]}`).join(', '); 128 | 129 | console.log(`Env variables: ${chalk.gray(debug)}`); 130 | 131 | app.listen(DEV_SERVER_PORT, () => { 132 | if (PUBLIC) { 133 | const ngrok = require('ngrok'); 134 | 135 | ngrok.connect({ 136 | addr: DEV_SERVER_PORT, 137 | region: 'eu' 138 | }).then(url => { 139 | console.log(`DevServer started at ${chalk.blue.underline(`${url}`)}`); 140 | }).catch(e => { 141 | console.error(e); 142 | process.exit(1); 143 | }); 144 | } else { 145 | let output = `DevServer started at ${chalk.blue.underline(`http://localhost:${DEV_SERVER_PORT}`)}`; 146 | if (process.env.LIVERELOAD === 'true' && process.env.NODE_ENV !== 'production') { 147 | const livereload = require('./lib/server/lr'); 148 | livereload().listen(LIVRELOAD_PORT, () => { 149 | output += chalk.gray(`, live reload started at http://localhost:${LIVRELOAD_PORT}`); 150 | console.log(output); 151 | }); 152 | } else { 153 | console.log(output); 154 | } 155 | } 156 | }); 157 | 158 | /** 159 | ** 160 | * @param url 161 | * @returns {string} 162 | */ 163 | function getHostname(url) { 164 | try { 165 | return new URL(url).origin; 166 | } catch (e) { 167 | return ''; 168 | } 169 | } 170 | 171 | function normalize(str) { 172 | try { 173 | const url = new URL(str); 174 | if (url.hostname === 'yandex.ru') { 175 | return url.searchParams.get('text') || ''; 176 | } else { 177 | return url.toString(); 178 | } 179 | } catch (e) { 180 | return str; 181 | } 182 | } 183 | 184 | function cleanupParams(queryParams) { 185 | if (!queryParams) { 186 | return {}; 187 | } 188 | 189 | const params = { ...queryParams }; 190 | 191 | delete params.text; 192 | 193 | // служебные параметры dev-server 194 | delete params.disable; 195 | delete params.hostname; 196 | 197 | return params; 198 | } 199 | 200 | function getTurbo(req, url, params) { 201 | const headers = { ...req.headers }; 202 | delete headers.host; 203 | // выключаем поддержку brotli 204 | headers['accept-encoding'] = 'gzip, deflate'; 205 | 206 | const turboHost = process.env.TURBO_HOST || 'https://yandex.ru'; 207 | 208 | if (!url) { 209 | return rp({ 210 | uri: `${turboHost}/turbo`, 211 | headers, 212 | gzip: true 213 | }).then(removeCSP); 214 | } 215 | 216 | return rp({ 217 | uri: `${turboHost}/turbo`, 218 | headers, 219 | qs: { 220 | text: url, 221 | ...params 222 | }, 223 | gzip: true 224 | }).then(removeCSP); 225 | } 226 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'stylelint-config-recommended-scss', 3 | plugins: [ 4 | 'stylelint-high-performance-animation' 5 | ], 6 | rules: { 7 | 'plugin/no-low-performance-animation-properties': true, 8 | 'unit-whitelist': ['px', '%', 'rem', 's', 'ms', 'deg', 'vw', 'vh'], 9 | 'selector-max-specificity': '0,4,0', 10 | 'selector-max-type': 0, 11 | 'selector-max-attribute': 0, 12 | 'at-rule-blacklist': ['font-face', 'import'], 13 | 'property-blacklist': [ 14 | 'perspective', 15 | 'backface-visibility', 16 | 'mask', 17 | 'mask-image', 18 | 'mask-border', 19 | 'clip-path' 20 | ], 21 | 'function-url-scheme-whitelist': '/^\.\//', 22 | 'function-url-no-scheme-relative': true, 23 | 'selector-class-pattern': '^((?!markup|page__|typo|grid).)*$' 24 | }, 25 | ignoreFiles: [ 26 | 'hosts/**/*.min.css', 27 | 'hosts/**/*.css.gz', 28 | 'hosts/**/*.yaml' 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui bdd 2 | --timeout 60000 3 | --recursive 4 | --full-trace 5 | --colors 6 | -------------------------------------------------------------------------------- /test/remove-csp.spec.js: -------------------------------------------------------------------------------- 1 | const removeCsp = require('../lib/server/remove-csp'); 2 | const { describe, it } = require('mocha'); 3 | const assert = require('chai').assert; 4 | 5 | describe('remove csp', () => { 6 | it('should remove meta tag from html string', () => { 7 | const html = ''; 8 | 9 | it('should remove old styles html string', () => { 10 | const htmlWithoutCSS = ''; 11 | assert.equal(removeCSS(html), htmlWithoutCSS); 12 | }); 13 | 14 | it('should replace old styles with new one', () => { 15 | const htmlWithNewCSS = ''; 16 | assert.equal(replaceCSS(html, ''), htmlWithNewCSS); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /webmaster-host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turboext/css/b359413a462133efdd7bef3deb61eda2acb5c3bd/webmaster-host.png --------------------------------------------------------------------------------