Module:Music chart
| This Lua module is used on approximately 77,000 pages and changes may be widely noticed. Test changes in the module's /sandbox or /testcases subpages, or in your own module sandbox. Consider discussing changes on the talk page before implementing them. |
| This module is rated as beta. It is considered ready for widespread use, but as it is still relatively new, it should be applied with some caution to ensure results are as expected. |
| This module is currently protected from editing. See the protection policy and protection log for more details. Please discuss any changes on the talk page; you may submit an edit request to ask an administrator to make an edit if it is uncontroversial or supported by consensus. You may also request that this page be unprotected. |
This module powers the chart templates: {{Single chart}}, {{Album chart}}, {{Year-end single chart}}, and {{Year-end album chart}}.
It generates table rows with chart name, reference, and peak position for music releases. Chart data is stored in four distinct JSON pages, allowing easy maintenance without editing Lua code.
Charts output
Architecture
JSON pages
| Page | Purpose |
|---|---|
| Module:Music chart/single.json | Weekly single chart definitions (formatting schema included) |
| Module:Music chart/album.json | Weekly album chart definitions |
| Module:Music chart/year-end-single.json | Year-end single chart definitions |
| Module:Music chart/year-end-album.json | Year-end album chart definitions |
Helpers for individual charts:
- Module:Music chart/chartdata-czech.json — week ID lookup table for Czech/Slovak charts (ifpicr.cz)
Template integration
Each template calls the module with a type parameter that determines which JSON page to load:
<!-- Template:Single chart -->
<includeonly>{{#invoke:Music chart|main|type=single}}</includeonly>
<!-- Template:Album chart -->
<includeonly>{{#invoke:Music chart|main|type=album}}</includeonly>
<!-- Template:Year-end single chart -->
<includeonly>{{#invoke:Music chart|main|type=year-end-single}}</includeonly>
<!-- Template:Year-end album chart -->
<includeonly>{{#invoke:Music chart|main|type=year-end-album}}</includeonly>
All arguments from template calls are passed automatically. For example:
{{Single chart|Australia|1|artist=Beyoncé|song=Halo|access-date=January 1, 2025}}
The module receives: 1=Australia, 2=1, artist=Beyoncé, song=Halo, access-date=January 1, 2025, type=single.
Output structure
The template produces two table cells and a reference:
| Chart Name (Provider)<ref name="refname">"[url url_title]". lang. ref. ref_note. Retrieved access-date.</ref>
| style="text-align:center;"| Position
With rowheader=true:
! scope="row"| Chart Name (Provider)<ref>...</ref>
| style="text-align:center;"| Position
With note parameter:
| Chart Name (Provider)<ref>...</ref><br>''<small>Note text</small>''
| style="text-align:center;"| Position
Template parameters
Parameters passed to the template are used as placeholders in URL and title templates.
Common parameters
| Parameter | Description |
|---|---|
1 (chart) |
Chart key (required). Must match a key in JSON data. |
2 (position) |
Peak position on the chart (required). Must be a number 1–200 or dash (–) for not charted. |
3 |
Set to M for manual reference mode.
|
artist |
Artist name. |
song / album |
Song or album title (depending on chart type). |
year, week |
Chart week identifiers. |
date |
Chart date (format depends on chart). |
songid, artistid, chartid, id |
Numeric IDs for some charts. |
url, title |
For charts requiring user-provided URL. |
access-date |
Date when source was accessed. |
publish-date |
Publication date of the source (shown in reference for year-end charts). |
archive-url, archive-date |
Archive link and date for archived sources. |
refname |
Custom reference name (overrides auto-generated). |
rowheader |
Set to true to output chart name as row header.
|
note |
Additional note displayed below chart name in small italic text. |
refgroup |
Reference group name for grouping footnotes (e.g., lower-alpha).
|
dvd |
DVD/video title (alternative to album for music video charts).
|
Computed placeholders
Derived automatically from the date parameter. All formats work with any input date format (YYYY-MM-DD, YYYYMMDD, YYMMDD, DD-MM-YYYY, DD.MM.YYYY):
| Placeholder | Output format | Example (any input → output) |
|---|---|---|
{dateDigits} |
YYYYMMDD (digits only) | 20240115
|
{dateMDY} |
Month Day, Year | January 15, 2024
|
{dateDMY} |
DD.MM.YYYY | 15.01.2024
|
{dateYMD} |
YYYY-MM-DD | 2024-01-15
|
{dateSlash} |
D/M/YYYY (no leading zeros) | 15/1/2024
|
{dateYear} |
YYYY | 2024
|
Manual reference mode
For special cases where automatic URL generation doesn't work, use manual mode by setting the third parameter to M:
{{Single chart|Australia|1|M
|url=https://example.com/custom-source
|title=Custom Source Title
|work=Billboard
|date=January 1, 2025
|access-date=January 5, 2025
}}
Manual mode:
- Requires
urlandtitleparameters - Generates reference using {{cite news}} format
- Supports additional params:
work,location,publisher,date,archive-url,archive-date,url-status - Uses same chart name and provider from JSON
JSON data format
Each JSON page contains chart definitions grouped by country/region. The structure is:
{
"_schema": { ... },
"CountryGroup1": {
"ChartKey1": { ... },
"ChartKey2": { ... }
},
"CountryGroup2": {
"ChartKey3": { ... }
}
}
Keys starting with _ (like _schema) are ignored by the module and can be used for documentation.
Basic chart entry
{
"Australia": {
"Australia": {
"chart": "Australia",
"provider": "[[ARIA Charts|ARIA]]",
"url": "https://australian-charts.com/showitem.asp?interpret={artist}&titel={song}&cat=s",
"url_title": "{artist} – {song}",
"ref": "[[ARIA Charts|ARIA Top 50 Singles]]."
},
"AustraliaKent": {
"chart": "Australia",
"provider": "[[Kent Music Report]]",
"ref": "{{cite Kent|page={page}}}"
}
}
}
Note: If chart is omitted, the country group name is used as the chart name.
Core fields
| Field | Required | Description | Example |
|---|---|---|---|
chart |
No | Display name for the chart. Defaults to country group name. | "Australia"
|
provider |
No | Provider name shown in parentheses after chart name. Supports wikilinks. | "ARIA"
|
url |
Yes* | URL template with {placeholders}. If starts with [, treated as pre-built wikilink. |
"https://example.com/{artist}"
|
url_title |
Yes* | Link title template. Combined with URL as "[url title]". |
"{artist} – {song}"
|
ref |
Yes* | Reference text shown after URL. Supports wikilinks and templates. | "ARIA Charts."
|
lang |
No | Language note inserted between URL and ref. | "(in German)" or "(in German)"
|
* At least ref is required. url and url_title are required for charts with links.
URL encoding
The encode field controls how text parameters ({artist}, {song}, {album}, {dvd}) are encoded in URLs. Other parameters (IDs, dates, numbers) are not affected. It's an array of operations applied in fixed order.
| Operation | Description | Example |
|---|---|---|
normalize |
Remove diacritics (accents). Converts é→e, ñ→n, ü→u, etc. Use for sites like Billboard that don't support accented characters in URLs. |
Céline → Celine
|
ansi |
Latin-1 (ISO-8859-1) encoding instead of UTF-8. Encodes é as %E9 (single byte) instead of %C3%A9 (UTF-8). Use for legacy sites like Hung Medien. |
Céline → C%E9line
|
lower |
Convert to lowercase | The Beatles → the beatles
|
clean-symbols |
Remove special characters. Keeps only letters (a-z, A-Z), numbers (0-9), and dashes (-). Spaces become dashes. Symbols between digits become dashes (e.g., 2.0 → 2-0). |
20/20 Experience! → 20-20-experience
|
space-plus |
Replace spaces with +, full URL encoding. This is the default — only specify explicitly in multiple entries when overriding a different chart-level space setting. |
The Beatles → The+Beatles
|
space-dash |
Replace spaces with -, encode only non-ASCII. Preserves ASCII symbols like $, '. Use for Billboard and similar sites. |
A$AP Rocky → A$AP-Rocky
|
space-url |
Replace spaces with %20 (standard URL encoding) |
The Beatles → The%20Beatles
|
Order in array doesn't matter — operations are always applied in this sequence: normalize → lower → clean-symbols → space replacement → URL encoding.
Entry-level encode: The encode field can also be set in multiple entries to override the chart-level setting for specific URL variants.
Examples:
| encode | Input | Output |
|---|---|---|
| (not specified) | The Beatles |
The+Beatles
|
["ansi"] |
Céline Dion |
C%E9line+Dion
|
["space-dash"] |
The Beatles |
The-Beatles
|
["space-dash"] |
A$AP Rocky |
A$AP-Rocky
|
["space-url"] |
The Beatles |
The%20Beatles
|
["normalize", "space-dash"] |
Céline Dion |
Celine-Dion
|
["normalize", "lower", "clean-symbols", "space-dash"] |
20/20 Experience! |
20-20-experience
|
["ansi", "space-dash"] |
Céline Dion |
C%E9line-Dion
|
Date validation
The date_format field specifies expected date format. If provided, the module validates user input and shows error for invalid dates.
| Format | Pattern | Example |
|---|---|---|
YYYY-MM-DD |
4-2-2 digits with dashes | 2024-01-15
|
YYYYMMDD |
8 digits | 20240115
|
DD-MM-YYYY |
2-2-4 digits with dashes | 15-01-2024
|
MM-DD-YYYY |
2-2-4 digits with dashes | 01-15-2024
|
DD.MM.YYYY |
2-2-4 digits with dots | 15.01.2024
|
YYMMDD |
6 digits | 240115
|
DD.MM.YYYY–DD.MM.YYYY |
Date range with dots | 15.01.2024–21.01.2024
|
YYYY.MM.DD–YYYY.MM.DD |
Date range with dots (Korean) | 2024.01.15–2024.01.21
|
YYYYMMDD-YYYYMMDD |
Date range | 20240115-20240121
|
Date range separators: For date range formats, the module accepts en-dash (–), hyphen (-), and tilde (~) as separators. In the params column of showCharts output, all separators are displayed as hyphen for consistency.
Use date_format_alt to accept an alternative format.
Year and week validation
The module also validates year and week parameters when provided:
yearmust be exactly 4 digits (e.g.,2024)weekmust be 1–2 digits (e.g.,1,03,52) or combined format like51+52
If the parameter is used in URL construction (critical), an error is shown in red and the page is added to tracking category. If only used in reference text, a warning is shown in preview mode only.
Other fields
| Field | Description |
|---|---|
defunct |
Set true for inactive charts. Adds tracking category and highlights row in showCharts table.
|
alias_for |
Variant to another chart key. Entry should contain ONLY this field. Should gradually be replaced with the main ones and deleted. |
ref_note |
Note/instruction shown AFTER ref in standard mode, or as prefix in combine mode. Can be overridden in multiple entries.
|
ref_suffix |
Text added AFTER "Retrieved date" in reference. Useful for additional notes that should appear at the very end. |
refname_format |
Custom ref name format with placeholders. Supports {param|default} syntax.
|
url_validation |
Required substring in user-provided URL. Shows error "Invalid URL. Required domain: X." if missing. |
track_param |
Parameter name to track. Creates category if parameter is missing. |
number_one_category |
Category added when position equals 1. |
category_conditions |
Array of conditional categories. Each element: {"when": "condition", "category": "Category name"}. See Condition syntax. All matching conditions add their category.
|
doc_note |
Documentation note shown in showCharts table (when notes=yes). Can be set at chart level or in individual multiple entries.
|
Note: Punctuation (periods) between reference parts is added automatically by the module. Do not include trailing periods in ref, ref_note, or ref_suffix values in JSON.
Multiple URL variants
For charts that have different URL structures depending on available parameters, use the multiple array:
{
"Denmark": {
"chart": "Denmark",
"provider": "[[Hitlisten|Tracklisten]]",
"multiple": [
{
"when": "year, week",
"url": "http://hitlisten.nu/default.asp?w={week}&y={year}&list=t40",
"url_title": "Track Top-40 – Hitlisten.NU",
"lang": "(in Danish)"
},
{
"when": "artist, song",
"url": "https://danishcharts.dk/showitem.asp?interpret={artist}&titel={song}&cat=s",
"url_title": "{artist} – {song}"
}
],
"ref": "[[Hitlisten|Tracklisten]]."
}
}
The module checks entries in order and uses the first one where when condition matches.
Condition syntax
| Syntax | Meaning | Example |
|---|---|---|
param |
Parameter exists and is not empty | "when": "year"
|
!param |
Parameter is missing or empty | "when": "!url"
|
param=value |
Parameter equals specific value | "when": "type=remix"
|
param<value |
Parameter is less than value (numeric) | "when": "year<1987"
|
param>value |
Parameter is greater than value (numeric) | "when": "year>2000"
|
param<=value |
Parameter is less than or equal (numeric) | "when": "year<=2002"
|
param>=value |
Parameter is greater than or equal (numeric) | "when": "year>=2003"
|
date<value |
Year from date is less than value | "when": "date<2016"
|
archivedate<value |
Year from archivedate is less than value | "when": "archivedate<2016"
|
a, b |
AND — all conditions must match | "when": "year, week"
|
a | b |
OR — any condition must match | "when": "artist | song"
|
helper=value |
Helper function returns specific value | "when": "helper=before2016w34"
|
| (empty) | Default fallback — always matches | "when": ""
|
Notes:
- Conditions are checked in order; first match wins (for
multiple) - For
category_conditions, ALL matching conditions add their categories - Numeric comparisons convert values to numbers; non-numeric values become 0
- For
date,archivedate, andarchive-dateparameters, comparisons extract the year (first 4 characters) from the value - The
positionparameter is available incategory_conditions
Entry overrides
Each entry in multiple can override these fields from the parent chart:
url,url_title,encoderef,ref_note,langprovider,chartrefname_formatdate_format,date_format_altdoc_note(for showCharts documentation)
Example: Different chart names by year
Some charts changed names over time. Use multiple with year conditions to override chart:
{
"Billboarddanceclubsongs": {
"chart": "US [[Dance Club Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/DSI",
"url_title": "{artist} Chart History (Dance Club Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": ["lower", "space-dash"],
"multiple": [
{"when": "year<1987", "chart": "US [[Hot Dance Club Play]]"},
{"when": "year<2003", "chart": "US [[Hot Dance Music/Club Play]]"}
]
}
}
This outputs:
- Before 1987: "US Hot Dance Club Play (Billboard)"
- 1987–2002: "US Hot Dance Music/Club Play (Billboard)"
- 2003+: "US Dance Club Songs (Billboard)" — uses default
chartfrom parent
The module checks entries in order. If no when matches, it uses the parent chart's values.
Use ref_note in entries for conditional notes (e.g., instructions for old chart entries):
{
"Germany": {
"chart": "Germany",
"provider": "[[GfK Entertainment charts|GfK]]",
"url": "https://www.offiziellecharts.de/titel-details-{songid}",
"url_title": "Offizielle Deutsche Charts",
"lang": "(in German)",
"ref": "[[GfK Entertainment charts]]",
"multiple": [
{"when": "year<1977", "ref_note": "To see the peak chart position, click 'TITEL VON', followed by the artist's name"}
]
}
}
In showCharts table, conditional overrides are shown inline: [year<1977 → ref_note text]
Combine mode
Set "combine": true to include ALL matching entries as a bullet list in one reference:
{
"Finland": {
"chart": "Finland",
"provider": "[[The Official Finnish Charts|Suomen virallinen lista]]",
"ref_note": "The first is domestic singles, the second is foreign:",
"multiple": [
{
"url": "http://www.ifpi.fi/tilastot/myydyimmat/{year}/kotimaiset/singlet",
"url_title": "Myydyimmät kotimaiset singlet vuonna {year}",
"lang": "(in Finnish)"
},
{
"url": "http://www.ifpi.fi/tilastot/myydyimmat/{year}/ulkomaiset/singlet",
"url_title": "Myydyimmät ulkomaiset singlet vuonna {year}",
"lang": "(in Finnish)"
}
],
"combine": true,
"ref": "Musiikkituottajat – IFPI Finland."
}
}
Output reference will contain:
The first is domestic singles, the second is foreign: * "[url1 title1]" (in Finnish). Musiikkituottajat – IFPI Finland. Retrieved January 1, 2025. * "[url2 title2]" (in Finnish). Musiikkituottajat – IFPI Finland. Retrieved January 1, 2025.
Helper functions
Helpers are special Lua functions for dynamic URL generation. They're defined in the module and referenced by name in JSON.
Conditional categories
The category_conditions field allows adding categories based on parameter values. Unlike number_one_category (which only checks position=1), this supports complex conditions.
{
"UKsinglesdownloads": {
"chart": "UK [[UK Singles Downloads Chart|Singles Downloads]]",
"provider": "[[Official Charts Company|OCC]]",
"category_conditions": [
{"when": "position=1", "category": "UK Singles Downloads Chart number-one singles"}
],
"url": "...",
"ref": "..."
}
}
Multiple conditions can be specified — all matching conditions add their categories:
"category_conditions": [
{"when": "position=1", "category": "Number-one singles"},
{"when": "position<=10", "category": "Top 10 singles"},
{"when": "year>=2020", "category": "2020s chart entries"}
]
The position parameter is available in conditions even though it's not a template parameter — it's extracted from the second positional argument.
Available helpers
| Helper | Charts | Required params | Description |
|---|---|---|---|
south_africa_size |
single: Southafrica2 | year, week | Returns chart size (10/20/50/100) based on year and week. Chart size changed multiple times: 2021-2022 → 100, 2023 w18+ → 10, 2025 w13+ → 50, etc. |
australia_issue |
single: Australiadance, Australiapandora, Australiaurban | url | Extracts issue number from pandora.nla.gov.au URL. Matches patterns like "Issue+123" or "issue%20456". |
bulgaria_date_range |
single: Bulgaria | url | Extracts and formats date range from bamp-bg.org URL. Converts "01012024-07012024.html" to "01.01.2024 – 07.01.2024". |
slovakia_period |
single: Slovakdigital, Slovakradio | year, week | Returns period identifier: "before2016w34" (≤week 34), "after2016w34" (weeks 35–42), or "use_new_chart" (≥week 43, shows error recommending Slovakia2/Slovakdigital2). |
czech_week_id |
single: Czech Republic, Czechdigital, Slovakia2, Slovakdigital2; album: Czech, Slovakia | year, week | Returns weekId for ifpicr.cz URL. Looks up ID from Module:Music chart/chartdata-czech.json; for future weeks beyond table, calculates based on newest entry. Weeks 51+52 share same ID. |
germany_timestamp |
album: GermanyComp | date | Converts DD.MM.YYYY to Unix timestamp in milliseconds for URL construction. |
Using helpers in JSON
{
"Southafrica2": {
"chart": "South Africa",
"provider": "[[The Official South African Charts|TOSAC]]",
"helper": "south_africa_size",
"url": "https://theofficialsacharts.co.za/chart/{year}/{week}/top-{helper}",
"url_title": "Top {helper} – Week {week}, {year}",
"ref": "[[The Official South African Charts]]."
}
}
The {helper} placeholder is replaced with the helper function's return value.
Adding new helpers
1. Add function in module:
function Helpers.your_helper_name(args)
local year = tonumber(args.year) or 0
-- your logic here
return "result"
end
2. Register required parameters:
Helpers.params = {
your_helper_name = {"year", "week"}, -- params won't be flagged as "unused"
-- ...
}
3. Use in JSON:
{
"helper": "your_helper_name",
"url": "https://example.com/{helper}"
}
Adding new charts
Step 1: Edit JSON page. Open the appropriate JSON page and add entry under country group:
"NewCountry": {
"NewCountry": {
"chart": "New Country Singles",
"provider": "[[New Chart Provider]]",
"url": "https://example.com/chart?artist={artist}&song={song}",
"url_title": "{artist} – {song}",
"ref": "New Chart Provider."
}
}
Step 2: Verify. Use showCharts to verify:
{{#invoke:Music chart|showCharts|type=single|country=NewCountry}}
Check that:
- Required params are correctly detected
- Sample output looks correct
- No errors in output
Step 3: Add testcases. Add cases to the both proper parameters section and errors section.
Common patterns
Simple chart (artist + song):
"SimpleChart": {
"chart": "Simple Chart",
"provider": "[[Provider]]",
"url": "https://example.com/{artist}/{song}",
"url_title": "{artist} – {song}",
"ref": "Provider Name."
}
Chart with date:
"DateChart": {
"chart": "Date Chart",
"provider": "[[Provider]]",
"date_format": "YYYY-MM-DD",
"url": "https://example.com/chart/{date}",
"url_title": "Chart for {dateMDY}",
"ref": "Provider Name."
}
Chart with year/week:
"WeekChart": {
"chart": "Week Chart",
"provider": "[[Provider]]",
"url": "https://example.com/{year}/week-{week}",
"url_title": "Week {week}, {year}",
"ref": "Provider Name."
}
Chart requiring user URL:
"ManualUrlChart": {
"chart": "Manual URL Chart",
"provider": "[[Provider]]",
"url_validation": "example.com",
"url": "{url}",
"url_title": "Chart Page",
"ref": "Provider Name."
}
Defunct chart:
"OldChart": {
"chart": "Old Chart",
"provider": "[[Provider]]",
"defunct": true,
"ref": "Provider Name (defunct)."
}
Module configuration
The CONFIG table at the top of the module contains all settings.
Text output
months— month names array for{dateMDY}placeholdertype_names— display names for chart typeserrors— error message templateswarnings— warning templates shown in preview modetext— reference text templatescategories— tracking category templates
Position validation
max_position— maximum allowed numeric position (default: 200)accepted_dashes— array of accepted dash characters for "not charted" position; en-dash (–) for enwiki, em-dash (—) for ruwiki, hyphen (-) as fallback. Example:{"–", "—", "-"}
Core settings
default_type— default chart type when not specifiedjson_path— path template to JSON data files,%sreplaced with typecategory_prefix— namespace prefix for categoriesdate_format_mdy— format string for{dateMDY}placeholder (%M= month name,%d= day,%Y= year)ref_prefixes— prefixes for auto-generated refnames by typeerror_display— where errors appear:"both"(cell and ref),"cell"only, or"ref"onlycheck_unknown_params— enable/disable tracking of unknown parameters (default:true)check_unused_params— enable/disable tracking of unused parameters (default:true)date_wrapper— optional template to reformat dates in references; set tonilto output as-is, or use parser function like{{#iferror:{{#time:j F Y|%s}}|%s}}to convert "2025-01-15" → "15 January 2025"date_patterns— Lua patterns for validating date formatsparam_aliases— alternative parameter names that map to canonical namesparams— parameter validation groups:base— always allowedcontent— chart-specificmanual— only with |3=M mode- Unknown parameters trigger tracking category; add new valid params here to suppress warnings
showCharts settings
sort_order— sort order for groups and chart IDs:"abc"(alphabetical) or"keep"(preserve JSON order)group_wrapper— optional template for country/group names, e.g.{{Country|%s}}; to exclude specific groups from wrapping, add[no wrap]to the group name in JSON
Reference prefixes
ref_prefixes maps chart types to their reference name prefixes:
single→"sc"(produces refs likesc_Australia_Beyoncé)album→"ac"(produces refs likeac_Billboard200_Taylor Swift)year-end-singleandyear-end-album→"ye"(produces refs likeye_US_2024)
Reference naming
Default reference name format: {prefix}_{chartkey}_{suffix}
- Prefix: from
ref_prefixes(sc, ac, ye) - Suffix:
artistfor weekly charts,yearfor year-end charts
Examples:
sc_Australia_Beyoncéac_Billboard200_Taylor Swiftyear-end_US_2024
Custom format via refname_format in JSON:
"refname_format": "sc_Oricon2_{songid}"
"refname_format": "sc_Finland_{artist}_{song|Airplay}"
The {param|default} syntax provides fallback if param is empty.
Entry points
main
Primary function called by templates. Processes all arguments and generates table row output.
{{#invoke:Music chart|main|type=single}}
chartExists
Utility function to check if a chart key exists in the JSON data.
{{#invoke:Music chart|chartExists|chart=Australia|type=single}}
Returns 1 if chart exists, 0 otherwise. Useful for conditional logic in templates.
showCharts
Generates a table showing all charts from JSON with sample outputs.
{{#invoke:Music chart|showCharts|type=single}}
Output columns:
| Column | Parameter (default) | Description |
|---|---|---|
| — | type (single) |
Chart type: single, album, year-end-single, year-end-album |
| Group | country (all) |
Filters to one country/region group |
| Chart ID | — | Chart key. Aliases in yellow, defunct in red, multiple entries show condition. |
| Uses | uses (yes) |
Page count using this chart (links to category) |
| Chart | — | Display name of the chart |
| Provider | — | Chart provider name |
| Required params | params (yes) |
Parameters needed, date format in brackets |
| #1 Category | number1 (no) |
Category when position=1 |
| Sample ref output | ref (yes) |
Generated reference with sample values |
| Note | notes (no) |
Shows doc_note from JSON. Consecutive identical notes are merged with rowspan.
|
| — | splitdvd (no) |
When yes, charts ending with "MV" (Music DVD) are output in a separate table below the main table.
|
Example with all parameters:
{{#invoke:Music chart|showCharts|type=single|country=Australia|uses=yes|params=yes|number1=yes|ref=yes|notes=yes|splitdvd=yes}}
Notes column: When notes=yes, displays doc_note field from JSON for each chart. For charts with multiple entries, each entry can have its own doc_note that overrides the chart-level note. Consecutive rows with identical notes are automatically merged using rowspan.
Output includes links to JSON data page and template testcases page.
Error handling
Validation checks
The module performs these validations:
- Missing chart key — Error if first parameter is empty
- Unknown chart — Error if chart key not found in JSON
- Missing required parameters — Error if placeholders in URL/title can't be filled
- Invalid date format — Error if date doesn't match expected format (when
date_formatspecified) - Invalid year format — Error if year is not exactly 4 digits
- Invalid week format — Error if week is not 1–2 digits (or combined like "51+52")
- URL validation — Error if user URL doesn't contain required substring (when
url_validationspecified) - Unsubstituted placeholders — Error if output still contains
{placeholder}patterns
Error display
Errors appear as red text. The error_display config controls where:
"both"— Show in both table cell and reference"cell"— Show only in table cell"ref"— Show only in reference
Warnings (preview only)
Some issues show orange warnings only in preview mode (not on saved pages):
| Warning | When shown |
|---|---|
| Unused parameters | Parameters provided but not used by chart |
| Invalid date (ref only) | Date format invalid, but date only used in reference text (not URL) |
| Invalid year/week (ref only) | Year/week format invalid, but only used in reference text (not URL) |
Detection uses {{REVISIONID}} — empty in preview, has value on saved pages.
If date is used in URL, invalid format shows as red error on all pages.
Tracking categories
Categories are added for various conditions:
| Category pattern | When added |
|---|---|
{Type} chart usages for {ChartKey} |
Always (for usage tracking) |
{Type} chart used with missing parameters |
Required params missing |
{Type} chart used with unknown chart |
Chart key not found |
{Type} chart with invalid position |
Position is not a valid number (1–200) or accepted dash |
Pages using {type} chart with unknown parameters |
Unknown params provided |
{Type} chart with unused parameters |
Params provided but not used |
{Type} chart called without artist |
Artist param missing |
{Type} chart called without song |
Song param missing (single charts) |
{Type} chart called without album |
Album or dvd param missing (album charts) |
{Type} chart used with defunct chart |
Chart marked as defunct |
{Type} chart making named ref |
Custom refname provided |
{Type} chart using manual ref mode |
Manual mode (3=M) used |
{Type} chart with manual mode missing url or title |
Manual mode used without url or title |
Categories are only added in main namespace (ns=0, articles). They are suppressed in all other namespaces including Template, User, and Talk pages.
local p = {}
-------------------------------------------------------------------------------
-- Module:Music Chart
-- Generates table rows for music chart positions with automatic references.
-- Chart data stored in JSON files, referenced by key (e.g., "Australia").
--
-- Called via templates:
-- {{Single chart|Australia|1|artist=...|song=...|...}}
-- {{Album chart|Australia|1|artist=...|album=...|...}}
-- {{Year-end single chart|Australia|1|artist=...|song=...|year=2024|...}}
-- {{Year-end album chart|Australia|1|artist=...|album=...|year=2024|...}}
--
-- Template code: {{#invoke:Music chart|main|type=single}}
-- Arguments passed automatically from template call.
-------------------------------------------------------------------------------
--=============================================================================
-- SECTION 1: CONFIGURATION
-- All module settings in one place. Edit here to customize behavior.
--=============================================================================
local CONFIG = {
---------------------------------------------------------------------------
-- TEXT OUTPUT
---------------------------------------------------------------------------
-- Month names for {dateMDY} placeholder
months = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
},
-- Human-readable names for chart types (used in error messages and categories)
type_names = {
single = "Single",
album = "Album",
["year-end-single"] = "Year-end single",
["year-end-album"] = "Year-end album",
},
---------------------------------------------------------------------------
-- POSITION VALIDATION
---------------------------------------------------------------------------
-- Maximum allowed numeric position (1-200 typical for charts)
max_position = 200,
-- Accepted dash characters for "not charted" position
-- en-dash (–) for enwiki, em-dash (—) for ruwiki, hyphen (-) as fallback
-- For multiple: accepted_dashes = {"–", "—", "-"},
accepted_dashes = {"–"},
-- Error message templates
errors = {
prefix = 'ERROR in "%s": ',
unknown_chart = 'Unknown chart "%s".',
missing_params = "Missing parameters: %s.",
invalid_date = "Invalid date format. Expected: %s.",
invalid_year = "Invalid year format: %s. Expected 4 digits.",
invalid_week = "Invalid week format: %s. Expected 1–2 digits (use 51+52 for combined weeks).",
missing_chart = "Missing parameter: chart.",
missing_position = "Missing parameter: position.",
invalid_position = "Invalid position: %s. Expected number 1–%d or dash (–).",
manual_missing_url_title = "Manual mode (M) requires url and title parameters.",
url_validation = "Invalid URL. Required domain: %s.",
use_new_chart = "For this date range, use %s instead.",
},
-- Warning templates (shown only in preview mode)
warnings = {
unused_params = 'WARNING: Unused parameters for "%s": %s.', -- chartkey, params
unknown_params = 'WARNING: Unknown parameters for "%s": %s.', -- chartkey, params
invalid_date_ref = "Date format should be %s.",
},
-- Reference text templates
text = {
retrieved = "Retrieved %s.", -- %s = access-date
archived = "Archived from [%s the original] on %s.", -- %s = archive-url, archive-date
},
-- Category name templates
-- %s placeholders filled with type name, chart key, param name
categories = {
usage = "%s chart usages for %s", -- type, chartkey
named_ref = "%s chart making named ref",
manual_ref = "%s chart using manual ref mode",
defunct = "%s chart used with defunct chart",
unknown_chart = "%s chart used with unknown chart",
missing_params = "%s chart used with missing parameters",
unknown_params = "Pages using %s chart with unknown parameters",
unused_params = "%s chart with unused parameters",
manual_missing_url_title = "%s chart with manual mode missing url or title",
without_artist = "%s chart called without artist",
without_song = "%s chart called without song",
without_album = "%s chart called without album", -- checks both album and dvd
invalid_position = "%s chart with invalid position",
track_param = "%s chart %s without %s parameter", -- type, chartkey, param
unsubstituted = "%s chart with unsubstituted parameters",
},
---------------------------------------------------------------------------
-- CORE SETTINGS
---------------------------------------------------------------------------
-- Default chart type when not specified
default_type = "single",
-- Path to JSON data files
-- %s is replaced with chart type: "single", "album", "year-end-single", "year-end-album"
json_path = "Module:Music chart/%s.json",
-- Category namespace prefix
category_prefix = "Category:",
-- Format for {dateMDY} placeholder
-- %M = month name, %d = day number, %Y = 4-digit year
-- Example: "%M %d, %Y" with date 2024-01-15 → "January 15, 2024"
date_format_mdy = "%M %d, %Y",
-- Prefixes for auto-generated reference names (refname)
-- Result: prefix_chartkey_artist (e.g., "sc_Australia_Beyoncé")
ref_prefixes = {
single = "sc", -- Single chart
album = "ac", -- Album chart
["year-end-single"] = "ye",
["year-end-album"] = "ye",
},
-- Where to display errors: "both" | "cell" | "ref"
error_display = "both",
-- Check for unknown parameters (not in CONFIG.params)
check_unknown_params = true,
-- Check for unused parameters (provided but not used by chart)
check_unused_params = true,
-- Optional wrapper for dates in references (access-date, publish-date, archive-date)
-- nil = output dates as-is
-- Use %s for date value (two %s if wrapper needs fallback)
-- Examples:
-- Optional template to reformat dates in references (access-date, archive-date).
-- #time parses most date formats automatically (2025-01-15, January 15, 2025, 15.01.2025, etc.)
-- #iferror returns original value if parsing fails (e.g., invalid or unusual format)
-- Examples:
-- "{{#iferror:{{#time:F j, Y|%s}}|%s}}" → "January 15, 2025" (MDY)
-- "{{#iferror:{{#time:j F Y|%s}}|%s}}" → "15 January 2025" (DMY)
-- nil → output date as-is without reformatting
date_wrapper = nil,
-- Date validation patterns (Lua patterns for each format)
date_patterns = {
["DD-MM-YYYY"] = "^%d%d%-%d%d%-%d%d%d%d$",
["DD.MM.YYYY"] = "^%d%d%.%d%d%.%d%d%d%d$",
["DD/MM/YYYY"] = "^%d%d/%d%d/%d%d%d%d$",
["MM-DD-YYYY"] = "^%d%d%-%d%d%-%d%d%d%d$",
["YYMMDD"] = "^%d%d%d%d%d%d$",
["YYYY-MM-DD"] = "^%d%d%d%d%-%d%d%-%d%d$",
["YYYYMMDD"] = "^%d%d%d%d%d%d%d%d$",
["DD.MM.YYYY–DD.MM.YYYY"] = "^%d%d%.%d%d%.%d%d%d%d[–%-~]%d%d%.%d%d%.%d%d%d%d$",
["YYYY.MM.DD–YYYY.MM.DD"] = "^%d%d%d%d%.%d%d%.%d%d[–%-~]%d%d%d%d%.%d%d%.%d%d$",
["YYYYMMDD-YYYYMMDD"] = "^%d%d%d%d%d%d%d%d[%-~]%d%d%d%d%d%d%d%d$",
},
-- Parameter aliases (canonical → alternatives)
param_aliases = {
["access-date"] = {"accessdate"},
["publish-date"] = {"publishdate"},
["archive-url"] = {"archiveurl"},
["archive-date"] = {"archivedate"},
},
-- Parameter groups for validation
-- base: always allowed | content: chart-specific | manual: only with 3=M
params = {
base = {
[1] = true, [2] = true, [3] = true,
chart = true, type = true, position = true,
refname = true, refgroup = true, rowheader = true,
note = true,
artist = true, song = true, album = true, dvd = true,
["access-date"] = true, ["publish-date"] = true,
["archive-url"] = true, ["archive-date"] = true,
},
content = {
date = true, year = true, week = true,
startdate = true, enddate = true,
artistid = true, songid = true, chartid = true, id = true,
page = true,
url = true, title = true,
},
manual = { work = true, location = true, publisher = true, ["url-status"] = true },
},
---------------------------------------------------------------------------
-- OUTPUT FORMATS
---------------------------------------------------------------------------
-- Cell prefixes
cell_normal = "| ",
cell_header = '!scope="row"| ',
-- Position cell style
position_style = 'style="text-align:center;"|',
-- Note format
note_format = "<br>''<small>%s</small>''",
---------------------------------------------------------------------------
-- SHOW CHARTS SETTINGS (affect only showCharts output)
---------------------------------------------------------------------------
-- Sort order for group (countries) and chart IDs: "abc" (alphabetical) or "keep" (JSON order)
sort_order = "abc",
-- Optional wrapper for Group column (country/region names)
-- nil = output as-is
-- Use %s for group name
-- Examples:
-- "{{Country|%s}}" → wraps in Country template
-- "{{Flagicon|%s}} %s" → adds flag icon (two %s: for flag and name)
group_wrapper = "{{Country|%s}}",
}
--=============================================================================
-- SECTION 2: UTILITY FUNCTIONS
--=============================================================================
-- Get type display name
local function getTypeName(chartType)
return CONFIG.type_names[chartType] or chartType
end
-- Build category link
local function catLink(pattern, ...)
return string.format("[[" .. CONFIG.category_prefix .. pattern .. "]]", ...)
end
-- Build category link with sort key
local function catLinkSort(pattern, sortKey, ...)
local catName = string.format(pattern, ...)
return string.format("[[%s%s|%s]]", CONFIG.category_prefix, catName, sortKey)
end
-- Check if arg has non-empty value
local function hasArg(args, key)
return args[key] and args[key] ~= ""
end
-- Helper to set error messages based on CONFIG.error_display
local function setErrorDisplay(errMsg, currentInline, currentRef)
local inline = currentInline or ""
local inRef = currentRef or ""
if CONFIG.error_display == "both" or CONFIG.error_display == "cell" then
inline = inline .. errMsg
end
if CONFIG.error_display == "both" or CONFIG.error_display == "ref" then
inRef = inRef .. errMsg
end
return inline, inRef
end
--=============================================================================
-- SECTION 3: URL ENCODERS
-- Encode parameter values for URLs. Selected via "encode" field in JSON.
-- Encoding applies only to text params: artist, song, album, dvd
--
-- Operations (in "encode" array):
-- normalize - remove diacritics (é→e, ñ→n)
-- ansi - Latin-1 encoding (é→%E9 instead of UTF-8 %C3%A9)
-- lower - lowercase
-- clean-symbols - remove special chars, keep alphanumeric and dash
-- space-plus - space → + (default if no encode specified), full URL encoding
-- space-dash - space → -, encode only non-ASCII (preserves $, ', etc.)
-- space-url - space → %20 (standard URL encoding)
--
-- Order in array doesn't matter - applied in fixed order:
-- normalize → lower → clean-symbols → space replacement → URL encoding
--
-- "encode" can be set at chart level or in multiple entries (entry overrides chart)
--=============================================================================
local Encoders = {}
-- Remove diacritics using Unicode normalization (NFD decomposition)
-- é → e + combining accent → remove combining → e
local function removeDiacritics(s)
-- NFD decomposes: é → e + ́ (combining acute)
local decomposed = mw.ustring.toNFD(s)
-- Remove combining diacritical marks (U+0300–U+036F)
return mw.ustring.gsub(decomposed, "[\204\128-\205\175]", "")
end
-- Parse encode config (array or nil) into flags
local function parseEncodeConfig(config)
local flags = { ansi = false, normalize = false, lower = false, cleanSymbols = false, space = "+", spaceUrl = false }
if not config then return flags end
if type(config) == "string" then config = {config} end
for _, op in ipairs(config) do
if op == "ansi" then flags.ansi = true
elseif op == "normalize" then flags.normalize = true
elseif op == "lower" then flags.lower = true
elseif op == "clean-symbols" then flags.cleanSymbols = true
elseif op == "space-plus" then flags.space = "+"
elseif op == "space-dash" then flags.space = "-"
elseif op == "space-url" then flags.spaceUrl = true
end
end
return flags
end
-- Main encode function
function Encoders.encode(s, config)
if not s then return "" end
local flags = parseEncodeConfig(config)
-- 1. Remove diacritics (é→e, ñ→n)
if flags.normalize then
s = removeDiacritics(s)
end
-- 2. Lowercase
if flags.lower then
s = string.lower(s)
end
-- 3. Clean (remove special chars, keep alphanumeric and dash)
if flags.cleanSymbols then
s = string.gsub(s, " ", "-")
s = string.gsub(s, "(%d)[^%w%-](%d)", "%1-%2") -- 2.0 → 2-0
s = string.gsub(s, "[^%w%-]", "")
return s -- clean mode doesn't need further encoding
end
-- 4. Handle & before space replacement
s = string.gsub(s, "&", "%%26")
-- 5. Space replacement + encoding
if flags.spaceUrl then
-- Standard URL encoding (space → %20)
return mw.uri.encode(s, "PATH")
elseif flags.ansi then
-- Latin-1 (ISO-8859-1) encoding
local r = ""
for i = 1, mw.ustring.len(s) do
local k = mw.ustring.codepoint(s, i, i)
if k == 32 then
r = r .. flags.space
elseif k <= 32 or k > 126 then
if k > 255 then
-- UTF-8 multi-byte (Chinese, etc.)
local char = mw.ustring.sub(s, i, i)
for j = 1, #char do
r = r .. string.format("%%%02X", char:byte(j))
end
else
-- Latin-1 single byte
r = r .. string.format("%%%02X", k)
end
else
r = r .. mw.ustring.sub(s, i, i)
end
end
return r
elseif flags.space == "-" then
-- space-dash: replace spaces, encode only non-ASCII (preserve $, ', etc.)
s = string.gsub(s, " ", "-")
local r = ""
for i = 1, mw.ustring.len(s) do
local k = mw.ustring.codepoint(s, i, i)
if k > 127 then
-- Non-ASCII: UTF-8 encode
local char = mw.ustring.sub(s, i, i)
for j = 1, #char do
r = r .. string.format("%%%02X", char:byte(j))
end
else
r = r .. mw.ustring.sub(s, i, i)
end
end
return r
else
-- Default: space-plus with full URL encoding
s = mw.uri.encode(s, "PATH")
s = string.gsub(s, "%%20", "+")
return s
end
end
-- Get encoder function for a config
function Encoders.get(config)
return function(s) return Encoders.encode(s, config) end
end
--=============================================================================
-- SECTION 4: HELPER FUNCTIONS
-- Special chart-specific logic called via "helper" field in JSON.
-- Result available as {helper} placeholder in URL/title templates.
--
-- To add new helper:
-- 1. Add function Helpers.your_name(args) returning string
-- 2. Add required params to Helpers.params["your_name"]
-- 3. Use in JSON: "helper": "your_name", in URL: "{helper}"
--=============================================================================
local Helpers = {}
-- Required params for each helper (won't be flagged as "unused")
Helpers.params = {
south_africa_size = {"year", "week"},
australia_issue = {"url"},
bulgaria_date_range = {"url"},
slovakia_period = {"year", "week"},
germany_timestamp = {"date"},
czech_week_id = {"year", "week"},
israel_week = {"year", "week"},
}
-- Alternative charts to recommend when helper returns "use_new_chart"
Helpers.alternatives = {
Slovakdigital = "Slovakdigital2",
Slovakia = "Slovakia2",
}
-- [single: Southafrica2] South Africa chart size changed over time:
-- 2021-2022: 100 | 2023 w1-17: 100, w18+: 10 | 2024: 10
-- 2025 w1-12: 10, w13-37: 50, w38+: 20 | 2026+: 20
function Helpers.south_africa_size(args)
local year, week = tonumber(args.year) or 0, tonumber(args.week) or 0
if year < 2023 then return "100"
elseif year == 2023 then return week < 18 and "100" or "10"
elseif year == 2024 then return "10"
elseif year == 2025 then
if week < 13 then return "10"
elseif week < 38 then return "50"
else return "20" end
else return "20" end
end
-- [single: Australiadance, Australiapandora, Australiaurban]
-- Extract issue number from pandora.nla.gov.au URL
-- Matches "Issue+123" or "issue%20456" → "123" or "456"
function Helpers.australia_issue(args)
local url = args.url or ""
return string.match(url, "[IiSsUuEe]+[+%%20]*(%d+)") or ""
end
-- [single: Bulgaria] Extract and format date range from bamp-bg.org URL
-- URL: ...01012024-07012024.html → "01.01.2024 – 07.01.2024"
function Helpers.bulgaria_date_range(args)
local url = args.url or ""
local d1, m1, y1, d2, m2, y2 = string.match(url, "(%d%d)(%d%d)(%d%d%d%d)%-(%d%d)(%d%d)(%d%d%d%d)%.html$")
if d1 then return string.format("%s.%s.%s – %s.%s.%s", d1, m1, y1, d2, m2, y2) end
d1, m1, y1, d2, m2, y2 = string.match(url, "(%d%d)(%d%d)(%d%d)%-(%d%d)(%d%d)(%d%d)%.html$")
if d1 then return string.format("%s.%s.20%s – %s.%s.20%s", d1, m1, y1, d2, m2, y2) end
d1, m1, d2, m2, y2 = string.match(url, "%-(%d%d)(%d%d)%-(%d%d)(%d%d)(%d%d%d%d)%.html$")
if d1 then return string.format("%s.%s.%s – %s.%s.%s", d1, m1, y2, d2, m2, y2) end
return ""
end
-- [single: Slovakdigital, Slovakia] URL structure changed after week 34 of 2016
-- Archives: hitparadask.ifpicr.cz has 2006w35–2016w34, hitparada.ifpicr.cz has 2016w43+
-- Gap: weeks 35-42 of 2016 have no archives
-- For 2016w43+: recommend using Slovakdigital2/Slovakia2 instead
-- Returns: "before2016w34", "after2016w34", or "use_new_chart"
function Helpers.slovakia_period(args)
local year, week = tonumber(args.year) or 0, tonumber(args.week) or 0
local yw = year * 100 + week
if yw <= 201634 then return "before2016w34"
elseif yw >= 201643 then return "use_new_chart"
else return "after2016w34" end -- 201635-201642: gap period, use after2016w34 URL
end
-- [album: GermanyComp] Convert DD.MM.YYYY to Unix timestamp (milliseconds)
-- Returns timestamp for Monday 12:00 UTC of that week
function Helpers.germany_timestamp(args)
local date = args.date or ""
local d, m, y = string.match(date, "^(%d%d)%.(%d%d)%.(%d%d%d%d)$")
if not d then return "" end
d, m, y = tonumber(d), tonumber(m), tonumber(y)
local function isLeapYear(yr)
return yr % 4 == 0 and (yr % 100 ~= 0 or yr % 400 == 0)
end
local daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
local days = 0
for yr = 1970, y - 1 do days = days + (isLeapYear(yr) and 366 or 365) end
if isLeapYear(y) then daysInMonth[2] = 29 end
for mo = 1, m - 1 do days = days + daysInMonth[mo] end
days = days + d - 1
local dow = (days + 3) % 7 -- 0=Mon, 6=Sun
days = days - dow -- go back to Monday
return string.format("%.0f", (days * 86400 + 12 * 3600) * 1000)
end
-- Czech/Slovak week IDs cache
local czechWeekIds = nil
local czechMaxKey, czechMaxId = nil, nil
-- [single: Czech Republic, Czechdigital, Slovakia2, Slovakdigital2; album: Czech, Slovakia]
-- Returns weekId for ifpicr.cz URL parameter
-- Data from Module:Music chart/chartdata-czech.json, future weeks calculated from newest entry
-- Supports combined weeks like "51+52" - uses first week's ID
function Helpers.czech_week_id(args)
-- Handle combined weeks like "51+52" - extract first number
local year = tonumber(args.year) or 0
local week = tonumber(string.match(args.week or "", "^%d+")) or 0
if year == 0 or week == 0 then return "" end
-- Load week IDs table on first use
if not czechWeekIds then
czechWeekIds = mw.loadJsonData("Module:Music chart/chartdata-czech.json")
-- Find the newest week (highest key) and its ID
czechMaxKey, czechMaxId = 0, 0
for k, v in pairs(czechWeekIds) do
-- Parse "YYYY-WW" format
local y, w = string.match(k, "^(%d+)-(%d+)$")
if y then
local numKey = tonumber(y) * 100 + tonumber(w)
if numKey > czechMaxKey then
czechMaxKey = numKey
czechMaxId = v
end
end
end
end
-- Format key as YYYY-WW (string with dash to ensure it stays a string in JSON)
local key = string.format("%d-%02d", year, week)
-- Check if in table
if czechWeekIds[key] then
return tostring(czechWeekIds[key])
end
-- For future weeks: calculate from last known
local currentYw = year * 100 + week
if currentYw > czechMaxKey then
local lastYear, lastW = math.floor(czechMaxKey / 100), czechMaxKey % 100
-- Weeks difference, accounting for 51+52 sharing same ID each year
local fullYears = year - lastYear - 1
local weeksDiff = (52 - lastW) + (fullYears * 51) + week
if week >= 52 then weeksDiff = weeksDiff - 1 end -- current year's 51+52
return tostring(czechMaxId + weeksDiff)
end
return ""
end
-- [single: Israel] Returns "WW DD-MM-YY DD-MM-YY" for Media Forest URL
-- Week 1 starts on Sunday closest to Jan 1 (prev Sunday unless prev year had 53 weeks and Jan 1 is Fri)
function Helpers.israel_week(args)
local year, week = tonumber(args.year) or 0, tonumber(args.week) or 0
if year == 0 or week == 0 then return "" end
local function toDays(y, m, d) -- days since Jan 1, 2000
local days = (y - 2000) * 365 + math.floor((y - 1997) / 4) - math.floor((y - 1901) / 100) + math.floor((y - 1601) / 400)
local mdays = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}
days = days + mdays[m] + d - 1
if m > 2 and (y % 4 == 0 and (y % 100 ~= 0 or y % 400 == 0)) then days = days + 1 end
return days
end
local function toDate(days) -- days to DD-MM-YY
local y, m, d = 2000, 1, days + 1
while true do
local ydays = (y % 4 == 0 and (y % 100 ~= 0 or y % 400 == 0)) and 366 or 365
if d <= ydays then break end
d, y = d - ydays, y + 1
end
local mdays = {31, (y % 4 == 0 and (y % 100 ~= 0 or y % 400 == 0)) and 29 or 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
for i = 1, 12 do if d <= mdays[i] then m = i; break end; d = d - mdays[i] end
return string.format("%02d-%02d-%02d", d, m, y % 100)
end
local jan1 = toDays(year, 1, 1)
local dow = (jan1 + 6) % 7 -- 0=Sun
local prevDow = (toDays(year - 1, 1, 1) + 6) % 7
local week1Sun = (dow == 0) and jan1 or ((prevDow == 4 and dow == 5) and (jan1 + 7 - dow) or (jan1 - dow))
local sun = week1Sun + (week - 1) * 7
return string.format("%02d%%20%s%%20%s", week, toDate(sun), toDate(sun + 6))
end
-- Call helper by name (safe wrapper)
function Helpers.call(name, args)
return name and Helpers[name] and Helpers[name](args) or ""
end
-- Get required params for helper
function Helpers.getRequiredParams(helperName)
return helperName and Helpers.params[helperName] or {}
end
--=============================================================================
-- SECTION 5: DATE UTILITIES
-- Parse and format dates. Computed placeholders: {dateDigits}, {dateMDY},
-- {dateDMY}, {dateYMD}, {dateYear}
--=============================================================================
local DateUtils = {}
function DateUtils.digitsOnly(s)
return s and string.gsub(s, "[%-%.%/]", "") or ""
end
function DateUtils.formatMDY(y, m, d)
local month = CONFIG.months[tonumber(m)] or m
return CONFIG.date_format_mdy:gsub("%%M", month):gsub("%%d", tonumber(d)):gsub("%%Y", y)
end
function DateUtils.wrap(date)
if not date or date == "" then return nil end
if CONFIG.date_wrapper then return string.format(CONFIG.date_wrapper, date, date) end
return date
end
-- Parse date string into y, m, d components
function DateUtils.parse(date, chart)
if not date then return nil end
local y, m, d
-- YYYY-MM-DD
y, m, d = string.match(date, "^(%d%d%d%d)%-(%d%d)%-(%d%d)$")
if y then return y, m, d end
-- YYYYMMDD
y, m, d = string.match(date, "^(%d%d%d%d)(%d%d)(%d%d)$")
if y then return y, m, d end
-- YYMMDD
local yy, mm, dd = string.match(date, "^(%d%d)(%d%d)(%d%d)$")
if yy then return "20" .. yy, mm, dd end
-- DD-MM-YYYY or DD.MM.YYYY or DD/MM/YYYY
d, m, y = string.match(date, "^(%d%d)[%-%.%/](%d%d)[%-%.%/](%d%d%d%d)$")
if d then
local mid = tonumber(m)
local isMMDD = (chart and chart.date_format and string.match(chart.date_format, "^MM")) or (mid and mid > 12)
if isMMDD then return y, d, m end -- swap d and m
return y, m, d
end
return nil
end
-- Parse date string and compute all format variants
function DateUtils.compute(date, chart)
if not date then return {} end
local vals = { dateDigits = DateUtils.digitsOnly(date) }
local y, m, d = DateUtils.parse(date, chart)
if not y then return vals end
vals.dateYear = y
vals.dateMDY = DateUtils.formatMDY(y, m, d) -- January 15, 2024
vals.dateDMY = string.format("%s.%s.%s", d, m, y) -- 15.01.2024
vals.dateYMD = string.format("%s-%s-%s", y, m, d) -- 2024-01-15
vals.dateSlash = string.format("%d/%d/%s", tonumber(d), tonumber(m), y) -- 15/1/2024
return vals
end
-- Validate date against chart's expected format
function DateUtils.validate(date, chart, entry)
local dateFormat = (entry and entry.date_format) or chart.date_format
local dateFormatAlt = (entry and entry.date_format_alt) or chart.date_format_alt
if not dateFormat or not date then return true, nil end
local pat = CONFIG.date_patterns[dateFormat]
local patAlt = dateFormatAlt and CONFIG.date_patterns[dateFormatAlt]
if (pat and mw.ustring.match(date, pat)) or (patAlt and mw.ustring.match(date, patAlt)) then return true, nil end
local formats = dateFormatAlt and (dateFormat .. " or " .. dateFormatAlt) or dateFormat
return false, formats
end
-- Validate year format (must be exactly 4 digits)
function DateUtils.validateYear(year)
if not year or year == "" then return true, nil end
if mw.ustring.match(year, "^%d%d%d%d$") then return true, nil end
return false, year
end
-- Validate week format (must be 1-2 digits, or special format like "51+52")
function DateUtils.validateWeek(week)
if not week or week == "" then return true, nil end
-- Allow 1-2 digits (1-52)
if mw.ustring.match(week, "^%d%d?$") then return true, nil end
-- Allow combined weeks like "51+52"
if mw.ustring.match(week, "^%d%d?%+%d%d?$") then return true, nil end
return false, week
end
--=============================================================================
-- SECTION 6: TEMPLATE SUBSTITUTION
-- Replace {placeholder} patterns with values. Handles URL encoding.
-- Encoding applies only to text params: artist, song, album, dvd
--=============================================================================
local Template = {}
-- Replace literal string (not pattern)
function Template.safeReplace(str, search, repl)
local pos = string.find(str, search, 1, true)
while pos do
str = string.sub(str, 1, pos - 1) .. repl .. string.sub(str, pos + #search)
pos = string.find(str, search, pos + #repl, true)
end
return str
end
-- Build reverse alias map: alias -> canonical
local aliasToCanonical = {}
for canonical, aliases in pairs(CONFIG.param_aliases) do
for _, alias in ipairs(aliases) do aliasToCanonical[alias] = canonical end
end
-- Text parameters that need URL encoding
local textParams = { artist = true, song = true, album = true, dvd = true }
-- Substitute all {placeholders} in template
-- encodeConfig: array of operations from chart.encode or entry.encode
function Template.substitute(template, args, chart, encodeConfig)
if not template then return "" end
local result = template
-- Computed date placeholders
for k, v in pairs(DateUtils.compute(args.date, chart)) do
result = Template.safeReplace(result, "{" .. k .. "}", v)
end
-- Helper placeholder
if chart and chart.helper then
result = Template.safeReplace(result, "{helper}", Helpers.call(chart.helper, args))
end
local dateParams = {archivedate = true, ["archive-date"] = true, accessdate = true, ["access-date"] = true}
-- Substitute a single placeholder
local function subst(name, rawValue)
local placeholder = "{" .. name .. "}"
if not string.find(result, placeholder, 1, true) then return end
local encoded
if name == "url" and string.match(rawValue, "^https?://") then
encoded = rawValue -- URL as-is
elseif dateParams[name] then
encoded = DateUtils.wrap(rawValue) or rawValue -- Date with optional wrapper
elseif textParams[name] and type(encodeConfig) == "table" then
encoded = Encoders.encode(rawValue, encodeConfig) -- Text params encoded only when table passed
else
encoded = rawValue -- Everything else as-is
end
result = Template.safeReplace(result, placeholder, encoded)
end
-- Process all args
for key, value in pairs(args) do
if type(value) == "string" and value ~= "" then
local k = tostring(key)
subst(k, value)
-- Substitute aliases
if CONFIG.param_aliases[k] then
for _, alias in ipairs(CONFIG.param_aliases[k]) do subst(alias, value) end
elseif aliasToCanonical[k] then
subst(aliasToCanonical[k], value)
end
end
end
return result
end
--=============================================================================
-- SECTION 7: PARAMETER UTILITIES
-- Extract params from templates, check if known/unused/missing.
--=============================================================================
local Params = {}
-- Computed placeholders map to source param
Params.computed = {
dateDigits = "date", dateDMY = "date", dateYMD = "date", dateMDY = "date",
dateSlash = "date", dateYear = "date", helper = true
}
-- Extract param name from placeholder (handles computed params)
local function resolveParam(placeholder)
local source = Params.computed[placeholder]
if source == true then return nil end -- helper-computed, skip
return source or placeholder
end
-- Add params from template string to list (preserving order) and seen set
function Params.addFromTemplateOrdered(list, seen, str)
if not str then return end
for placeholder in string.gmatch(str, "{([%w%-]+)}") do
local param = resolveParam(placeholder)
if param and not seen[param] then
table.insert(list, param)
seen[param] = true
end
end
end
-- Extract {placeholder} names from template string (returns set)
function Params.extractFromTemplate(str)
local params = {}
if not str then return params end
for placeholder in string.gmatch(str, "{([%w%-]+)}") do
local param = resolveParam(placeholder)
if param then params[param] = true end
end
return params
end
function Params.isKnown(param)
if CONFIG.params.base[param] or CONFIG.params.content[param] or CONFIG.params.manual[param] then
return true
end
-- Check if param is a canonical name with aliases
if CONFIG.param_aliases[param] then return true end
-- Check if param is an alias
for _, aliasList in pairs(CONFIG.param_aliases) do
for _, alias in ipairs(aliasList) do
if alias == param then return true end
end
end
return false
end
function Params.getValue(args, param)
if hasArg(args, param) then return args[param] end
-- If param is canonical, check its aliases
if CONFIG.param_aliases[param] then
for _, alt in ipairs(CONFIG.param_aliases[param]) do
if hasArg(args, alt) then return args[alt] end
end
end
-- If param is an alias, check canonical name
local canonical = aliasToCanonical[param]
if canonical and hasArg(args, canonical) then return args[canonical] end
return nil
end
function Params.hasValue(args, param)
return Params.getValue(args, param) ~= nil
end
-- Add params from template string to a set
function Params.addFromTemplate(set, str)
for k in pairs(Params.extractFromTemplate(str)) do set[k] = true end
end
-- Collect all params used by chart definition
function Params.collectFromChart(chart)
local used = {}
Params.addFromTemplate(used, chart.url)
Params.addFromTemplate(used, chart.url_title)
Params.addFromTemplate(used, chart.ref)
Params.addFromTemplate(used, chart.ref_note)
if chart.multiple then
for _, entry in ipairs(chart.multiple) do
Params.addFromTemplate(used, entry.url)
Params.addFromTemplate(used, entry.url_title)
Params.addFromTemplate(used, entry.ref)
Params.addFromTemplate(used, entry.ref_note)
if entry.when then
for param in string.gmatch(entry.when, "!?([%w_%-]+)") do
if param ~= "helper" and not tonumber(param) then used[param] = true end
end
end
end
end
for _, param in ipairs(Helpers.getRequiredParams(chart.helper)) do used[param] = true end
return used
end
-- Find unknown params (not in CONFIG)
function Params.checkUnknown(allKeys, args)
local unknown = {}
for param in pairs(allKeys) do if not Params.isKnown(param) then table.insert(unknown, tostring(param)) end end
if args[3] and args[3] ~= "M" then table.insert(unknown, "3=" .. tostring(args[3])) end
table.sort(unknown)
return #unknown > 0 and table.concat(unknown, ", ") or nil
end
-- Find unused params (provided but not used by chart)
function Params.checkUnused(args, chart)
local usedByChart = Params.collectFromChart(chart)
local isManualRef = args[3] == "M"
local unused = {}
for param in pairs(args) do
if CONFIG.params.content[param] and not usedByChart[param] then table.insert(unused, tostring(param)) end
if CONFIG.params.manual[param] and not isManualRef then table.insert(unused, tostring(param)) end
end
table.sort(unused)
return #unused > 0 and table.concat(unused, ", ") or nil
end
-- Validate position value
function Params.validatePosition(position)
if not position or position == "" then return false, "empty" end
for _, dash in ipairs(CONFIG.accepted_dashes) do
if position == dash then return true end
end
local num = tonumber(position)
if num and num >= 1 and num <= CONFIG.max_position and num == math.floor(num) then
return true
end
return false, position
end
--=============================================================================
-- SECTION 8: MISSING PARAMS CHECKER
--=============================================================================
local MissingChecker = {}
function MissingChecker.formatParamSet(paramSet)
local list = {}
for param in pairs(paramSet) do table.insert(list, param) end
table.sort(list)
return table.concat(list, "+")
end
function MissingChecker.getMissing(paramSet, args)
local missing = {}
for param in pairs(paramSet) do if not Params.hasValue(args, param) then table.insert(missing, param) end end
table.sort(missing)
return missing
end
function MissingChecker.hasAll(paramSet, args)
for param in pairs(paramSet) do if not Params.hasValue(args, param) then return false end end
return true
end
function MissingChecker.collectRequired(url, title, ref, chart)
local required = {}
if type(url) == "table" then
for _, entry in ipairs(url) do
Params.addFromTemplate(required, entry.url)
Params.addFromTemplate(required, entry.url_title)
end
else
Params.addFromTemplate(required, url)
Params.addFromTemplate(required, title)
end
Params.addFromTemplate(required, ref)
Params.addFromTemplate(required, chart.ref_note)
for _, param in ipairs(Helpers.getRequiredParams(chart.helper)) do required[param] = true end
return required
end
-- Check if v1 is dominated by v2 (v2 is subset of v1 and v1 has more params)
local function isDominated(v1, v2)
for k in pairs(v2) do
if not v1[k] then return false end
end
for k in pairs(v1) do
if not v2[k] then return true end
end
return false
end
-- Check missing for charts with multiple URL variants
function MissingChecker.checkMultiple(chart, args, ref)
local refParams = {}
Params.addFromTemplate(refParams, ref)
Params.addFromTemplate(refParams, chart.ref_note)
local helperReq = Helpers.getRequiredParams(chart.helper)
-- Build list of param sets for each variant
local variants = {}
for _, entry in ipairs(chart.multiple) do
local variantParams = {}
Params.addFromTemplate(variantParams, entry.url or chart.url)
Params.addFromTemplate(variantParams, entry.url_title or chart.url_title)
Params.addFromTemplate(variantParams, entry.ref_note)
for k in pairs(refParams) do variantParams[k] = true end
for _, param in ipairs(helperReq) do variantParams[param] = true end
table.insert(variants, variantParams)
end
-- Check if any variant is fully satisfied
for _, variantParams in ipairs(variants) do
if MissingChecker.hasAll(variantParams, args) then return nil, variantParams end
end
-- Extract unique URL-only param sets (excluding ref params)
local uniqueVariants, seen = {}, {}
for _, variantParams in ipairs(variants) do
local urlOnly = {}
for k in pairs(variantParams) do
if not refParams[k] then urlOnly[k] = true end
end
local key = MissingChecker.formatParamSet(urlOnly)
if key ~= "" and not seen[key] then
seen[key] = true
table.insert(uniqueVariants, urlOnly)
end
end
-- Multiple unique variants: filter out dominated ones and show options
if #uniqueVariants > 1 then
local dominated = {}
for i, v1 in ipairs(uniqueVariants) do
for j, v2 in ipairs(uniqueVariants) do
if i ~= j and isDominated(v1, v2) then
dominated[i] = true
break
end
end
end
local options = {}
for i, v in ipairs(uniqueVariants) do
if not dominated[i] then
table.insert(options, MissingChecker.formatParamSet(v))
end
end
if #options == 1 then return options[1], refParams end
return table.concat(options, " or "), refParams
end
-- Single unique variant: show missing params
if #uniqueVariants == 1 then
local allRequired = {}
for k in pairs(uniqueVariants[1]) do allRequired[k] = true end
for k in pairs(refParams) do allRequired[k] = true end
local missing = MissingChecker.getMissing(allRequired, args)
return #missing > 0 and table.concat(missing, ", ") or nil, uniqueVariants[1]
end
-- No URL params, only ref params
local missingRef = MissingChecker.getMissing(refParams, args)
return #missingRef > 0 and table.concat(missingRef, ", ") or nil, refParams
end
function MissingChecker.check(chart, args, url, title, ref)
if chart.multiple then return MissingChecker.checkMultiple(chart, args, ref) end
local required = MissingChecker.collectRequired(url, title, ref, chart)
local missing = MissingChecker.getMissing(required, args)
return #missing > 0 and table.concat(missing, ", ") or nil, required
end
--=============================================================================
-- SECTION 9: ENTRY SELECTION
--=============================================================================
local EntrySelector = {}
-- Comparison operators lookup table
local COMPARISON_OPS = {
["<="] = function(a, b) return a <= b end,
[">="] = function(a, b) return a >= b end,
["<"] = function(a, b) return a < b end,
[">"] = function(a, b) return a > b end,
}
-- Check single condition
local function checkSingleCondition(cond, args)
cond = mw.text.trim(cond)
-- Negation check: !param
if string.sub(cond, 1, 1) == "!" then
local param = string.sub(cond, 2)
return not Params.getValue(args, param)
end
-- Try comparison operators in order of pattern length
local param, op, val
for _, operator in ipairs({"<=", ">=", "<", ">"}) do
param, op, val = string.match(cond, "^([%w_%-]+)(" .. operator .. ")(.+)$")
if param then break end
end
if param and op and val then
local argVal
local paramValue = Params.getValue(args, param)
if (param == "date" or param == "archivedate" or param == "archive-date") and paramValue then
argVal = tonumber(string.sub(paramValue, 1, 4)) or tonumber(string.match(paramValue, "(%d%d%d%d)")) or 0
else
argVal = tonumber(paramValue) or 0
end
return COMPARISON_OPS[op](argVal, tonumber(val) or 0)
end
-- Equality check: param=value
local key, eqVal = string.match(cond, "^([^=]+)=(.+)$")
if key and eqVal then return Params.getValue(args, key) == eqVal end
-- Existence check: param (non-empty)
return Params.getValue(args, cond) ~= nil
end
-- Check if "when" condition matches args
function EntrySelector.matchesCondition(when, args, helperValue)
if not when or when == "" then return true end
local helperVal = string.match(when, "^helper=(.+)$")
if helperVal then return helperValue == helperVal end
if string.find(when, "|", 1, true) then
for param in string.gmatch(when, "([^|]+)") do
if checkSingleCondition(param, args) then return true end
end
return false
end
for param in string.gmatch(when, "([^,]+)") do
if not checkSingleCondition(param, args) then return false end
end
return true
end
-- Select URL entry from chart.multiple based on args
-- Returns table with: url, url_title, ref, lang, provider, chart, entry, encode, helperValue
function EntrySelector.select(chart, args)
-- Default result from chart base values
local result = {
url = chart.url,
url_title = chart.url_title,
ref = chart.ref,
lang = chart.lang,
provider = chart.provider,
chart = chart.chart,
encode = chart.encode,
entry = nil,
helperValue = nil
}
if not chart.multiple then return result end
local helperValue = chart.helper and Helpers.call(chart.helper, args) or nil
result.helperValue = helperValue
-- Helper to apply entry overrides with fallback to chart defaults
local function applyEntry(entry)
result.url = entry.url or chart.url
result.url_title = entry.url_title or chart.url_title
result.ref = entry.ref or chart.ref
result.lang = entry.lang or chart.lang
result.provider = entry.provider or chart.provider
result.chart = entry.chart or chart.chart
result.encode = entry.encode or chart.encode
result.entry = entry
end
if chart.combine then
local entries = {}
for _, entry in ipairs(chart.multiple) do
if EntrySelector.matchesCondition(entry.when, args, helperValue) then
table.insert(entries, {
url = entry.url or chart.url,
url_title = entry.url_title or chart.url_title,
ref = entry.ref,
ref_note = entry.ref_note,
lang = entry.lang,
encode = entry.encode or chart.encode
})
end
end
if #entries > 0 then
result.url = entries
result.url_title = nil
end
else
for _, entry in ipairs(chart.multiple) do
if EntrySelector.matchesCondition(entry.when, args, helperValue) then
applyEntry(entry)
break
end
end
end
return result
end
--=============================================================================
-- SECTION 10: OUTPUT BUILDERS
--=============================================================================
local Builder = {}
-- Build wikitext link from URL and title
-- encodeConfig: table of operations for text params (empty {} = default space-plus), nil = no encoding
function Builder.link(url, title, args, chart, encodeConfig)
if not url or url == "" then return "" end
if string.sub(url, 1, 1) == "[" then return '"' .. Template.substitute(url, args, chart, nil) .. '"' end
local encodedUrl = Template.substitute(url, args, chart, encodeConfig or {})
local linkTitle = Template.substitute(title or "", args, chart, nil)
return linkTitle ~= "" and ('"[' .. encodedUrl .. " " .. linkTitle .. ']"') or ("[" .. encodedUrl .. "]")
end
-- Build chart display name with provider
function Builder.chartName(chartName, provider, chartKey)
local name = chartName or chartKey
return provider and provider ~= "" and (name .. " (" .. provider .. ")") or name
end
-- Build reference name
function Builder.refName(chartType, chartKey, args, chart, selectedEntry)
if args.refname then return args.refname end
local prefix = CONFIG.ref_prefixes[chartType] or "sc"
local suffix = string.match(chartType, "^year%-end") and (args.year or "") or (args.artist or "")
local defaultRefname = prefix .. "_" .. chartKey .. "_" .. suffix
local format = (selectedEntry and selectedEntry.refname_format) or (chart and chart.refname_format)
if not format then return defaultRefname end
local refname = format
for key, value in pairs(args) do
if type(value) == "string" and value ~= "" then
refname = refname:gsub("{" .. key .. "|[^}]*}", value):gsub("{" .. key .. "}", value)
end
end
refname = refname:gsub("{[^}]+|([^}]*)}", "%1"):gsub("{[^}]+}", "")
return #refname < 5 and defaultRefname or refname
end
-- Build reference content
function Builder.refContent(chart, args, urlResult, lang, refText, isYearEnd, selectedEntry)
local accessDate = DateUtils.wrap(Params.getValue(args, "access-date"))
local archiveDate = DateUtils.wrap(Params.getValue(args, "archive-date"))
local pubDate = DateUtils.wrap(Params.getValue(args, "publish-date"))
local archiveUrl = Params.getValue(args, "archive-url")
local refNote = (selectedEntry and selectedEntry.ref_note) or chart.ref_note
-- Combine mode: bullet list
if type(urlResult) == "table" and chart.combine then
local bullets, hasEntryRef = {}, false
for _, entry in ipairs(urlResult) do
local link = Builder.link(entry.url, entry.url_title, args, chart, entry.encode)
if link ~= "" then
local entryLang = entry.lang or lang
local linkWithLang = entryLang and (link .. " " .. entryLang) or link
local parts = {linkWithLang}
if entry.ref then
parts[#parts + 1] = entry.ref
hasEntryRef = true
if accessDate then parts[#parts + 1] = string.format(CONFIG.text.retrieved, accessDate) end
end
if entry.ref_note then parts[#parts + 1] = Template.substitute(entry.ref_note, args, chart, nil) end
local line = table.concat(parts, ". ")
if not line:match("%.$") then line = line .. "." end
bullets[#bullets + 1] = "*" .. line
end
end
local prefix = refNote and Template.substitute(refNote, args, chart, nil) or ""
local result = prefix ~= "" and (prefix .. "\n" .. table.concat(bullets, "\n")) or table.concat(bullets, "\n")
-- Add shared ref if no entry-level refs
if not hasEntryRef and refText and refText ~= "" then
local sharedRef = refText
if accessDate then sharedRef = sharedRef .. ". " .. string.format(CONFIG.text.retrieved, accessDate) end
if not sharedRef:match("%.$") then sharedRef = sharedRef .. "." end
result = result .. "\n" .. sharedRef
end
return result
end
-- Standard format
local parts = {}
local urlLang = ""
if type(urlResult) == "table" then
for _, entry in ipairs(urlResult) do
local link = Builder.link(entry.url, entry.url_title, args, chart)
if link ~= "" then urlLang = urlLang .. (urlLang ~= "" and " " or "") .. link end
end
elseif urlResult and urlResult ~= "" then
urlLang = urlResult
end
if lang then urlLang = urlLang .. " " .. lang end
if urlLang ~= "" then table.insert(parts, urlLang) end
if refText then table.insert(parts, refText) end
if pubDate and isYearEnd then table.insert(parts, pubDate) end
if archiveUrl and archiveDate and not (refText and string.find(refText, "Archived", 1, true)) then
table.insert(parts, string.format(CONFIG.text.archived, archiveUrl, archiveDate))
end
if refNote then table.insert(parts, Template.substitute(refNote, args, chart, nil)) end
local result = table.concat(parts, ". ")
local skipDot = result:match("%.$") or result:match("{{cite Kent")
if result ~= "" and not skipDot then result = result .. "." end
if accessDate then result = result .. (result ~= "" and " " or "") .. string.format(CONFIG.text.retrieved, accessDate) end
if chart.ref_suffix then result = result .. " " .. Template.substitute(chart.ref_suffix, args, chart, nil) end
return result
end
-- Build URL result (with or without substitution based on missing params)
function Builder.urlResult(url, urlTitle, args, chart, hasMissing, encodeConfig)
if type(url) == "table" then return url end
if hasMissing then
if not url or url == "" then return "" end
if string.sub(url, 1, 1) == "[" then return '"' .. url .. '"' end
return urlTitle and ('"[' .. url .. " " .. urlTitle .. ']"') or ("[" .. url .. "]")
end
return Builder.link(url, urlTitle, args, chart, encodeConfig)
end
-- Build note text
function Builder.noteText(args)
return hasArg(args, "note") and string.format(CONFIG.note_format, args.note) or ""
end
-- Build ref tag
function Builder.refTag(frame, refContent, refname, refgroup)
if not refContent or refContent == "" then return "" end
local refAttrs = {name = refname}
if refgroup and refgroup ~= "" then refAttrs.group = refgroup end
local processed = string.find(refContent, "{{", 1, true) and frame:preprocess(refContent) or refContent
return frame:extensionTag('ref', processed, refAttrs)
end
-- Build final output row
function Builder.outputRow(args, chartName, warnings, errorInline, refTag, noteText, position, cats)
local cell = args.rowheader == "true" and CONFIG.cell_header or CONFIG.cell_normal
return string.format('%s%s%s%s%s%s\n|%s%s%s',
cell, chartName, warnings, errorInline, refTag, noteText,
CONFIG.position_style, position, cats)
end
--=============================================================================
-- SECTION 11: CATEGORIES
--=============================================================================
local Categories = {}
function Categories.shouldCategorize()
return mw.title.getCurrentTitle().namespace == 0
end
function Categories.build(chartType, chartKey, chart, args, position)
if not Categories.shouldCategorize() then return "" end
local t = getTypeName(chartType)
local cats = {catLink(CONFIG.categories.usage, t, chartKey)}
if chart.defunct then table.insert(cats, catLinkSort(CONFIG.categories.defunct, chartKey, t)) end
if args[3] == "M" then table.insert(cats, catLinkSort(CONFIG.categories.manual_ref, chartKey, t)) end
if not hasArg(args, "artist") then table.insert(cats, catLink(CONFIG.categories.without_artist, t)) end
if string.match(chartType, "single") and not hasArg(args, "song") then
table.insert(cats, catLink(CONFIG.categories.without_song, t))
end
if string.match(chartType, "album") and not hasArg(args, "album") and not hasArg(args, "dvd") then
table.insert(cats, catLink(CONFIG.categories.without_album, t))
end
if hasArg(args, "refname") then table.insert(cats, catLink(CONFIG.categories.named_ref, t)) end
-- Category conditions from chart definition
if chart.category_conditions then
local oldPosition = args.position
args.position = position
local helperValue = chart.helper and Helpers.call(chart.helper, args) or nil
for _, cond in ipairs(chart.category_conditions) do
if EntrySelector.matchesCondition(cond.when, args, helperValue) and cond.category then
table.insert(cats, "[[" .. CONFIG.category_prefix .. cond.category .. "]]")
end
end
args.position = oldPosition
elseif chart.number_one_category and tonumber(position) == 1 then
table.insert(cats, "[[" .. CONFIG.category_prefix .. chart.number_one_category .. "]]")
end
if chart.track_param and not hasArg(args, chart.track_param) then
table.insert(cats, catLink(CONFIG.categories.track_param, t, chartKey, chart.track_param))
end
return table.concat(cats)
end
--=============================================================================
-- SECTION 12: ERROR HANDLING
--=============================================================================
local Errors = {}
local ERROR_SPAN = '<span style="color:red;">' .. CONFIG.errors.prefix .. '%s</span>'
local WARNING_SPAN = '<span style="color:orange;">%s</span>'
-- Format error span with chartKey and message
local function errorSpan(chartKey, msg)
return string.format(ERROR_SPAN, chartKey, msg)
end
function Errors.make(chartType, chartKey, msg)
local t = getTypeName(chartType)
local cat = Categories.shouldCategorize() and catLink(CONFIG.categories.missing_params, t) or ""
return errorSpan(chartKey, msg) .. cat
end
function Errors.inline(chartKey, param, extra)
local msg
if param == "invalid_date" then msg = string.format(CONFIG.errors.invalid_date, extra or "YYYY-MM-DD")
elseif param == "invalid_year" then msg = string.format(CONFIG.errors.invalid_year, extra or "?")
elseif param == "invalid_week" then msg = string.format(CONFIG.errors.invalid_week, extra or "?")
elseif param == "unknown chart" then msg = string.format(CONFIG.errors.unknown_chart, chartKey)
else msg = string.format(CONFIG.errors.missing_params, param) end
return errorSpan(chartKey, msg)
end
function Errors.warning(text)
return string.format(WARNING_SPAN, text)
end
function Errors.checkUnsubstituted(output)
local found = {}
for param in string.gmatch(output, "{([%w%-]+)}") do found[param] = true end
local list = {}
for ph in pairs(found) do table.insert(list, ph) end
if #list > 0 then table.sort(list); return table.concat(list, ", ") end
return nil
end
function Errors.isPreview()
local frame = mw.getCurrentFrame()
return frame and frame:preprocess("{{REVISIONID}}") == ""
end
--=============================================================================
-- SECTION 13: DATA LOADING
--=============================================================================
local dataCache = {}
local function loadData(chartType)
if not CONFIG.type_names[chartType] then return nil end
if dataCache[chartType] then return dataCache[chartType] end
local grouped = mw.loadJsonData(string.format(CONFIG.json_path, chartType))
local flat = {}
for country, charts in pairs(grouped) do
if string.sub(country, 1, 1) ~= "_" then
for key, info in pairs(charts) do
local copy = {}
for k, v in pairs(info) do copy[k] = v end
if not copy.chart then copy.chart = country end
flat[key] = copy
end
end
end
dataCache[chartType] = flat
return flat
end
--=============================================================================
-- SECTION 14: MAIN ENTRY POINT
--=============================================================================
local function parseArgs(frame)
local args = {}
local allKeys = CONFIG.check_unknown_params and {} or nil
for k, v in pairs(frame.args) do
if allKeys then allKeys[k] = true end
if v and v ~= "" then args[k] = mw.text.trim(v) end
end
for k, v in pairs(frame:getParent().args) do
if allKeys then allKeys[k] = true end
if v and v ~= "" then args[k] = mw.text.trim(v) end
end
return args, allKeys
end
local function resolveChart(data, chartKey)
local chart = data[chartKey]
if not chart then return nil end
return chart.alias_for and data[chart.alias_for] or chart
end
-- Check unknown params and return warning string and category string
local function checkUnknownParams(allKeys, args, chartKey, chartType, shouldCat)
if not CONFIG.check_unknown_params then return "", "" end
local unknownParams = Params.checkUnknown(allKeys, args)
if not unknownParams then return "", "" end
local warning = ""
if Errors.isPreview() then
warning = Errors.warning(string.format(CONFIG.warnings.unknown_params, chartKey, unknownParams))
end
local cat = ""
local typeName = getTypeName(chartType)
if shouldCat == nil then shouldCat = Categories.shouldCategorize() end
if shouldCat then
cat = catLinkSort(CONFIG.categories.unknown_params, chartKey .. ": " .. unknownParams, string.lower(typeName))
end
return warning, cat
end
-- Check if param is used in URL or helper (critical error) vs only in ref (warning)
local function checkParamInUrl(chart, url, paramName)
local urlParams = Params.extractFromTemplate(url)
if chart.multiple then
for _, entry in ipairs(chart.multiple) do
for k in pairs(Params.extractFromTemplate(entry.url)) do urlParams[k] = true end
end
end
if urlParams[paramName] then return true end
for _, param in ipairs(Helpers.getRequiredParams(chart.helper)) do
if param == paramName then return true end
end
return false
end
-- Handle manual ref mode (3=M)
local function handleManualMode(frame, args, allKeys, chartType, chartKey, chart, typeName, shouldCat)
if not hasArg(args, "url") or not hasArg(args, "title") then
local errCat = shouldCat and catLinkSort(CONFIG.categories.manual_missing_url_title, chartKey, string.lower(typeName)) or ""
return CONFIG.cell_normal .. errorSpan(chartKey, CONFIG.errors.manual_missing_url_title) .. errCat
end
-- Build cite news template using table for cleaner construction
local citeParams = {
"{{cite news",
"|url=" .. args.url,
"|title=" .. args.title,
}
local optionalParams = {
{args.work, "|work="},
{args.publisher, "|publisher="},
{args.location, "|location="},
{args.date, "|date="},
{Params.getValue(args, "access-date"), "|access-date="},
{Params.getValue(args, "archive-url"), "|archive-url="},
{Params.getValue(args, "archive-date"), "|archive-date="},
{args["url-status"], "|url-status="},
}
for _, param in ipairs(optionalParams) do
if param[1] then table.insert(citeParams, param[2] .. param[1]) end
end
table.insert(citeParams, "}}")
local cite = table.concat(citeParams)
-- Manual refs are anonymous by default (like original template), unless refname explicitly provided
local refname = args.refname -- nil if not provided = anonymous ref
local refTag = Builder.refTag(frame, frame:preprocess(cite), refname, args.refgroup)
local chartName = Builder.chartName(chart.chart, chart.provider, chartKey)
local position = args.position or args[2]
local noteText = Builder.noteText(args)
local unknownWarning, unknownCat = checkUnknownParams(allKeys, args, chartKey, chartType, shouldCat)
local cats = Categories.build(chartType, chartKey, chart, args, position) .. unknownCat
return Builder.outputRow(args, chartName, unknownWarning, "", refTag, noteText, position, cats)
end
function p.main(frame)
local args, allKeys = parseArgs(frame)
local chartType = args.type or CONFIG.default_type
local chartKey = args.chart or args[1]
local position = args.position or args[2]
local typeName = getTypeName(chartType)
local shouldCat = Categories.shouldCategorize()
if not chartKey or chartKey == "" then
return CONFIG.cell_normal .. Errors.make(chartType, "?", CONFIG.errors.missing_chart)
end
if not position or position == "" then
return CONFIG.cell_normal .. Errors.make(chartType, chartKey, CONFIG.errors.missing_position)
end
local positionValid, positionErr = Params.validatePosition(position)
if not positionValid then
local errMsg = string.format(CONFIG.errors.invalid_position, positionErr, CONFIG.max_position)
local cat = shouldCat and catLink(CONFIG.categories.invalid_position, typeName) or ""
return CONFIG.cell_normal .. errorSpan(chartKey, errMsg) .. cat
end
local data = loadData(chartType)
local chart = resolveChart(data, chartKey)
if not chart then
local cat = shouldCat and catLinkSort(CONFIG.categories.unknown_chart, chartKey, typeName) or ""
return CONFIG.cell_normal .. errorSpan(chartKey, string.format(CONFIG.errors.unknown_chart, chartKey)) .. cat
end
if args[3] == "M" then
return handleManualMode(frame, args, allKeys, chartType, chartKey, chart, typeName, shouldCat)
end
local isYearEnd = string.match(chartType, "^year%-end") ~= nil
local sel = EntrySelector.select(chart, args)
-- Check if helper recommends using a different chart
if sel.helperValue == "use_new_chart" then
local altChart = Helpers.alternatives[chartKey]
local altText = altChart or (chartKey .. "2")
local errMsg = string.format(CONFIG.errors.use_new_chart, altText)
local cat = shouldCat and catLinkSort(CONFIG.categories.unknown_chart, chartKey, typeName) or ""
return CONFIG.cell_normal .. errorSpan(chartKey, errMsg) .. cat
end
if sel.lang and string.find(sel.lang, "{{", 1, true) then
sel.lang = frame:preprocess(sel.lang)
end
local errorInline, errorInRef, extraCats = "", "", ""
-- URL validation
if chart.url_validation and hasArg(args, "url") then
if not string.find(args.url, chart.url_validation, 1, true) then
local msg = string.format(CONFIG.errors.url_validation, chart.url_validation)
errorInline, errorInRef = setErrorDisplay(errorSpan(chartKey, msg), errorInline, errorInRef)
end
end
-- Missing params
local missing = MissingChecker.check(chart, args, sel.url, sel.url_title, sel.ref)
if missing and errorInline == "" then
local errMsg = Errors.inline(chartKey, missing)
errorInline, errorInRef = setErrorDisplay(errMsg, errorInline, errorInRef)
if shouldCat then
extraCats = extraCats .. catLinkSort(CONFIG.categories.missing_params, chartKey, typeName)
end
end
-- Unused params
local unusedWarning = ""
if CONFIG.check_unused_params then
local unusedParams = Params.checkUnused(args, chart)
if unusedParams then
if shouldCat then
extraCats = extraCats .. catLinkSort(CONFIG.categories.unused_params, chartKey, typeName)
end
if Errors.isPreview() then
unusedWarning = Errors.warning(string.format(CONFIG.warnings.unused_params, chartKey, unusedParams))
end
end
end
-- Unknown params
local unknownWarning, unknownCat = checkUnknownParams(allKeys, args, chartKey, chartType, shouldCat)
extraCats = extraCats .. unknownCat
-- Date validation
if args.date then
local valid, formats = DateUtils.validate(args.date, chart, sel.entry)
if not valid then
if checkParamInUrl(chart, sel.url, "date") then
local dateErr = Errors.inline(chartKey, "invalid_date", formats)
errorInline, errorInRef = setErrorDisplay(dateErr, errorInline, errorInRef)
elseif Errors.isPreview() then
errorInline = errorInline .. Errors.warning(string.format(CONFIG.warnings.invalid_date_ref, formats))
end
end
end
-- Year/week format validation
local dateParamChecks = {
{param = "year", validator = DateUtils.validateYear, errorKey = "invalid_year"},
{param = "week", validator = DateUtils.validateWeek, errorKey = "invalid_week"},
}
for _, check in ipairs(dateParamChecks) do
if args[check.param] then
local valid, badValue = check.validator(args[check.param])
if not valid then
if checkParamInUrl(chart, sel.url, check.param) then
local err = Errors.inline(chartKey, check.errorKey, badValue)
errorInline, errorInRef = setErrorDisplay(err, errorInline, errorInRef)
if shouldCat then
extraCats = extraCats .. catLinkSort(CONFIG.categories.unsubstituted, chartKey .. ": " .. check.param, typeName)
end
elseif Errors.isPreview() then
errorInline = errorInline .. Errors.warning(string.format(CONFIG.errors[check.errorKey], badValue))
end
end
end
end
local urlResult = Builder.urlResult(sel.url, sel.url_title, args, chart, missing ~= nil, sel.encode)
local refText = missing and (sel.ref or "") or Template.substitute(sel.ref or "", args, chart, nil)
local refContent = Builder.refContent(chart, args, urlResult, sel.lang, refText, isYearEnd, sel.entry)
if errorInRef ~= "" then refContent = errorInRef .. " " .. refContent end
local chartName = Builder.chartName(sel.chart, sel.provider, chartKey)
local refname = Builder.refName(chartType, chartKey, args, chart, sel.entry)
local refTag = Builder.refTag(frame, refContent, refname, args.refgroup)
local noteText = Builder.noteText(args)
local cats = Categories.build(chartType, chartKey, chart, args, position) .. extraCats
local warnings = unusedWarning .. unknownWarning
local output = Builder.outputRow(args, chartName, warnings, errorInline, refTag, noteText, position, cats)
-- Check for unsubstituted placeholders
local unsubstituted = Errors.checkUnsubstituted(output .. refContent)
if unsubstituted then
if errorInline == "" then
output = Builder.outputRow(args, chartName, warnings, Errors.inline(chartKey, unsubstituted), refTag, noteText, position, cats)
end
if shouldCat then
output = output .. catLinkSort(CONFIG.categories.unsubstituted, chartKey .. ": " .. unsubstituted, typeName)
end
end
return output
end
--=============================================================================
-- SECTION 15: UTILITY FUNCTIONS
--=============================================================================
function p.chartExists(frame)
local chartType = frame.args.type or CONFIG.default_type
local chartKey = frame.args.chart or frame.args[1]
if not chartKey then return "0" end
return loadData(chartType)[chartKey] and "1" or "0"
end
--=============================================================================
-- SECTION 16: SHOW CHARTS TABLE GENERATOR
--=============================================================================
local ShowCharts = {}
function ShowCharts.countArray(arr)
if not arr then return 0 end
local count = 0
for _ in ipairs(arr) do count = count + 1 end
return count
end
function ShowCharts.chartIdCell(chartKey)
return "<code>" .. chartKey .. "</code>"
end
function ShowCharts.usageCountLink(chartKey, typeName)
local cat = string.format(CONFIG.categories.usage, typeName, chartKey)
local count = mw.site.stats.pagesInCategory(cat, "pages") or 0
return "[[:" .. CONFIG.category_prefix .. cat .. "|" .. count .. "]]"
end
function ShowCharts.buildParamsStr(entry, chart)
local paramList = {}
local seen = {}
if entry.when then
for part in string.gmatch(entry.when, "[^,|]+") do
local param = string.match(mw.text.trim(part), "^!?([%w_%-]+)")
if param and not seen[param] then
table.insert(paramList, param)
seen[param] = true
end
end
end
Params.addFromTemplateOrdered(paramList, seen, entry.url or chart.url)
Params.addFromTemplateOrdered(paramList, seen, entry.url_title or chart.url_title)
Params.addFromTemplateOrdered(paramList, seen, chart.ref)
Params.addFromTemplateOrdered(paramList, seen, chart.ref_note)
Params.addFromTemplateOrdered(paramList, seen, entry.ref_note)
for _, param in ipairs(Helpers.getRequiredParams(chart.helper)) do
if not seen[param] then
table.insert(paramList, param)
seen[param] = true
end
end
local dateFormat = entry.date_format or chart.date_format
if dateFormat then
local displayFormat = dateFormat:gsub("–", "-"):gsub("~", "-")
for i, param in ipairs(paramList) do
if param == "date" then
paramList[i] = "date <span style=\"font-size:85%\">[" .. displayFormat .. "]</span>"
break
end
end
end
return #paramList > 0 and table.concat(paramList, ", ") or "—"
end
function ShowCharts.isRefOnlyOverride(entry, chart)
return not (entry.url and entry.url ~= chart.url) and not (entry.url_title and entry.url_title ~= chart.url_title)
end
local function escapeForTable(str)
if not str then return nil end
local links = {}
str = str:gsub("%[%[(.-)%]%]", function(l)
links[#links + 1] = "[[" .. l .. "]]"
return "\1" .. #links .. "\1"
end)
str = str:gsub("|", "|")
return str:gsub("\1(%d+)\1", function(n) return links[tonumber(n)] end)
end
function ShowCharts.buildCombineBullet(entry, chart, sampleArgs)
local link = escapeForTable(Builder.link(entry.url or chart.url, entry.url_title or chart.url_title, sampleArgs, chart))
local lang = entry.lang or chart.lang
if lang then link = link .. " " .. mw.text.nowiki(lang) end
-- Only add entry-level ref, not chart-level (chart.ref is added separately below bullets)
if entry.ref then link = link .. ". " .. escapeForTable(entry.ref) end
if entry.ref_note then link = link .. ". " .. escapeForTable(Template.substitute(entry.ref_note, sampleArgs, chart, nil)) end
-- Add trailing dot
if not link:match("%.$") then link = link .. "." end
return link
end
function ShowCharts.buildSampleOutput(entry, chart, sampleArgs)
local function sub(s)
if not s then return nil end
return escapeForTable(Template.substitute(s, sampleArgs, chart, nil))
end
local url = entry.url or chart.url
local urlTitle = entry.url_title or chart.url_title
local lang = entry.lang or chart.lang
local ref = entry.ref or chart.ref
local refNote = entry.ref_note or chart.ref_note
local hasCondition = ShowCharts.isRefOnlyOverride(entry, chart) and entry.when
local parts = {}
if url then
local link = escapeForTable(Builder.link(url, urlTitle, sampleArgs, chart))
if link and link ~= "" then
if lang then link = link .. " " .. mw.text.nowiki(lang) end
parts[#parts + 1] = link
end
end
if hasCondition then
local overrides = {}
if entry.ref_note then overrides[#overrides + 1] = sub(entry.ref_note) end
if entry.ref and entry.ref ~= chart.ref then overrides[#overrides + 1] = "ref: " .. sub(entry.ref) end
if entry.lang and entry.lang ~= chart.lang then overrides[#overrides + 1] = "lang: " .. mw.text.nowiki(entry.lang) end
if #overrides > 0 then
parts[#parts + 1] = "[" .. entry.when .. " → " .. table.concat(overrides, "; ") .. "]"
end
end
if ref then parts[#parts + 1] = sub(ref) end
local refNoteShownInCondition = hasCondition and entry.ref_note
if refNote and not refNoteShownInCondition then
parts[#parts + 1] = sub(refNote)
end
if #parts == 0 then return "''(no url)''" end
local out = table.concat(parts, ". ")
if not out:match("%.$") then out = out .. "." end
if chart.ref_suffix then out = out .. " " .. sub(chart.ref_suffix) end
return out
end
function ShowCharts.getSampleArgs(chart)
local sampleArgs = {
date = "2024-01-15", year = "2024", week = "3",
chartid = "12345", songid = "67890", artistid = "11111",
dvd = "Sample DVD", startdate = "01/01/2024", enddate = "07/01/2024",
id = "123", page = "42", title = "Sample Title",
}
local dateFormats = {
["YYYYMMDD"] = "20240115",
["YYMMDD"] = "110115",
["DD-MM-YYYY"] = "15-01-2024",
["MM-DD-YYYY"] = "01-15-2024",
["DD.MM.YYYY"] = "15.01.2024",
["YYYYMMDD-YYYYMMDD"] = "20251219-20251225",
["DD.MM.YYYY–DD.MM.YYYY"] = "19.12.2025–25.12.2025",
["YYYY.MM.DD–YYYY.MM.DD"] = "2025.12.19–2025.12.25",
}
if chart.date_format and dateFormats[chart.date_format] then
sampleArgs.date = dateFormats[chart.date_format]
end
return sampleArgs
end
function ShowCharts.rowspanCell(content, rowspan, style)
return string.format('%srowspan="%d" | %s', style or "", rowspan, content or "")
end
function ShowCharts.categoryLink(catName)
if not catName then return "—" end
return "[[:" .. CONFIG.category_prefix .. catName .. "|" .. catName .. "]]"
end
function ShowCharts.formatGroup(name, frame)
if string.find(name, "%[no wrap%]") then
return (string.gsub(name, "%s*%[no wrap%]%s*", ""))
end
if CONFIG.group_wrapper then
local wrapped = string.format(CONFIG.group_wrapper, name, name)
if string.find(wrapped, "{{", 1, true) then
return frame:preprocess(wrapped)
end
return wrapped
end
return name
end
function ShowCharts.isMvChart(chartKey)
return string.sub(chartKey, -2) == "MV"
end
function ShowCharts.buildRows(grouped, countries, opts, frame, mvFilter)
local rows = {}
for _, country in ipairs(countries) do
local keys = {}
for k in pairs(grouped[country]) do
if mvFilter == nil or ShowCharts.isMvChart(k) == mvFilter then table.insert(keys, k) end
end
if #keys > 0 then
if opts.sortAlpha then table.sort(keys) end
-- Count rows for this country (for rowspan)
local countryRowCount = 0
for _, k in ipairs(keys) do
local c = grouped[country][k]
if c.multiple and not c.alias_for and not c.combine then
countryRowCount = countryRowCount + ShowCharts.countArray(c.multiple)
else
countryRowCount = countryRowCount + 1
end
end
local countryRowsEmitted = 0
for _, chartKey in ipairs(keys) do
local chart = grouped[country][chartKey]
local isFirstCountryRow = (countryRowsEmitted == 0)
local sampleArgs = ShowCharts.getSampleArgs(chart)
-- Base style for defunct charts (without pipe)
local style = chart.defunct and 'style="background:#ffebee;" ' or ""
local docNote = chart.doc_note or ""
-- Helper to build row start with country cell
-- First row of country: "| rowspan=N | Country || "
-- Other rows: "| " (start new row cell)
local function countryCell()
if isFirstCountryRow then
isFirstCountryRow = false
return string.format('| rowspan="%d" | %s || ', countryRowCount, ShowCharts.formatGroup(country, frame))
end
return "| "
end
-- Cell style prefix: "style=... | " for defunct, empty for normal
local cs = style ~= "" and (style .. "| ") or ""
if chart.alias_for then
local aliasStyle = 'style="background:#fffde7;" '
local aliasCs = aliasStyle .. "| "
local colspan = 2 + (opts.showParams and 1 or 0) + (opts.showCategories and 1 or 0) + (opts.showRef and 1 or 0)
local aliasContent = string.format('colspan="%d" style="background:#fffde7; text-align:center; color:#666;" | \'\' → %s\'\'', colspan, chart.alias_for)
local row = countryCell() .. aliasCs .. ShowCharts.chartIdCell(chartKey)
if opts.showUses then row = row .. " || " .. aliasCs .. ShowCharts.usageCountLink(chartKey, opts.typeName) end
row = row .. " || " .. aliasContent
table.insert(rows, {content = row, note = docNote, noteStyle = aliasStyle})
countryRowsEmitted = countryRowsEmitted + 1
elseif chart.multiple and chart.combine then
local row = countryCell() .. cs .. ShowCharts.chartIdCell(chartKey) .. (chart.defunct and " ''(defunct)''" or "")
if opts.showUses then row = row .. " || " .. cs .. ShowCharts.usageCountLink(chartKey, opts.typeName) end
row = row .. " || " .. cs .. (chart.chart or country)
row = row .. " || " .. cs .. (chart.provider or "—")
if opts.showParams then
local allParams = {}
for _, ent in ipairs(chart.multiple) do
local paramStr = ShowCharts.buildParamsStr(ent, chart)
if paramStr ~= "" then allParams[paramStr] = true end
end
local list = {}; for param in pairs(allParams) do table.insert(list, param) end
row = row .. " || " .. cs .. table.concat(list, ", ")
end
if opts.showCategories then row = row .. " || " .. cs .. ShowCharts.categoryLink(chart.number_one_category) end
if opts.showRef then
local bullets = {}
for _, entry in ipairs(chart.multiple) do table.insert(bullets, "• " .. ShowCharts.buildCombineBullet(entry, chart, sampleArgs)) end
local prefix = chart.ref_note and escapeForTable(Template.substitute(chart.ref_note, sampleArgs, chart, nil)) or ""
local content = prefix ~= "" and (prefix .. "<br>" .. table.concat(bullets, "<br>")) or table.concat(bullets, "<br>")
if chart.ref then content = content .. "<br>" .. escapeForTable(Template.substitute(chart.ref, sampleArgs, chart, nil)) .. "." end
row = row .. " || " .. cs .. content
end
table.insert(rows, {content = row, note = docNote, noteStyle = style})
countryRowsEmitted = countryRowsEmitted + 1
elseif chart.multiple then
local entryCount = ShowCharts.countArray(chart.multiple)
for j, entry in ipairs(chart.multiple) do
local isFirst = (j == 1)
local entryNote = entry.doc_note or docNote
local row = countryCell() .. cs .. ShowCharts.chartIdCell(chartKey) .. (chart.defunct and " ''(defunct)''" or "")
local showWhen = entry["when"] and not ShowCharts.isRefOnlyOverride(entry, chart)
if showWhen then row = row .. " → " .. entry["when"] end
if isFirst then
if opts.showUses then row = row .. " || " .. ShowCharts.rowspanCell(ShowCharts.usageCountLink(chartKey, opts.typeName), entryCount, style) end
row = row .. " || " .. ShowCharts.rowspanCell(chart.chart or country, entryCount, style)
row = row .. " || " .. ShowCharts.rowspanCell(chart.provider or "—", entryCount, style)
end
if opts.showParams then row = row .. " || " .. cs .. ShowCharts.buildParamsStr(entry, chart) end
if opts.showCategories and isFirst then
row = row .. " || " .. ShowCharts.rowspanCell(ShowCharts.categoryLink(chart.number_one_category), entryCount, style)
end
if opts.showRef then row = row .. " || " .. cs .. ShowCharts.buildSampleOutput(entry, chart, sampleArgs) end
table.insert(rows, {content = row, note = entryNote, noteStyle = style})
countryRowsEmitted = countryRowsEmitted + 1
end
else
local row = countryCell() .. cs .. ShowCharts.chartIdCell(chartKey) .. (chart.defunct and " ''(defunct)''" or "")
if opts.showUses then row = row .. " || " .. cs .. ShowCharts.usageCountLink(chartKey, opts.typeName) end
row = row .. " || " .. cs .. (chart.chart or country)
row = row .. " || " .. cs .. (chart.provider or "—")
if opts.showParams then
local entry = { url = chart.url, url_title = chart.url_title }
row = row .. " || " .. cs .. ShowCharts.buildParamsStr(entry, chart)
end
if opts.showCategories then row = row .. " || " .. cs .. ShowCharts.categoryLink(chart.number_one_category) end
if opts.showRef then
local entry = { url = chart.url, url_title = chart.url_title, lang = chart.lang }
row = row .. " || " .. cs .. ShowCharts.buildSampleOutput(entry, chart, sampleArgs)
end
table.insert(rows, {content = row, note = docNote, noteStyle = style})
countryRowsEmitted = countryRowsEmitted + 1
end
end
end
end
-- Calculate note rowspans
if opts.showNotes then
local i = 1
while i <= #rows do
local note = rows[i].note
if note ~= "" then
local span = 1
while i + span <= #rows and rows[i + span].note == note do span = span + 1 end
rows[i].noteRowspan = span
for j = 1, span - 1 do rows[i + j].noteRowspan = 0 end
i = i + span
else
rows[i].noteRowspan = 1
i = i + 1
end
end
end
return rows
end
function ShowCharts.generateTable(rows, caption, opts)
local out = {}
table.insert(out, '{| class="wikitable sortable"')
table.insert(out, '|+ ' .. caption)
table.insert(out, '|-')
local header = '! Group !! Chart ID'
if opts.showUses then header = header .. ' !! Uses' end
header = header .. ' !! Chart !! Provider'
if opts.showParams then header = header .. ' !! Required params' end
if opts.showCategories then header = header .. ' !! #1 Category' end
if opts.showRef then header = header .. ' !! Sample ref output' end
if opts.showNotes then header = header .. ' !! Notes' end
table.insert(out, header)
for _, row in ipairs(rows) do
table.insert(out, '|-')
local line = row.content
if opts.showNotes then
local span = row.noteRowspan or 1
if span > 1 then
line = line .. string.format(' || rowspan="%d" %s| %s', span, row.noteStyle, row.note)
elseif span == 1 then
local noteCell = row.noteStyle ~= "" and (row.noteStyle .. "| ") or ""
line = line .. " || " .. noteCell .. row.note
end
end
table.insert(out, line)
end
table.insert(out, '|}')
return table.concat(out, '\n')
end
function p.showCharts(frame)
local chartType = frame.args.type or CONFIG.default_type
local filterCountry = frame.args.country
local splitDvd = frame.args.splitdvd == "yes" or frame.args.splitdvd == "1"
local ok, grouped = pcall(mw.loadJsonData, string.format(CONFIG.json_path, chartType))
if not ok then return '<span style="color:red;">ERROR: Cannot load JSON</span>' end
local typeName = getTypeName(chartType)
local opts = {
showParams = frame.args.params ~= "no",
showRef = frame.args.ref ~= "no",
showUses = frame.args.uses ~= "no",
showCategories = frame.args.number1 == "yes" or frame.args.number1 == "1",
showNotes = frame.args.notes == "yes" or frame.args.notes == "1",
sortAlpha = CONFIG.sort_order == "abc",
typeName = typeName
}
local countries = {}
for c in pairs(grouped) do
if string.sub(c, 1, 1) ~= "_" and (not filterCountry or c == filterCountry) then table.insert(countries, c) end
end
if opts.sortAlpha then table.sort(countries) end
local out = {}
local jsonPage = string.format(CONFIG.json_path, chartType):gsub("%.json$", "")
table.insert(out, string.format("'''Data:''' [[%s.json]] • '''Testcases:''' [[Template:%s chart/testcases]]", jsonPage, typeName))
table.insert(out, "")
if splitDvd then
local mainRows = ShowCharts.buildRows(grouped, countries, opts, frame, false)
local dvdRows = ShowCharts.buildRows(grouped, countries, opts, frame, true)
if #mainRows > 0 then
table.insert(out, ShowCharts.generateTable(mainRows, typeName .. " chart outputs", opts))
end
if #dvdRows > 0 then
table.insert(out, "")
table.insert(out, ShowCharts.generateTable(dvdRows, "Music DVD chart outputs", opts))
end
else
local rows = ShowCharts.buildRows(grouped, countries, opts, frame, nil)
table.insert(out, ShowCharts.generateTable(rows, typeName .. " chart outputs", opts))
end
return table.concat(out, '\n')
end
return p