您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Semantic markup for file lists in pause.perl.org
- // ==UserScript==
- // @name PAUSE
- // @namespace https://sourceforge.net/u/van-de-bugger/
- // @description Semantic markup for file lists in pause.perl.org
- // @include https://pause.perl.org/pause/authenquery
- // @include https://pause.perl.org/pause/authenquery?ACTION=show_files
- // @include https://pause.perl.org/pause/authenquery?ACTION=delete_files
- // @include https://pause.perl.org/pause/authenquery?ACTION=reindex
- // @version 0.1.2
- // @grant GM_addStyle
- // ==/UserScript==
- /*
- ------------------------------------------------------------------------------------------------
- Copyright (C) 2016 Van de Bugger
- License GPLv3+: The GNU General Public License version 3 or later
- <http://www.gnu.org/licenses/gpl-3.0.txt>.
- This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the
- extent permitted by law.
- ------------------------------------------------------------------------------------------------
- */
- /**
- This is a Greasemonkey script for [PAUSE](https://pause.perl.org/).
- ## Problem
- File lists displayed in "Show my files" and "Delete files" are err… too spartan.
- If you have a dozen of distributions and few releases (let me say, 3) for every distribution, your
- file list will include more than hundred files. The list is badly aligned (if you have file names
- longer than ~50 characters). It would be nice to group files by releases and distributions, use
- color highlighting for denoting trial releases and files scheduled for deletion. But spartan list
- markup does not allow applying user style sheet because entire list is just a single `PRE` element
- without a structure.
- The list on "Delete files" page has 3-color highlighting, but it does not help because coloring is
- not semantic: first line highlighted with red, the second with green, the third with blue
- regardless of files shown in these lines. Usually release consist of 3 files (.meta, .readme and
- .tar.gz), so at the top of the list .meta files are red, .readme are green, and tarballs are blue,
- but trial releases (which includes only tarball with no .meta and no .readme) and special file
- `CHECKSUMS` break the system.
- Deleting a release is not trivial because you have to mark 3 files to delete. Color highlighting
- dazzles and it is so easy to mark a file from adjacent release. (Thanks they are not deleted
- immediatelly.)
- ## Solution
- This script parses original file list and re-creates it with semantic markup. Every file has name,
- size and date. Files are grouped into releases, releases are grouped into distributions.
- Semantic markup allows using user style sheet to highlight files, releases, or distributions, group
- releases and/or distributions visually, add headings, etc.
- Also, the script makes two changes in list behavior:
- 1. The script adds two buttons: "Show tarballs only" and "Show all files". Pressing the first
- button hides non-tarball files making the list 3 times shorter. Pressing the second button shows
- hidden files back. The buttons are added to both "Show my files" and "Delete files" pages.
- 2. The script automates selecting releases to delete. When you change state of tarball file
- checkbox, the change is automatically propagated to all files of the release containing this
- tarball. So, the most frequent operation (selecting a release to delete) now requires just one
- click instead of 3 clicks. It also eliminates risk to mistakenly select 2 files from one release
- and a file from adjacent release. (Selecting individual files is still possible, though.)
- Both behavioral changes work together nicely: You can hide non-tarball files to have more
- compact list of tarballs, select a tarball from the list — accompanying (and currently
- invisible) meta and readme files will be selected automatically.
- *Note:* The script does *not* provide any style sheet. I made it intentionally because my color
- preferences may not be suitable for everybody. You may try [my style
- sheet](https://userstyles.org/styles/135527/pause).
- ## Details
- <pre id="fileList" class="fileList">
- <span id="Foo" class="distribution">
- <span id="Foo-v0.1.2" class="release">
- <span id="Foo.meta" class="file meta">
- <!-- checkbox in case of "Delete files" page -->
- <span class="name">Foo.meta</span>
- <span class="namePad"> </span>
- <span class="sizePad"> </span>
- <span class="size">2 300</span>
- <span class="date">2016-11-11T12:00:05Z</span>
- <span class="eof"><!-- newline character --></span>
- </span>
- <!-- More files of this release -->
- </span>
- <!-- More releases of this distribution -->
- </span>
- <!-- More distributions -->
- <span id="special" class="special">
- <span id="CHECKSUMS" class="file">
- <span class="name">CHECKSUMS</span>
- <span class="namePad"> </span>
- <span class="sizePad"></span>
- <span class="size">34 895</span>
- <span class="date">2016-11-11T12:00:15Z</span>
- <span class="eof"><!-- newline character --></span>
- </span>
- </span>
- </pre>
- The sample is splitted into multiple lines and indented for the sake of readability, actual `PRE`
- element has only `SPAN`s children. Text nodes appears only in the bottom level `SPAN`s (name, size,
- date, etc).
- The list generated for "Delete files" page also has `INPUT` elements (checkboxes) taken from the
- original list.
- Trial releases have additional class `trial`. Files sheduled for deletion — class `delete`,
- files with ".tar.gz" suffix — `tarball`, files with ".readme" suffix — `readme`, files with
- ".meta" suffix — `meta` (shown in example above).
- Dates are converted to ISO fomat (with dropped second fraction part). Digits in file sizes are
- groped by inserting blanks.
- It is assumed the element is displayed using monospaced font.
- ## History Log
- * v0.1.0 — Initial release, "Show my files" and "Delete files" are processed.
- * v0.1.1 — Page detection implemented to avoid unintentional processing other pages. "Force
- reindexing" page is processed too.
- * v0.1.2 — Buttons functionality does not require external style sheet, buttons work out-of-the-box
- now.
- **/
- function assert( condition, message ) {
- if ( ! condition ) {
- console.log( "throwing: " + message );
- throw message;
- };
- };
- function getCookie( name ) {
- var prefix = name + "=";
- var item;
- if ( document.cookie.length > 0 ) {
- var list = document.cookie.split( ";" );
- item = list.find( function ( item ) {
- return item.startsWith( prefix );
- } );
- };
- if ( item == null ) {
- return null;
- };
- return item.substr( prefix.length );
- };
- function setCookie( name, value ) {
- var prefix = name + "=";
- var list;
- if ( document.cookie.length > 0 ) {
- list = document.cookie.split( ";" );
- list = list.filter( function ( item ) {
- return ! item.startsWith( prefix );
- } );
- } else {
- list = [];
- };
- list.push( prefix + value );
- document.cookie = list.join( ";" );
- };
- /**
- Calls func for each element of the list. list can be either array or NodeList object (which is
- array-like but does not have forEach method). The func is called with 3 arguments: element,
- index, and list.
- **/
- function forEach( list, func ) {
- if ( list != null ) {
- for ( var i = 0; i < list.length; ++ i ) {
- func( list[ i ], i, list );
- };
- };
- };
- /**
- Adds class addClassName to all elements with className.
- **/
- function addClass( className, addClassName ) {
- forEach( document.getElementsByClassName( className ), function ( elem ) {
- elem.classList.add( addClassName );
- } );
- };
- /**
- Removes class remClassName from all elements with className.
- **/
- function remClass( className, remClassName ) {
- forEach( document.getElementsByClassName( className ), function ( elem ) {
- elem.classList.remove( remClassName );
- } );
- };
- /**
- Returns true iff the version is trial one.
- **/
- function isTrial( version ) {
- return version.includes( "_" );
- };
- /**
- Splits filename into 3 parts: distribution name, version number, and file type (aka suffix).
- Returns an object with 3 fields: dist, ver, and type. ver does not contain leading dash,
- suffix does not contain leading dot.
- **/
- function parseFilename( name ) {
- var type, ver;
- // Find file type and drop it from name:
- name = name.replace( /(\.meta|\.readme|\.tar.gz)$/, function ( found ) {
- type = found.replace( /^\./, "" );
- return "";
- } );
- // Find version and drop it from name:
- name = name.replace( /-[^-]*$/, function ( found ) {
- ver = found.replace( /^-/, "" );
- return "";
- } );
- return {
- dist : name,
- ver : ver,
- type : type,
- };
- };
- /**
- Groups digits of the specified number (by inserting blanks into the number). Number must be a
- string made of digits only (no sign nor fractional part is allowed).
- **/
- function groupDigits( number ) {
- if ( number != null && number.match( /^\d+$/ ) ) {
- var groups = [];
- var rem = number.length % 3;
- var pos = 0;
- if ( rem > 0 ) {
- groups.push( number.substr( pos, rem ) );
- pos += rem;
- };
- while ( pos < number.length ) {
- groups.push( number.substr( pos, 3 ) );
- pos += 3;
- };
- return groups.join( " " );
- };
- return number;
- };
- /**
- Format of dates used by PAUSE.
- **/
- var dateRe = /[A-Z][a-z][a-z], \d\d [A-Z][a-z][a-z] \d\d\d\d \d\d:\d\d:\d\d GMT/;
- /**
- Searches string for a first date, and converts found date to the ISO format.
- **/
- function isoDate( string ) {
- return string == null ? null : string.replace( dateRe, function ( found ) {
- var date = new Date( found );
- return date.toISOString().replace( /\.000Z$/, "Z" );
- } );
- };
- /**
- Creates a DOM element.
- **/
- function createElement( tag, attrs, content ) {
- var elem = document.createElement( tag );
- for ( var attr in attrs ) {
- elem.setAttribute( attr, attrs[ attr ] );
- };
- if ( typeof content == "string" ) {
- elem.appendChild( document.createTextNode( content ) );
- } else {
- forEach( content, function ( item ) {
- if ( item != null ) {
- elem.appendChild( item );
- };
- } );
- };
- return elem;
- };
- /**
- Creates SPAN elelment representing a file.
- **/
- function createFile( file, nameWidth, sizeWidth ) {
- var classList = [ "file" ];
- if ( file.name.endsWith( ".meta" ) ) {
- classList.push( "meta" );
- };
- if ( file.name.endsWith( ".readme" ) ) {
- classList.push( "readme" );
- };
- if ( file.name.endsWith( ".tar.gz" ) ) {
- classList.push( "tarball" );
- };
- if ( file.date != null && file.date.includes( "Scheduled for deletion" ) ) {
- classList.push( "delete" );
- };
- var size = file.size == null ? null : groupDigits( file.size );
- var date = file.date == null ? null : isoDate( file.date );
- var file = createElement( "span", {
- class : classList.join( " " ),
- }, [
- ( "input" in file ? file.input : null ),
- createElement( "span", { class: "name" }, file.name ),
- size == null && date == null ? (
- null
- ) : (
- createElement( "span", { class: "namePad" }, " ".repeat( nameWidth - file.name.length ) )
- ),
- size == null ? (
- null
- ) : (
- createElement( "span", { class: "sizePad" }, " ".repeat( sizeWidth - size.length ) )
- ),
- size == null ? null : createElement( "span", { class: "size" }, size ),
- date == null ? null : createElement( "span", { class: "date" }, date ),
- createElement( "span", { class: "eof" }, "\n" ),
- ] );
- return file;
- };
- /**
- Splits given string into file name, size, and date.
- **/
- function parseLine( line ) {
- var items = line.trim().split( /\s\s+/ );
- return {
- name : items[ 0 ],
- size : items[ 1 ],
- date : items[ 2 ],
- };
- };
- /**
- Creates a SPAN element with two buttons: "Show tarballs only" and "Show all files". The first
- button hides all non-tarball files (by adding class "hidden"), the second button shows that
- files back (by removing class "hidden").
- **/
- function createButtons() {
- var hide = createElement( "input", {
- id : "showTarballsOnly",
- type : "button",
- value : "Show tarballs only",
- }, [] );
- var show = createElement( "input", {
- id : "showAllFiles",
- type : "button",
- value : "Show all files",
- disabled : "disabled",
- }, [] );
- hide.onclick = function () {
- addClass( "special", "hidden" );
- addClass( "meta", "hidden" );
- addClass( "readme", "hidden" );
- show.disabled = "";
- hide.disabled = "disabled";
- setCookie( "showMode", this.id );
- };
- show.onclick = function () {
- remClass( "special", "hidden" );
- remClass( "meta", "hidden" );
- remClass( "readme", "hidden" );
- show.disabled = "disabled";
- hide.disabled = "";
- setCookie( "showMode", this.id );
- };
- var showMode = getCookie( "showMode" );
- if ( showMode == hide.id ) {
- hide.onclick();
- };
- return createElement( "span", { id : "showMode" }, [
- hide, show
- ] );
- };
- /**
- Parses a file list taken from "Show my files" page. Returns array of objects. Each object
- represents one file and has fileds: name, size, and date.
- File list on "Show my files" page looks like:
- <pre> name size date<br> name size date<br>...</pre>
- **/
- function parseShowFiles( element ) {
- var files = [];
- forEach( element.childNodes, function ( child ) {
- if ( child.nodeType == 3 ) {
- files.push( parseLine( child.textContent ) );
- } else if ( child.nodeType == 1 && child.tagName == "BR" ) {
- // Ignore BR elements.
- } else {
- assert( 0, "unexpected node" );
- };
- } );
- return files;
- };
- /**
- Parses a file list taken from "Delete files" page. Returns array of objects. Each object
- represents one file and has fileds: name, size, date, and input. Input is an DOM INPUT element
- *removed* from the tree.
- File list on "Delete files" page looks like:
- <pre>
- <span><input> name size date</span>
- <span><input> name size date</span>
- ...
- </pre>
- **/
- function parseDeleteFiles( element ) {
- var files = [];
- forEach( element.childNodes, function ( child ) {
- if ( child.nodeType == 1 && child.tagName == "SPAN" ) {
- var file = parseLine( child.childNodes[ 1 ].textContent );
- file.input = child.removeChild( child.childNodes[ 0 ] );
- files.push( file );
- } else if ( child.nodeType == 3 && child.textContent.trim() == "" ) {
- // Ignore whitespace-only text nodes.
- } else {
- assert( 0, "unexpected node" );
- };
- } );
- return files;
- };
- /**
- Group plain list of files into hierarchy: Files are groped into releases, releases are grouped
- into distributions. Returns an object:
- {
- distros : {
- distro_name : {
- release_version : {
- file_type : file,
- another_type : athoter_file,
- ...,
- },
- another_release_version : {
- ...
- },
- ...,
- },
- another_distro_name : {
- ...
- },
- ...,
- },
- special : [
- special_file, another_special_file,
- ],
- }
- **/
- function groupFiles( files ) {
- var registry = {
- distros : {},
- special : [],
- };
- files.forEach( function ( file ) {
- var name = parseFilename( file.name );
- if ( file.name == "CHECKSUMS" ) {
- registry.special.push( file );
- } else {
- if ( ! ( name.dist in registry.distros ) ) {
- registry.distros[ name.dist ] = {};
- };
- if ( ! ( name.ver in registry.distros[ name.dist ] ) ) {
- registry.distros[ name.dist ][ name.ver ] = {};
- };
- registry.distros[ name.dist ][ name.ver ][ name.type ] = file;
- };
- } );
- return registry;
- };
- /**
- Generates a DOM elelent, representing the file list with semantic markup.
- **/
- function generateList( files ) {
- var distributions = [];
- var nameWidth = files.reduce( function ( width, file ) {
- return Math.max( width, file.name.length );
- }, 0 );
- var sizeWidth = files.reduce( function ( width, file ) {
- return Math.max( width, file.size == null ? 0 : file.size.length );
- }, 0 );
- sizeWidth = groupDigits( "9".repeat( sizeWidth ) ).length;
- var registry = groupFiles( files );
- for ( var dist in registry.distros ) {
- var releases = [];
- for ( var release in registry.distros[ dist ] ) {
- var files = [];
- for ( var type in registry.distros[ dist ][ release ] ) {
- var file = registry.distros[ dist ][ release ][ type ];
- files.push( createFile( file, nameWidth, sizeWidth ) );
- };
- releases.push( createElement(
- "span",
- {
- id : dist + "-" + release,
- class : "release" + ( isTrial( release ) ? " trial" : "" ),
- },
- files
- ) );
- };
- distributions.push( createElement( "span",
- {
- id : dist,
- class : "distribution",
- },
- releases
- ) );
- };
- if ( registry.special.length > 0 ) {
- var special = [];
- registry.special.forEach( function ( file ) {
- special.push( createFile( file, nameWidth, sizeWidth ) );
- } );
- distributions.push( createElement( "span",
- {
- id : "special",
- class : "special",
- },
- special
- ) );
- };
- return createElement( "pre", { id: "fileList", class : "fileList" }, distributions );
- };
- /**
- Automates checkboxes. When a tarball checkbox changed, synchronize other checkboxes of the same
- release with the tarball checkbox.
- **/
- function automateCheckboxes( element ) {
- forEach( element.getElementsByTagName( "input" ), function ( checkbox ) {
- if ( checkbox.value.endsWith( ".tar.gz" ) ) {
- checkbox.onchange = function () {
- var state = this.checked; // New state of the current checkbox.
- var files = this.parentElement.parentElement.childNodes;
- // List of files of this release.
- for ( var i = 0; i < files.length; ++ i ) {
- files[ i ].getElementsByTagName( "input" )[ 0 ].checked = state;
- };
- };
- };
- } );
- };
- var pre = document.getElementsByTagName( "pre" )[ 0 ];
- var files;
- /*
- Detect page by element with "firstheader" class. Page address cannot be used for that, because
- page with address <https://pause.perl.org/pause/authenquery?ACTION=delete_files> after pressing
- the button "Delete" becomes the page with address <https://pause.perl.org/pause/authenquery/>.
- */
- var firstHeader = document.getElementsByClassName( "firstheader" )[ 0 ];
- assert( firstHeader != null, "Can't find element of 'firstheader' class" );
- var title = firstHeader.textContent;
- // Parsing depends on the page.
- if ( title.toLowerCase() == "show my files" ) {
- files = parseShowFiles( pre );
- } else if ( title.toLowerCase() == "delete files" ) {
- files = parseDeleteFiles( pre );
- } else if ( title.toLowerCase() == "force reindexing" ) {
- files = parseDeleteFiles( pre );
- };
- assert( files != null, "Do not know how to parse '" + title + "' page" );
- // Recreate file list.
- var list = generateList( files );
- pre.parentElement.replaceChild( list, pre );
- var buttons = createButtons();
- list.parentElement.insertBefore( buttons, list );
- automateCheckboxes( list );
- GM_addStyle( ".fileList .hidden { display: none; }" );
- // end of file //