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 ] );
+ }
+}