diff --git a/.gitignore b/.gitignore index 263337e..b026140 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /contribs/ /.phpunit.result.cache /build/ +/website/mwcli.phar diff --git a/.phpcs.xml b/.phpcs.xml index 724e46f..df0a6c6 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -8,5 +8,5 @@ . temp/ vendor/ - config.php + website/simple.min.css diff --git a/README.md b/README.md index 748ad5e..92b0899 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Add a new site to the config file. sites:add [-c|--config [CONFIG]] [--url URL] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--url` — The URL of the wiki. Can be to any page or even api.php. *Required.* @@ -69,7 +69,7 @@ Get general information about a wiki. sites:info [-c|--config [CONFIG]] [-w|--wiki WIKI] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* @@ -80,7 +80,7 @@ List all configured sites. sites:list [-c|--config [CONFIG]] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' ### sites:remove @@ -89,7 +89,7 @@ Remove a site from the config file. sites:remove [-c|--config [CONFIG]] [-w|--wiki WIKI] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* @@ -100,7 +100,7 @@ Export pages and files in a category (and its subcategories). export:category [-c|--config [CONFIG]] [-w|--wiki WIKI] [-a|--category CATEGORY] [-d|--dest DEST] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* * `--category` `-a` — Name of the category to export (with or without the leading 'Category:', and in any language). @@ -115,7 +115,7 @@ Export a user's contributions. export:contribs [-c|--config [CONFIG]] [-w|--wiki WIKI] [-u|--user USER] [-d|--dest DEST] [-o|--only-author] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* * `--user` `-u` — Export contributions of this username. @@ -131,7 +131,7 @@ Export a wiki's pages as text files. export:wikitext [-c|--config [CONFIG]] [-w|--wiki WIKI] [-d|--dest DEST] [-e|--ext EXT] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* * `--dest` `-d` — The destination directory for exported files. @@ -146,7 +146,7 @@ Install an extension into a local wiki. Requires 'install_path' to be set in a s extension:install [-c|--config [CONFIG]] [-w|--wiki WIKI] [-g|--git] [-u|--gituser GITUSER] [--] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* * `--git` `-g` — Use Git to install the extension, instead of the default tarball method. @@ -161,7 +161,7 @@ Shows a list of installed extensions that have updates available, including thei extension:outdated [-c|--config [CONFIG]] [-w|--wiki WIKI] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* @@ -172,7 +172,7 @@ Upload local files to a wiki. upload:files [-c|--config [CONFIG]] [-w|--wiki WIKI] [-m|--comment COMMENT] [--] [...] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* * `--comment` `-m` — Revision comment. @@ -186,7 +186,7 @@ Upload local text files as wiki pages. upload:pages [-c|--config [CONFIG]] [-w|--wiki WIKI] [-m|--comment [COMMENT]] [-t|--watch] [--] * `--config` `-c` — Path of the Yaml config file to use. - Default: '[CWD]/config.yml' + Default: '[CONFIG]/mwcli/config.yml' * `--wiki` `-w` — The mwcli name of the wiki to use. Use sites:list to list all. *Required.* * `--comment` `-m` — Revision comment. diff --git a/bin/mwcli b/bin/mwcli index 08ad9d5..1022896 100755 --- a/bin/mwcli +++ b/bin/mwcli @@ -18,7 +18,7 @@ use Samwilson\MediaWikiCLI\Command\UploadFilesCommand; use Samwilson\MediaWikiCLI\Command\UploadPagesCommand; use Symfony\Component\Console\Application; -$application = new Application( 'mwcli', '1.3.0' ); +$application = new Application( 'mwcli', '@git-tag@' ); $application->add(new SitesAddCommand()); $application->add(new SitesInfoCommand()); $application->add(new SitesListCommand()); diff --git a/box.json b/box.json new file mode 100644 index 0000000..9aa6b01 --- /dev/null +++ b/box.json @@ -0,0 +1,33 @@ +{ + "output": "website/mwcli.phar", + "compression": "GZ", + "git-tag": "git-tag", + "directories": [ + "src/", + "i18n/" + ], + "finder": [ + { + "in": "vendor", + "name": "*.php", + "exclude": [ + "CHANGELOG", + "CONTRIBUTING", + "README", + "Tests", + "behat", + "ext", + "bin", + "build", + "doc", + "docs", + "doc-template", + "fixtures", + "test", + "tests", + "test_old", + "vendor-bin" + ] + } + ] +} diff --git a/composer.json b/composer.json index d2d6dbb..0f07c90 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,9 @@ "minus-x check .", "./bin/mwcli readme", "phpunit" + ], + "build": [ + "box compile" ] } } diff --git a/i18n/en.json b/i18n/en.json index 91886b6..8d5aac2 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2,6 +2,7 @@ "using-config": "Using config file $1", "option-config-desc": "Path of the Yaml config file to use.", "saved-config": "Configuration file saved.", + "old-config-exists": "A config file exists at $1 that you may wish to move to $2", "command-sites-add-desc": "Add a new site to the config file.", "command-sites-list-desc": "List all configured sites.", diff --git a/src/Command/CommandBase.php b/src/Command/CommandBase.php index 3b1b212..eb2d699 100644 --- a/src/Command/CommandBase.php +++ b/src/Command/CommandBase.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Yaml\Yaml; +use XdgBaseDir\Xdg; abstract class CommandBase extends Command { @@ -29,12 +30,14 @@ abstract class CommandBase extends Command { /** @var LoggerInterface */ protected $logger; + /** @var mixed|null Runtime config of the config file's data. */ + private $config; + public function configure() { // Set up i18n. $this->intuition = new Intuition( 'mwcli' ); $this->intuition->registerDomain( 'mwcli', dirname( __DIR__, 2 ) . '/i18n' ); - - $default = $this->getConfigDirDefault() . 'config.yml'; + $default = ( new Xdg() )->getHomeConfigDir() . '/mwcli/config.yml'; $this->addOption( 'config', 'c', InputOption::VALUE_OPTIONAL, $this->msg( 'option-config-desc' ), $default ); } @@ -76,27 +79,31 @@ protected function msg( string $msg, ?array $vars = [] ): string { ] ); } - /** - * Get the default config directory (the root directory of mwcli). - * @return string The full filesystem path, always with a trailing slash. - */ - protected function getConfigDirDefault(): string { - return rtrim( dirname( __DIR__, 2 ), DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR; - } - /** * @param InputInterface $input - * @return mixed[][] + * @return mixed */ protected function getConfig( InputInterface $input ): array { + if ( $this->config ) { + return $this->config; + } $configPath = $input->getOption( 'config' ); + + // Backwards compatibility check: if there's a config.yaml file in the + // current directory, tell the user. It may not actually be an mwcli one + // so we don't try to move it ourselves. + $cwdConfig = getcwd() . '/config.yml'; + if ( file_exists( $cwdConfig ) ) { + $this->io->warning( $this->msg( 'old-config-exists', [ $cwdConfig, $configPath ] ) ); + } + if ( !file_exists( $configPath ) ) { // Create an empty config file. $this->saveConfig( $input, [] ); } $this->io->block( $this->msg( 'using-config', [ $configPath ] ) ); - $config = Yaml::parseFile( $configPath ); - return $config; + $this->config = Yaml::parseFile( $configPath ); + return $this->config; } /** @@ -106,8 +113,13 @@ protected function getConfig( InputInterface $input ): array { */ protected function saveConfig( InputInterface $input, array $config ): void { $configPath = $input->getOption( 'config' ); + if ( !file_exists( dirname( $configPath ) ) ) { + mkdir( dirname( $configPath ), 0700, true ); + } file_put_contents( $configPath, Yaml::dump( $config, 3 ) ); $this->io->success( $this->msg( 'saved-config', [ $configPath ] ) ); + // Set the runtime cache to null so it will be refreshed next time the config is accessed. + $this->config = null; } protected function getApi( array $siteInfo, ?AuthMethod $authMethod = null ): ActionApi { diff --git a/src/Command/ExportCategoryCommand.php b/src/Command/ExportCategoryCommand.php index 33031ff..dbf11fc 100644 --- a/src/Command/ExportCategoryCommand.php +++ b/src/Command/ExportCategoryCommand.php @@ -31,7 +31,7 @@ public function configure() { $this->addOption( 'wiki', 'w', InputOption::VALUE_REQUIRED, $this->msg( 'option-wiki-desc' ) ); $this->addOption( 'category', 'a', InputOption::VALUE_REQUIRED, $this->msg( 'option-category-desc' ) ); $this->addOption( 'dest', 'd', InputOption::VALUE_REQUIRED, $this->msg( 'option-dest-desc' ), - $this->getConfigDirDefault() . 'categories' ); + getcwd() . '/categories' ); } public function execute( InputInterface $input, OutputInterface $output ) { diff --git a/src/Command/ExportContribsCommand.php b/src/Command/ExportContribsCommand.php index b791d24..0fcf3be 100644 --- a/src/Command/ExportContribsCommand.php +++ b/src/Command/ExportContribsCommand.php @@ -20,7 +20,7 @@ public function configure() { $this->addOption( 'wiki', 'w', InputOption::VALUE_REQUIRED, $this->msg( 'option-wiki-desc' ) ); $this->addOption( 'user', 'u', InputOption::VALUE_REQUIRED, $this->msg( 'option-user-desc' ) ); $this->addOption( 'dest', 'd', InputOption::VALUE_REQUIRED, $this->msg( 'option-dest-desc' ), - $this->getConfigDirDefault() . 'contribs' ); + getcwd() . '/contribs' ); $this->addOption( 'only-author', 'o', InputOption::VALUE_NONE, $this->msg( 'option-only-author-desc' ) ); } diff --git a/src/Command/ExportWikitextCommand.php b/src/Command/ExportWikitextCommand.php index b41f7f4..155614a 100644 --- a/src/Command/ExportWikitextCommand.php +++ b/src/Command/ExportWikitextCommand.php @@ -27,7 +27,7 @@ public function configure() { $this->setDescription( $this->msg( 'command-export-wikitext-desc' ) ); $this->addOption( 'wiki', 'w', InputOption::VALUE_REQUIRED, $this->msg( 'option-wiki-desc' ) ); $this->addOption( 'dest', 'd', InputOption::VALUE_REQUIRED, $this->msg( 'option-dest-desc' ), - $this->getConfigDirDefault() . 'wikitext' ); + getcwd() . '/wikitext' ); $this->addOption( 'ext', 'e', InputOption::VALUE_REQUIRED, $this->msg( 'option-ext-desc' ), 'txt' ); } diff --git a/src/Command/ReadmeGenCommand.php b/src/Command/ReadmeGenCommand.php index 7a3d903..0dfc427 100644 --- a/src/Command/ReadmeGenCommand.php +++ b/src/Command/ReadmeGenCommand.php @@ -4,6 +4,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use XdgBaseDir\Xdg; class ReadmeGenCommand extends CommandBase { @@ -38,7 +39,24 @@ public function execute( InputInterface $input, OutputInterface $output ) { } // Remove local paths. - $commandInfo = str_replace( getcwd(), '[CWD]', $commandInfo ); + $xdg = new Xdg(); + $commandInfo = str_replace( + [ + getcwd(), + $xdg->getHomeCacheDir(), + $xdg->getHomeConfigDir(), + $xdg->getHomeDataDir(), + $xdg->getHomeDir(), + ], + [ + '[CWD]', + '[CACHE]', + '[CONFIG]', + '[DATA]', + '[HOME]', + ], + $commandInfo + ); // Write new contents to README.md. $readmePath = dirname( __DIR__, 2 ) . '/README.md'; diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..2038b8d --- /dev/null +++ b/website/index.html @@ -0,0 +1,36 @@ + + + + + + + mwcli: a MediaWiki CLI tool + + +
+

MediaWiki CLI

+

A command line client for MediaWiki wikis.

+
+ +
+

+ MediaWiki CLI (or mwcli for short) is a multilingual cross-platform PHP command-line tool + for interacting with MediaWiki installations, + to carry out tasks such as uploading and downloading files, and exporting pages. +

+ +

+ Download mwcli.phar +

+ +

Other useful links:

+ + + +
+ + diff --git a/website/simple.min.css b/website/simple.min.css new file mode 100644 index 0000000..e5cddde --- /dev/null +++ b/website/simple.min.css @@ -0,0 +1,2 @@ +/* https://github.com/kevquirk/simple.css/blob/be882ce561b0fe5ced621cc64595c812790b2524/simple.min.css */ +:root{--sans-font:-apple-system,BlinkMacSystemFont,"Avenir Next",Avenir,"Nimbus Sans L",Roboto,"Noto Sans","Segoe UI",Arial,Helvetica,"Helvetica Neue",sans-serif;--mono-font:Consolas,Menlo,Monaco,"Andale Mono","Ubuntu Mono",monospace;--standard-border-radius:5px;--bg:#fff;--accent-bg:#f5f7ff;--text:#212121;--text-light:#585858;--border:#898ea4;--accent:#0d47a1;--accent-hover:#1266e2;--accent-text:var(--bg);--code:#d81b60;--preformatted:#444;--marked:#fd3;--disabled:#efefef}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--bg:#212121;--accent-bg:#2b2b2b;--text:#dcdcdc;--text-light:#ababab;--accent:#ffb300;--accent-hover:#ffe099;--accent-text:var(--bg);--code:#f06292;--preformatted:#ccc;--disabled:#111}img,video{opacity:.8}}*,:before,:after{box-sizing:border-box}textarea,select,input,progress{-webkit-appearance:none;-moz-appearance:none;appearance:none}html{font-family:var(--sans-font);scroll-behavior:smooth}body{color:var(--text);background-color:var(--bg);grid-template-columns:1fr min(45rem,90%) 1fr;margin:0;font-size:1.15rem;line-height:1.5;display:grid}body>*{grid-column:2}body>header{background-color:var(--accent-bg);border-bottom:1px solid var(--border);text-align:center;grid-column:1/-1;padding:0 .5rem 2rem}body>header>:only-child{margin-block-start:2rem}body>header h1{max-width:1200px;margin:1rem auto}body>header p{max-width:40rem;margin:1rem auto}main{padding-top:1.5rem}body>footer{color:var(--text-light);text-align:center;border-top:1px solid var(--border);margin-top:4rem;padding:2rem 1rem 1.5rem;font-size:.9rem}h1{font-size:3rem}h2{margin-top:3rem;font-size:2.6rem}h3{margin-top:3rem;font-size:2rem}h4{font-size:1.44rem}h5{font-size:1.15rem}h6{font-size:.96rem}p{margin:1.5rem 0}p,h1,h2,h3,h4,h5,h6{overflow-wrap:break-word}h1,h2,h3{line-height:1.1}@media only screen and (width<=720px){h1{font-size:2.5rem}h2{font-size:2.1rem}h3{font-size:1.75rem}h4{font-size:1.25rem}}a,a:visited{color:var(--accent)}a:hover{text-decoration:none}button,.button,a.button,input[type=submit],input[type=reset],input[type=button]{border:1px solid var(--accent);background-color:var(--accent);color:var(--accent-text);padding:.5rem .9rem;line-height:normal;text-decoration:none}.button[aria-disabled=true],input:disabled,textarea:disabled,select:disabled,button[disabled]{cursor:not-allowed;background-color:var(--disabled);border-color:var(--disabled);color:var(--text-light)}input[type=range]{padding:0}abbr[title]{cursor:help;text-decoration-line:underline;text-decoration-style:dotted}button:enabled:hover,.button:not([aria-disabled=true]):hover,input[type=submit]:enabled:hover,input[type=reset]:enabled:hover,input[type=button]:enabled:hover{background-color:var(--accent-hover);border-color:var(--accent-hover);cursor:pointer}.button:focus-visible,button:focus-visible:where(:enabled),input:enabled:focus-visible:where([type=submit],[type=reset],[type=button]){outline:2px solid var(--accent);outline-offset:1px}header>nav{padding:1rem 0 0;font-size:1rem;line-height:2}header>nav ul,header>nav ol{flex-flow:wrap;place-content:space-around center;align-items:center;margin:0;padding:0;list-style-type:none;display:flex}header>nav ul li,header>nav ol li{display:inline-block}header>nav a,header>nav a:visited{border:1px solid var(--border);border-radius:var(--standard-border-radius);color:var(--text);margin:0 .5rem 1rem;padding:.1rem 1rem;text-decoration:none;display:inline-block}header>nav a:hover,header>nav a.current,header>nav a[aria-current=page],header>nav a[aria-current=true]{border-color:var(--accent);color:var(--accent);cursor:pointer}@media only screen and (width<=720px){header>nav a{border:none;padding:0;line-height:1;text-decoration:underline}}aside,details,pre,progress{background-color:var(--accent-bg);border:1px solid var(--border);border-radius:var(--standard-border-radius);margin-bottom:1rem}aside{float:right;width:30%;margin-inline-start:15px;padding:0 15px;font-size:1rem}[dir=rtl] aside{float:left}@media only screen and (width<=720px){aside{float:none;width:100%;margin-inline-start:0}}article,fieldset,dialog{border:1px solid var(--border);border-radius:var(--standard-border-radius);margin-bottom:1rem;padding:1rem}article h2:first-child,section h2:first-child,article h3:first-child,section h3:first-child{margin-top:1rem}section{border-top:1px solid var(--border);border-bottom:1px solid var(--border);margin:3rem 0;padding:2rem 1rem}section+section,section:first-child{border-top:0;padding-top:0}section+section{margin-top:0}section:last-child{border-bottom:0;padding-bottom:0}details{padding:.7rem 1rem}summary{cursor:pointer;word-break:break-all;margin:-.7rem -1rem;padding:.7rem 1rem;font-weight:700}details[open]>summary+*{margin-top:0}details[open]>summary{margin-bottom:.5rem}details[open]>:last-child{margin-bottom:0}table{border-collapse:collapse;margin:1.5rem 0}figure>table{width:max-content;margin:0}td,th{border:1px solid var(--border);text-align:start;padding:.5rem}th{background-color:var(--accent-bg);font-weight:700}tr:nth-child(2n){background-color:var(--accent-bg)}table caption{margin-bottom:.5rem;font-weight:700}textarea,select,input,button,.button{font-size:inherit;border-radius:var(--standard-border-radius);box-shadow:none;max-width:100%;margin-bottom:.5rem;padding:.5rem;font-family:inherit;display:inline-block}textarea,select,input{color:var(--text);background-color:var(--bg);border:1px solid var(--border)}label{display:block}textarea:not([cols]){width:100%}select:not([multiple]){background-image:linear-gradient(45deg,transparent 49%,var(--text)51%),linear-gradient(135deg,var(--text)51%,transparent 49%);background-position:calc(100% - 15px),calc(100% - 10px);background-repeat:no-repeat;background-size:5px 5px,5px 5px;padding-inline-end:25px}[dir=rtl] select:not([multiple]){background-position:10px,15px}input[type=checkbox],input[type=radio]{vertical-align:middle;width:min-content;position:relative}input[type=checkbox]+label,input[type=radio]+label{display:inline-block}input[type=radio]{border-radius:100%}input[type=checkbox]:checked,input[type=radio]:checked{background-color:var(--accent)}input[type=checkbox]:checked:after{content:" ";border-right:solid var(--bg).08em;border-bottom:solid var(--bg).08em;background-color:#0000;border-radius:0;width:.18em;height:.32em;font-size:1.8em;position:absolute;top:.05em;left:.17em;transform:rotate(45deg)}input[type=radio]:checked:after{content:" ";background-color:var(--bg);border-radius:100%;width:.25em;height:.25em;font-size:32px;position:absolute;top:.125em;left:.125em}@media only screen and (width<=720px){textarea,select,input{width:100%}}input[type=color]{height:2.5rem;padding:.2rem}input[type=file]{border:0}hr{background:var(--border);border:none;height:1px;margin:1rem auto}mark{border-radius:var(--standard-border-radius);background-color:var(--marked);color:#000;padding:2px 5px}mark a{color:#0d47a1}img,video{border-radius:var(--standard-border-radius);max-width:100%;height:auto}figure{margin:0;display:block;overflow-x:auto}figure>img,figure>picture>img{margin-inline:auto;display:block}figcaption{text-align:center;color:var(--text-light);margin-block:1rem;font-size:.9rem}blockquote{border-inline-start:.35rem solid var(--accent);color:var(--text-light);margin-block:2rem;margin-inline:2rem 0;padding:.4rem .8rem;font-style:italic}cite{color:var(--text-light);font-size:.9rem;font-style:normal}dt{color:var(--text-light)}code,pre,pre span,kbd,samp{font-family:var(--mono-font);color:var(--code)}kbd{color:var(--preformatted);border:1px solid var(--preformatted);border-bottom:3px solid var(--preformatted);border-radius:var(--standard-border-radius);padding:.1rem .4rem}pre{max-width:100%;color:var(--preformatted);padding:1rem 1.4rem;overflow:auto}pre code{color:var(--preformatted);background:0 0;margin:0;padding:0}progress{width:100%}progress:indeterminate{background-color:var(--accent-bg)}progress::-webkit-progress-bar{border-radius:var(--standard-border-radius);background-color:var(--accent-bg)}progress::-webkit-progress-value{border-radius:var(--standard-border-radius);background-color:var(--accent)}progress::-moz-progress-bar{border-radius:var(--standard-border-radius);background-color:var(--accent);transition-property:width;transition-duration:.3s}progress:indeterminate::-moz-progress-bar{background-color:var(--accent-bg)}dialog{background-color:var(--bg);max-width:40rem;margin:auto}dialog::backdrop{background-color:var(--bg);opacity:.8}@media only screen and (width<=720px){dialog{max-width:100%;margin:auto 1em}}sup,sub{vertical-align:baseline;position:relative}sup{top:-.4em}sub{top:.3em}.notice{background:var(--accent-bg);border:2px solid var(--border);border-radius:var(--standard-border-radius);margin:2rem 0;padding:1.5rem}@media print{@page{margin:1cm}body{display:block}body>header{background-color:unset}body>header nav,body>footer{display:none}article{border:none;padding:0}a[href^=http]:after{content:" <" attr(href)">"}abbr[title]:after{content:" (" attr(title)")"}a{text-decoration:none}p{widows:3;orphans:3}hr{border-top:1px solid var(--border)}mark{border:1px solid var(--border)}pre,table,figure,img,svg{break-inside:avoid}pre code{white-space:pre-wrap}} \ No newline at end of file