diff --git a/rollup.config.js b/rollup.config.js index 438d3cc8..bcffa87b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -114,6 +114,17 @@ function getPlugins() { ].filter( ( v ) => !!v ); } +/** + * Gets global watch options + * + * @return watch options + */ +function getWatch() { + return { + include: [ 'src/**', 'i18n/**', 'package.json', 'rollup.config.js' ] + }; +} + /** * Automatically disable a given component based on environment variables. * @@ -153,7 +164,8 @@ export default [ '\n// ', footer: '// \n// <3' }, - plugins: getPlugins() + plugins: getPlugins(), + watch: getWatch() } ), // Standalone Attribution Notice Template Editor auto( 'ante', { @@ -166,7 +178,8 @@ export default [ '\n// ', footer: '// \n// <3' }, - plugins: getPlugins() + plugins: getPlugins(), + watch: getWatch() } ), // Standalone Infringement Assistant auto( 'ia', { @@ -179,6 +192,7 @@ export default [ '\n// ', footer: '// \n// <3' }, - plugins: getPlugins() + plugins: getPlugins(), + watch: getWatch() } ) ].filter( ( v ) => !!v ); diff --git a/src/session/DeputyRootSession.ts b/src/session/DeputyRootSession.ts index d00fe608..f4b9dc7e 100644 --- a/src/session/DeputyRootSession.ts +++ b/src/session/DeputyRootSession.ts @@ -65,7 +65,7 @@ export default class DeputyRootSession { await mw.loader.using( [ 'oojs-ui-core', 'oojs-ui.styles.icons-content' ], () => { - const firstHeading = casePage.findContributionSurveyHeadings()[ 0 ]; + const firstHeading = casePage.findFirstContributionSurveyHeading(); if ( firstHeading ) { const stopButton = new OO.ui.ButtonWidget( { label: mw.msg( 'deputy.session.otherActive.button' ), @@ -117,7 +117,7 @@ export default class DeputyRootSession { /** * Shows the interface for continuing a previous session. This includes * the `[continue CCI session]` notice at the top of each CCI page section heading - * and a single message box showing when the page was last worked on on top of the + * and a single message box showing when the page was last worked on top of the * first CCI heading found. * * @param casePage The case page to continue with @@ -128,7 +128,7 @@ export default class DeputyRootSession { mw.loader.using( [ 'oojs-ui-core', 'oojs-ui.styles.icons-content' ], () => { - const firstHeading = casePage.findContributionSurveyHeadings()[ 0 ]; + const firstHeading = casePage.findFirstContributionSurveyHeading(); if ( firstHeading ) { // Insert element directly into widget (not as text, or else event // handlers will be destroyed). @@ -199,7 +199,7 @@ export default class DeputyRootSession { return mw.loader.using( [ 'oojs-ui-core', 'oojs-ui.styles.icons-content' ], () => { - const firstHeading = casePage.findContributionSurveyHeadings()[ 0 ]; + const firstHeading = casePage.findFirstContributionSurveyHeading(); if ( firstHeading ) { const messageBox = DeputyMessageWidget( { classes: [ @@ -241,7 +241,6 @@ export default class DeputyRootSession { const sectionNames = ( Array.isArray( section ) ? section : [ section ] ).map( ( _section ) => sectionHeadingName( _section ) ); - // Save session to storage const casePage = _casePage ?? await DeputyCasePage.build(); const session = await this.setSession( { diff --git a/src/ui/root/DeputyContributionSurveySection.tsx b/src/ui/root/DeputyContributionSurveySection.tsx index 2e1092ee..2b994844 100644 --- a/src/ui/root/DeputyContributionSurveySection.tsx +++ b/src/ui/root/DeputyContributionSurveySection.tsx @@ -19,6 +19,7 @@ import { } from '../../models/ContributionSurveyRowSigningBehavior'; import { generateTrace } from '../../models/DeputyTrace'; import DeputyMessageWidget from '../shared/DeputyMessageWidget'; +import sectionHeadingN from '../../wiki/util/sectionHeadingN'; /** * The contribution survey section UI element. This includes a list of revisions @@ -250,7 +251,10 @@ export default class DeputyContributionSurveySection implements DeputyUIElement ( v: HTMLElement ) => v.querySelector( '.mw-collapsible' ) )?.querySelector( '.mw-collapsible' ) ?? null; - const sectionWikitext = await this.casePage.wikitext.getSectionWikitext( this.headingName ); + const sectionWikitext = await this.casePage.wikitext.getSectionWikitext( + this.headingName, + sectionHeadingN( this.heading, this.headingName ) + ); return this._section ?? ( this._section = new ContributionSurveySection( this.casePage, @@ -258,7 +262,7 @@ export default class DeputyContributionSurveySection implements DeputyUIElement collapsible != null, collapsible?.querySelector( 'th > div' ).innerText, wikitext ?? sectionWikitext, - sectionWikitext.revid + wikitext ? wikitext.revid : sectionWikitext.revid ) ); } diff --git a/src/wiki/DeputyCasePage.ts b/src/wiki/DeputyCasePage.ts index edc4d65d..a2dee6d8 100644 --- a/src/wiki/DeputyCasePage.ts +++ b/src/wiki/DeputyCasePage.ts @@ -77,6 +77,15 @@ export default class DeputyCasePage extends DeputyCase { } } + /** + * The n-cache stores the `n` of contribution survey headings. In other + * words, it differentiates survey headings by giving it a number if + * another section on the page has a matching heading. The n-cache + * only contains the n of contribution survey headings, but counts all + * HTML headings as part of the n-cache. + */ + nCache: Map; + /** * @param pageId The page ID of the case page. * @param title The title of the page being accessed diff --git a/src/wiki/DeputyCasePageWikitext.ts b/src/wiki/DeputyCasePageWikitext.ts index bf7d7d43..569a0e2d 100644 --- a/src/wiki/DeputyCasePageWikitext.ts +++ b/src/wiki/DeputyCasePageWikitext.ts @@ -46,8 +46,13 @@ export default class DeputyCasePageWikitext { * grab the section using API:Query for an up-to-date version. * * @param section The section to edit + * @param n If the section heading appears multiple times in the page and n is + * provided, this function extracts the nth occurrence of that section heading. */ - async getSectionWikitext( section: string | number ): Promise { + async getSectionWikitext( + section: string | number, + n = 1 + ): Promise { if ( typeof section === 'number' ) { return getPageContent( this.casePage.pageId, @@ -63,18 +68,28 @@ export default class DeputyCasePageWikitext { let capturing = false; let captureLevel = 0; + let currentN = 1; const sectionLines = []; for ( let i = 0; i < wikitextLines.length; i++ ) { const line = wikitextLines[ i ]; const headerCheck = /^(=={1,5})\s*(.+?)\s*=={1,5}$/.exec( line ); - if ( !capturing && headerCheck != null && headerCheck[ 2 ] === section ) { - sectionLines.push( line ); - capturing = true; - captureLevel = headerCheck[ 1 ].length; + if ( + !capturing && + headerCheck != null && + headerCheck[ 2 ] === section + ) { + if ( currentN < n ) { + currentN++; + } else { + sectionLines.push( line ); + capturing = true; + captureLevel = headerCheck[ 1 ].length; + } } else if ( capturing ) { if ( headerCheck != null && headerCheck[ 1 ].length <= captureLevel ) { capturing = false; + break; } else { sectionLines.push( line ); } diff --git a/src/wiki/util/sectionHeadingN.ts b/src/wiki/util/sectionHeadingN.ts new file mode 100644 index 00000000..728b8a2c --- /dev/null +++ b/src/wiki/util/sectionHeadingN.ts @@ -0,0 +1,43 @@ +import last from '../../util/last'; + +/** + * Checks the n of a given element, that is to say the `n`th occurrence of a section + * with this exact heading name in the entire page. + * + * This is purely string- and element-based, with no additional metadata or parsing + * information required. + * + * This function detects the `n` using the following conditions: + * - If the heading ID does not have an n suffix, the n is always 1. + * - If the heading ID does have an n suffix, and the detected heading name does not end + * with a number, the n is always the last number on the ID. + * - If the heading ID and heading name both end with a number, + * - The n is 1 if the ID has an equal number of ending number patterns (sequences of "_n", + * e.g. "_20_30_40" has three) with the heading name. + * - Otherwise, the n is the last number on the ID if the ID than the heading name. + * + * @param heading The heading to check + * @param headingName The name of the heading to check + * @return The n, a number + */ +export default function ( heading: HTMLHeadingElement, headingName: string ): number { + const headingNameEndPattern = /(?:\s|_)*(\d+)$/g; + const headingIdEndPattern = /_(\d+)$/g; + + const headingId = heading.getAttribute( 'id' ) ?? + heading.querySelector( '.mw-headline' ).getAttribute( 'id' ); + const headingIdMatches = headingId.match( headingIdEndPattern ); + const headingNameMatches = headingName.match( headingNameEndPattern ); + + if ( headingIdMatches == null ) { + return 1; + } else if ( headingNameMatches == null ) { + // Last number of the ID + return +( headingIdEndPattern.exec( last( headingIdMatches ) )[ 1 ] ); + } else if ( headingIdMatches.length === headingNameMatches.length ) { + return 1; + } else { + // Last number of the ID + return +( headingIdEndPattern.exec( last( headingIdMatches ) )[ 1 ] ); + } +}