Wikipedia
testwiki
https://test.wikipedia.org/wiki/Main_Page
MediaWiki 1.46.0-wmf.26
first-letter
Media
Special
Talk
User
User talk
Wikipedia
Wikipedia talk
File
File talk
MediaWiki
MediaWiki talk
Template
Template talk
Help
Help talk
Category
Category talk
Thread
Thread talk
Summary
Summary talk
Test namespace 1
Test namespace 1 talk
Test namespace 2
Test namespace 2 talk
Draft
Draft talk
Campaign
Campaign talk
TimedText
TimedText talk
Module
Module talk
SecurePoll
SecurePoll talk
CNBanner
CNBanner talk
Translations
Translations talk
Event
Event talk
Topic
Newsletter
Newsletter talk
Wikipedia:Requests/Permissions
4
32559
740055
739360
2026-05-01T18:21:59Z
Namoroka
19627
/* Requests for user rights */
740055
wikitext
text/x-wiki
<noinclude>{{Shortcut|WP:R/P|WP:RfP|WP:RfA|WP:PERM|WP:RFR|WP:RFPERM}}</noinclude>
{{Wikipedia:Requests/Top}}
== Requests for user rights ==
* Subpages: [[Wikipedia:Requests/Permissions/All|All (current and archived)]]
* Request:
<inputbox>
type=create
prefix=Wikipedia:Requests/Permissions/
preload=Template:PA2
buttonlabel=Requests for user rights
placeholder=Enter your username
</inputbox>
After creating the subpage, come back here and transclude the page below (<code><nowiki>{{Wikipedia:Requests/Permissions/Example}}</nowiki></code>).
<!-- Please transclude your requests below this line, LATEST AT THE TOP, in the form {{Wikipedia:Requests/Permissions/USERNAME}} -->
{{Wikipedia:Requests/Permissions/Namoroka}}
<!-- NEW ENTRIES AT THE TOP, NOT HERE -->
r0wiqrmlkx4dl7otrxevuds2qk1c7gf
Wikipedia:Requests/Bot status
4
32566
740046
658436
2026-05-01T17:19:02Z
Alachuckthebuck
49745
/* Request bot status */ transcluding my request for bot acess
740046
wikitext
text/x-wiki
<noinclude>{{/Top}}</noinclude>
== Request bot status ==
<inputbox>
type=create
default=Wikipedia:Requests/Bot status/Your Bot Name
preload=Template:PA2
buttonlabel=Requests for bot status
</inputbox>
[[Wikipedia:Requests/Bot status/All|All (current and archived)]]
----
{{Wikipedia:Requests/Bot status/Alachuckthebuck}}
[[Category:!Requests]]
2mueynjl7ydc7bac43wbvou6ap7c1z1
Template:Hidden
10
44109
740074
584312
2024-08-22T17:15:23Z
en>Ahecht
0
safesubst
740074
wikitext
text/x-wiki
{{hidden begin|toggle={{{showhide|}}}{{{toggle|}}}|expanded={{{expand|{{{expanded|}}}}}}|class={{{class|}}}|border={{{border|}}}|style={{{framestyle|{{{style|{{{css|}}}}}}}}}
|titlebgcolor={{{titlebgcolor|{{{background|{{{bg1|}}}}}}}}}|ta1={{{titlealign|{{{ta1|center}}}}}}|titlestyle={{safesubst<noinclude/>:#if:{{{multiline|{{{multi-line|}}}}}}|height:auto;}}{{safesubst<noinclude/>:#ifeq:{{{fw1|bold}}}|bold||font-weight:{{{fw1|bold}}};}}{{{headercss|{{{headerstyle|{{{titlestyle|}}}}}}}}}
|title={{safesubst<noinclude/>:#if:{{{multiline|{{{multi-line|}}}}}}|<div style="margin-right:4em;line-height:125%;height:auto;">{{{title|{{{header|{{{1}}}}}}}}}</div>|{{{title|{{{header|{{{1}}}}}}}}}}}
|contentbgcolor={{{contentbgcolor|{{{bg2|}}}}}}|ta2={{{ta2|}}}|contentstyle={{safesubst<noinclude/>:#ifeq:{{{fw2|normal}}}|normal||font-weight:{{{fw2|normal}}};}}{{{contentcss|{{{contentstyle|}}}}}} }}
{{{content|{{{contents|{{{text|{{{2}}}}}}}}}}}}{{hidden end}}{{safesubst<noinclude/>:#if:{{{background|}}}{{{bg1|}}}{{{bg2|}}}{{{titlealign|}}}{{{ta1|}}}{{{ta2|}}}{{{fw1|}}}{{{fw2|}}}|[[Category:Hidden templates using styles|{{NAMESPACE}}{{PAGENAME}}]]}}<noinclude>
{{documentation}}
</noinclude>
p5xonrpu5ow58rghncb9ny8veaty7fm
740075
740074
2026-05-01T19:28:18Z
Novem Linguae
49714
1 revision imported from [[:en:Template:Hidden]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740074
wikitext
text/x-wiki
{{hidden begin|toggle={{{showhide|}}}{{{toggle|}}}|expanded={{{expand|{{{expanded|}}}}}}|class={{{class|}}}|border={{{border|}}}|style={{{framestyle|{{{style|{{{css|}}}}}}}}}
|titlebgcolor={{{titlebgcolor|{{{background|{{{bg1|}}}}}}}}}|ta1={{{titlealign|{{{ta1|center}}}}}}|titlestyle={{safesubst<noinclude/>:#if:{{{multiline|{{{multi-line|}}}}}}|height:auto;}}{{safesubst<noinclude/>:#ifeq:{{{fw1|bold}}}|bold||font-weight:{{{fw1|bold}}};}}{{{headercss|{{{headerstyle|{{{titlestyle|}}}}}}}}}
|title={{safesubst<noinclude/>:#if:{{{multiline|{{{multi-line|}}}}}}|<div style="margin-right:4em;line-height:125%;height:auto;">{{{title|{{{header|{{{1}}}}}}}}}</div>|{{{title|{{{header|{{{1}}}}}}}}}}}
|contentbgcolor={{{contentbgcolor|{{{bg2|}}}}}}|ta2={{{ta2|}}}|contentstyle={{safesubst<noinclude/>:#ifeq:{{{fw2|normal}}}|normal||font-weight:{{{fw2|normal}}};}}{{{contentcss|{{{contentstyle|}}}}}} }}
{{{content|{{{contents|{{{text|{{{2}}}}}}}}}}}}{{hidden end}}{{safesubst<noinclude/>:#if:{{{background|}}}{{{bg1|}}}{{{bg2|}}}{{{titlealign|}}}{{{ta1|}}}{{{ta2|}}}{{{fw1|}}}{{{fw2|}}}|[[Category:Hidden templates using styles|{{NAMESPACE}}{{PAGENAME}}]]}}<noinclude>
{{documentation}}
</noinclude>
p5xonrpu5ow58rghncb9ny8veaty7fm
Template:Columns-list
10
70788
740078
584318
2023-12-22T03:07:49Z
en>Ahecht
0
If we're making the meat of the template substable, the rest should be too
740078
wikitext
text/x-wiki
{{<includeonly>safesubst:</includeonly>#if:{{{1|}}}|{{<includeonly>safesubst:</includeonly>#invoke:Template wrapper|wrap|_template=div col|_alias-map=1:content|colwidth=30em}}}}{{<includeonly>safesubst:</includeonly>#invoke:Check for unknown parameters|check|unknown={{main other|[[Category:Pages using columns-list with unknown parameters|_VALUE_{{PAGENAME}}]]}}|preview=Page using [[Template:Columns-list]] with unknown parameter "_VALUE_"|ignoreblank=y| 1 | class | content | colwidth | gap | rules | small | style }}<noinclude>
{{documentation}}
</noinclude>
bncwuudalvclxc2ss44oddbl9xjm222
740079
740078
2026-05-01T19:28:19Z
Novem Linguae
49714
1 revision imported from [[:en:Template:Columns-list]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740078
wikitext
text/x-wiki
{{<includeonly>safesubst:</includeonly>#if:{{{1|}}}|{{<includeonly>safesubst:</includeonly>#invoke:Template wrapper|wrap|_template=div col|_alias-map=1:content|colwidth=30em}}}}{{<includeonly>safesubst:</includeonly>#invoke:Check for unknown parameters|check|unknown={{main other|[[Category:Pages using columns-list with unknown parameters|_VALUE_{{PAGENAME}}]]}}|preview=Page using [[Template:Columns-list]] with unknown parameter "_VALUE_"|ignoreblank=y| 1 | class | content | colwidth | gap | rules | small | style }}<noinclude>
{{documentation}}
</noinclude>
bncwuudalvclxc2ss44oddbl9xjm222
User:MiszaBot/config
2
74217
740054
184999
2026-05-01T18:04:54Z
Ludo Games-T
73707
740054
wikitext
text/x-wiki
{{#if:{{{key|}}}|[[Category:Pages archived using a key]]|{{#ifeq:{{#invoke:String|find|source={{{archive|}}}|target={{#titleparts:{{FULLPAGENAME}}}}/}}|1|<nowiki />|<includeonly>{{Preview warning|Page using [[User:MiszaBot/config]] with [[:Category:Pages where archive parameter is not a subpage|archive parameter not a subpage]]}}[[Category:Pages where archive parameter is not a subpage]]</includeonly>}}}}<!--
-->{{#ifeq:{{{algo|}}}|{{#invoke:String|match|s={{{algo|}}}|pattern=old%(%d+[hd]?%)|show-pattern=no}}|<nowiki />|<includeonly>{{Preview warning|Page using [[User:MiszaBot/config]] with invalid duration in {{para|algo|old()}} parameter: must use a duration in hours (h) or days (d)}}</includeonly>}}<!--
-->
0y078ktpsr7elk6b38v45f7o70x9lkf
Template:Module other
10
77521
740084
736467
2026-04-18T03:45:36Z
en>Pppery
0
Changed protection settings for "[[Template:Module other]]" ([Edit=Require template editor access] (indefinite) [Move=Require template editor access] (indefinite))
740084
wikitext
text/x-wiki
{{safesubst:<noinclude/>#switch:
<noinclude><!-- If no or empty "demospace" parameter then detect namespace --></noinclude>
{{safesubst:<noinclude/>#if:{{{demospace|}}}
| {{safesubst:<noinclude/>lc: {{{demospace}}} }} <noinclude><!-- Use lower case "demospace" --></noinclude>
| {{safesubst:<noinclude/>#ifeq:{{safesubst:<noinclude/>NAMESPACE}}|{{safesubst:<noinclude/>ns:Module}}
| module
| other
}}
}}
| module = {{{module|{{{1|}}}}}}
| other
| #default = {{{other|{{{2|}}}}}}
}}<noinclude><!--End switch-->
{{documentation}}
<!-- Add categories to the /doc subpage, not here! -->
</noinclude>
6mo4qg9nwfuq6vj2r8irxnjnd71296k
740085
740084
2026-05-01T19:28:20Z
Novem Linguae
49714
1 revision imported from [[:en:Template:Module_other]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740084
wikitext
text/x-wiki
{{safesubst:<noinclude/>#switch:
<noinclude><!-- If no or empty "demospace" parameter then detect namespace --></noinclude>
{{safesubst:<noinclude/>#if:{{{demospace|}}}
| {{safesubst:<noinclude/>lc: {{{demospace}}} }} <noinclude><!-- Use lower case "demospace" --></noinclude>
| {{safesubst:<noinclude/>#ifeq:{{safesubst:<noinclude/>NAMESPACE}}|{{safesubst:<noinclude/>ns:Module}}
| module
| other
}}
}}
| module = {{{module|{{{1|}}}}}}
| other
| #default = {{{other|{{{2|}}}}}}
}}<noinclude><!--End switch-->
{{documentation}}
<!-- Add categories to the /doc subpage, not here! -->
</noinclude>
6mo4qg9nwfuq6vj2r8irxnjnd71296k
Template:Hidden begin
10
78002
740076
609494
2025-10-04T17:15:00Z
en>Jonesey95
0
fix text color in dark mode; this generally works but may cause problems, in which case revert or try specifying a different CSS value
740076
wikitext
text/x-wiki
<includeonly><templatestyles src="Template:Hidden begin/styles.css"/><div class="hidden-begin mw-collapsible {{#ifeq:{{{showhide|{{{toggle}}}}}}|left|mw-collapsible-leftside-toggle}} {{#if:{{{expanded|}}}||mw-collapsed}} {{{class|}}}" style="{{#if:{{{width|}}}|width:{{{width}}};}} {{#if:{{{border|}}}|border:{{{border}}};}} {{#if:{{{bgcolor|}}}|background-color:{{{bgcolor}}};color:inherit;}} {{{style|}}}"><!--
--><div class="hidden-title skin-nightmode-reset-color" style="{{#if:{{{ta1|}}}|text-align:{{{ta1}}};}} {{#if:{{{titlebgcolor|}}}|background-color:{{{titlebgcolor}}};color:inherit;}} {{{titlestyle|}}}">{{{title|{{{header|}}}}}}</div><!--
--><div class="hidden-content mw-collapsible-content" style="{{#if:{{{ta2|}}}|text-align:{{{ta2}}};}} {{#if:{{{contentbgcolor|}}}|background-color:{{{contentbgcolor}}};color:inherit;}} {{{contentstyle|{{{bodystyle|}}}}}}"><!--
Content added after the template
--></includeonly><noinclude>
{{Documentation}}
</noinclude>
3fk8uj9th66zvww6bd58rbapuyb6ij4
740077
740076
2026-05-01T19:28:19Z
Novem Linguae
49714
1 revision imported from [[:en:Template:Hidden_begin]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740076
wikitext
text/x-wiki
<includeonly><templatestyles src="Template:Hidden begin/styles.css"/><div class="hidden-begin mw-collapsible {{#ifeq:{{{showhide|{{{toggle}}}}}}|left|mw-collapsible-leftside-toggle}} {{#if:{{{expanded|}}}||mw-collapsed}} {{{class|}}}" style="{{#if:{{{width|}}}|width:{{{width}}};}} {{#if:{{{border|}}}|border:{{{border}}};}} {{#if:{{{bgcolor|}}}|background-color:{{{bgcolor}}};color:inherit;}} {{{style|}}}"><!--
--><div class="hidden-title skin-nightmode-reset-color" style="{{#if:{{{ta1|}}}|text-align:{{{ta1}}};}} {{#if:{{{titlebgcolor|}}}|background-color:{{{titlebgcolor}}};color:inherit;}} {{{titlestyle|}}}">{{{title|{{{header|}}}}}}</div><!--
--><div class="hidden-content mw-collapsible-content" style="{{#if:{{{ta2|}}}|text-align:{{{ta2}}};}} {{#if:{{{contentbgcolor|}}}|background-color:{{{contentbgcolor}}};color:inherit;}} {{{contentstyle|{{{bodystyle|}}}}}}"><!--
Content added after the template
--></includeonly><noinclude>
{{Documentation}}
</noinclude>
3fk8uj9th66zvww6bd58rbapuyb6ij4
Lotus Pond
0
78685
740008
446056
2026-05-01T14:30:49Z
Cryptocurrency777
73698
740008
wikitext
text/x-wiki
exists.
dsyy0d0s4fxuacmaqdz5dxjf4z9xryy
Template:Oldid
10
83044
740082
238645
2018-12-10T20:41:24Z
en>Jonesey95
0
rm stray trailing space, per talk page request
740082
wikitext
text/x-wiki
<span class="plainlinks">[{{fullurl:{{{page|{{{1|Main Page}}}}}}|oldid={{{oldid|{{{2|}}}}}}}} {{{label|{{{title|{{{3|{{#if:{{{oldid|{{{2|}}}}}}|Old revision|Current version}} of {{#if:{{{page|{{{1|}}}}}}|'''{{{page|{{{1}}}}}}'''|a page}}}}}}}}}}}]</span><noinclude>
{{documentation}}
</noinclude>
8oa36sx98rpl6fqzvwz7o9hp304i7yz
740083
740082
2026-05-01T19:28:19Z
Novem Linguae
49714
1 revision imported from [[:en:Template:Oldid]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740082
wikitext
text/x-wiki
<span class="plainlinks">[{{fullurl:{{{page|{{{1|Main Page}}}}}}|oldid={{{oldid|{{{2|}}}}}}}} {{{label|{{{title|{{{3|{{#if:{{{oldid|{{{2|}}}}}}|Old revision|Current version}} of {{#if:{{{page|{{{1|}}}}}}|'''{{{page|{{{1}}}}}}'''|a page}}}}}}}}}}}]</span><noinclude>
{{documentation}}
</noinclude>
8oa36sx98rpl6fqzvwz7o9hp304i7yz
Module:Protection banner/config
828
83704
740061
637555
2026-04-18T02:24:50Z
en>Santiago Claudio
0
Sync with sandbox per edit request
740061
Scribunto
text/plain
-- This module provides configuration data for [[Module:Protection banner]].
return {
--------------------------------------------------------------------------------
--
-- BANNER DATA
--
--------------------------------------------------------------------------------
--[[
-- Banner data consists of six fields:
-- * text - the main protection text that appears at the top of protection
-- banners.
-- * explanation - the text that appears below the main protection text, used
-- to explain the details of the protection.
-- * tooltip - the tooltip text you see when you move the mouse over a small
-- padlock icon.
-- * link - the page that the small padlock icon links to.
-- * alt - the alt text for the small padlock icon. This is also used as tooltip
-- text for the large protection banners.
-- * image - the padlock image used in both protection banners and small padlock
-- icons.
--
-- The module checks in three separate tables to find a value for each field.
-- First it checks the banners table, which has values specific to the reason
-- for the page being protected. Then the module checks the defaultBanners
-- table, which has values specific to each protection level. Finally, the
-- module checks the masterBanner table, which holds data for protection
-- templates to use if no data has been found in the previous two tables.
--
-- The values in the banner data can take parameters. These are specified
-- using ${TEXTLIKETHIS} (a dollar sign preceding a parameter name
-- enclosed in curly braces).
--
-- Available parameters:
--
-- ${CURRENTVERSION} - a link to the page history or the move log, with the
-- display message "current-version-edit-display" or
-- "current-version-move-display".
--
-- ${EDITREQUEST} - a link to create an edit request for the current page.
--
-- ${EXPLANATIONBLURB} - an explanation blurb, e.g. "Please discuss any changes
-- on the talk page; you may submit a request to ask an administrator to make
-- an edit if it is minor or supported by consensus."
--
-- ${IMAGELINK} - a link to set the image to, depending on the protection
-- action and protection level.
--
-- ${INTROBLURB} - the PROTECTIONBLURB parameter, plus the expiry if an expiry
-- is set. E.g. "Editing of this page by new or unregistered users is currently
-- disabled until dd Month YYYY."
--
-- ${INTROFRAGMENT} - the same as ${INTROBLURB}, but without final punctuation
-- so that it can be used in run-on sentences.
--
-- ${PAGETYPE} - the type of the page, e.g. "article" or "template".
-- Defined in the cfg.pagetypes table.
--
-- ${PROTECTIONBLURB} - a blurb explaining the protection level of the page, e.g.
-- "Editing of this page by new or unregistered users is currently disabled"
--
-- ${PROTECTIONDATE} - the protection date, if it has been supplied to the
-- template.
--
-- ${PROTECTIONLEVEL} - the protection level, e.g. "fully protected" or
-- "semi-protected".
--
-- ${PROTECTIONLOG} - a link to the protection log or the pending changes log,
-- depending on the protection action.
--
-- ${TALKPAGE} - a link to the talk page. If a section is specified, links
-- straight to that talk page section.
--
-- ${TOOLTIPBLURB} - uses the PAGETYPE, PROTECTIONTYPE and EXPIRY parameters to
-- create a blurb like "This template is semi-protected", or "This article is
-- move-protected until DD Month YYYY".
--
-- ${VANDAL} - links for the specified username (or the root page name)
-- using Module:Vandal-m.
--
-- Functions
--
-- For advanced users, it is possible to use Lua functions instead of strings
-- in the banner config tables. Using functions gives flexibility that is not
-- possible just by using parameters. Functions take two arguments, the
-- protection object and the template arguments, and they must output a string.
--
-- For example:
--
-- text = function (protectionObj, args)
-- if protectionObj.level == 'autoconfirmed' then
-- return 'foo'
-- else
-- return 'bar'
-- end
-- end
--
-- Some protection object properties and methods that may be useful:
-- protectionObj.action - the protection action
-- protectionObj.level - the protection level
-- protectionObj.reason - the protection reason
-- protectionObj.expiry - the expiry. Nil if unset, the string "indef" if set
-- to indefinite, and the protection time in unix time if temporary.
-- protectionObj.protectionDate - the protection date in unix time, or nil if
-- unspecified.
-- protectionObj.bannerConfig - the banner config found by the module. Beware
-- of editing the config field used by the function, as it could create an
-- infinite loop.
-- protectionObj:isProtected - returns a boolean showing whether the page is
-- protected.
-- protectionObj:isTemporary - returns a boolean showing whether the expiry is
-- temporary.
-- protectionObj:isIncorrect - returns a boolean showing whether the protection
-- template is incorrect.
--]]
-- The master banner data, used if no values have been found in banners or
-- defaultBanners.
masterBanner = {
text = '${INTROBLURB}',
explanation = '${EXPLANATIONBLURB}',
tooltip = '${TOOLTIPBLURB}',
link = '${IMAGELINK}',
alt = 'Page ${PROTECTIONLEVEL}'
},
-- The default banner data. This holds banner data for different protection
-- levels.
-- *required* - this table needs edit, move, autoreview and upload subtables.
defaultBanners = {
edit = {},
move = {},
autoreview = {
default = {
alt = 'Page protected with pending changes',
tooltip = 'All edits by unregistered and new users are subject to review prior to becoming visible to unregistered users',
image = 'Pending-protection-shackle.svg'
}
},
upload = {}
},
-- The banner data. This holds banner data for different protection reasons.
-- In fact, the reasons specified in this table control which reasons are
-- valid inputs to the first positional parameter.
--
-- There is also a non-standard "description" field that can be used for items
-- in this table. This is a description of the protection reason for use in the
-- module documentation.
--
-- *required* - this table needs edit, move, autoreview and upload subtables.
banners = {
edit = {
blp = {
description = 'For pages protected to promote compliance with the'
.. ' [[Wikipedia:Biographies of living persons'
.. '|biographies of living persons]] policy',
text = '${INTROFRAGMENT} to promote compliance with'
.. ' [[Wikipedia:Biographies of living persons'
.. "|Wikipedia's policy on the biographies"
.. ' of living people]].',
tooltip = '${TOOLTIPFRAGMENT} to promote compliance with the policy on'
.. ' biographies of living persons',
},
deceased = {
description = 'For user pages of Wikipedia users who are deceased',
text = '${INTROFRAGMENT} to prevent vandalism of a deceased'
.. ' Wikipedian\'s user page.'
.. ' A family member who wishes to edit this user page can use this'
.. ' user\'s ${TALKPAGE} or submit a request to [[Wikipedia:VRT|the'
.. ' Volunteer Response Team]].',
tooltip = '${TOOLTIPFRAGMENT} because this Wikipedian is deceased'
},
dmca = {
description = 'For pages protected by the Wikimedia Foundation'
.. ' due to [[Digital Millennium Copyright Act]] takedown requests',
explanation = function (protectionObj, args)
local ret = 'Pursuant to a rights owner notice under the Digital'
.. ' Millennium Copyright Act (DMCA) regarding some content'
.. ' in this article, the Wikimedia Foundation acted under'
.. ' applicable law and took down and restricted the content'
.. ' in question.'
if args.notice then
ret = ret .. ' A copy of the received notice can be found here: '
.. args.notice .. '.'
end
ret = ret .. ' For more information, including websites discussing'
.. ' how to file a counter-notice, please see'
.. " [[Wikipedia:Office actions]] and the article's ${TALKPAGE}."
.. "'''Do not remove this template from the article until the"
.. " restrictions are withdrawn'''."
return ret
end,
image = 'Office-protection-shackle.svg',
link = 'Wikipedia:Protection policy#office',
},
dispute = {
description = 'For pages protected due to editing disputes',
text = function (protectionObj, args)
-- Find the value of "disputes".
local display = 'disputes'
local disputes
if args.section then
disputes = string.format(
'[[%s:%s#%s|%s]]',
mw.site.namespaces[protectionObj.title.namespace].talk.name,
protectionObj.title.text,
args.section,
display
)
else
disputes = display
end
-- Make the blurb, depending on the expiry.
local msg
if type(protectionObj.expiry) == 'number' then
msg = '${INTROFRAGMENT} or until editing %s have been resolved.'
else
msg = '${INTROFRAGMENT} until editing %s have been resolved.'
end
return string.format(msg, disputes)
end,
explanation = "This protection is '''not''' an endorsement of the"
.. ' ${CURRENTVERSION}. ${EXPLANATIONBLURB}',
tooltip = '${TOOLTIPFRAGMENT} due to editing disputes',
},
ecp = {
description = 'For articles in topic areas authorized by'
.. ' [[Wikipedia:Arbitration Committee|ArbCom]] or'
.. ' meets the criteria for community use',
alt = 'Extended-protected ${PAGETYPE}',
},
mainpage = {
description = 'For pages protected for being displayed on the [[Main Page]]',
text = 'This file is currently'
.. ' [[Wikipedia:This page is protected|protected]] from'
.. ' editing because it is currently or will soon be displayed'
.. ' on the [[Main Page]].',
explanation = 'Images on the Main Page are protected due to their high'
.. ' visibility. Please discuss any necessary changes on the ${TALKPAGE}.'
.. '<br /><span style="font-size:90%;">'
.. "'''Administrators:''' Once this image is definitely off the Main Page,"
.. ' please unprotect this file, or reduce to semi-protection,'
.. ' as appropriate.</span>',
},
office = {
description = 'For pages protected by the Wikimedia Foundation',
text = function (protectionObj, args)
local ret = 'This ${PAGETYPE} is currently under the'
.. ' scrutiny of the'
.. ' [[Wikipedia:Office actions|Wikimedia Foundation Office]]'
.. ' and is protected.'
if protectionObj.protectionDate then
ret = ret .. ' It has been protected since ${PROTECTIONDATE}.'
end
return ret
end,
explanation = "If you can edit this page, please discuss all changes and"
.. " additions on the ${TALKPAGE} first. '''Do not remove protection from this"
.. " page unless you are authorized by the Wikimedia Foundation to do"
.. " so.'''",
image = 'Office-protection-shackle.svg',
link = 'Wikipedia:Protection policy#office',
},
reset = {
description = 'For pages protected by the Wikimedia Foundation and'
.. ' "reset" to a bare-bones version',
text = 'This ${PAGETYPE} is currently under the'
.. ' scrutiny of the'
.. ' [[Wikipedia:Office actions|Wikimedia Foundation Office]]'
.. ' and is protected.',
explanation = function (protectionObj, args)
local ret = ''
if protectionObj.protectionDate then
ret = ret .. 'On ${PROTECTIONDATE} this ${PAGETYPE} was'
else
ret = ret .. 'This ${PAGETYPE} has been'
end
ret = ret .. ' reduced to a'
.. ' simplified, "bare bones" version so that it may be completely'
.. ' rewritten to ensure it meets the policies of'
.. ' [[WP:NPOV|Neutral Point of View]] and [[WP:V|Verifiability]].'
.. ' Standard Wikipedia policies will apply to its rewriting—which'
.. ' will eventually be open to all editors—and will be strictly'
.. ' enforced. The ${PAGETYPE} has been ${PROTECTIONLEVEL} while'
.. ' it is being rebuilt.\n\n'
.. 'Any insertion of material directly from'
.. ' pre-protection revisions of the ${PAGETYPE} will be removed, as'
.. ' will any material added to the ${PAGETYPE} that is not properly'
.. ' sourced. The associated talk page(s) were also cleared on the'
.. " same date.\n\n"
.. "If you can edit this page, please discuss all changes and"
.. " additions on the ${TALKPAGE} first. '''Do not override"
.. " this action, and do not remove protection from this page,"
.. " unless you are authorized by the Wikimedia Foundation"
.. " to do so. No editor may remove this notice.'''"
return ret
end,
image = 'Office-protection-shackle.svg',
link = 'Wikipedia:Protection policy#office',
},
sock = {
description = 'For pages protected due to'
.. ' [[Wikipedia:Sock puppetry|sock puppetry]]',
text = '${INTROFRAGMENT} to prevent [[Wikipedia:Sock puppetry|sock puppets]] of'
.. ' [[Wikipedia:Blocking policy|blocked]] or'
.. ' [[Wikipedia:Banning policy|banned users]]'
.. ' from editing it.',
tooltip = '${TOOLTIPFRAGMENT} to prevent sock puppets of blocked or banned users from'
.. ' editing it',
},
template = {
description = 'For [[Wikipedia:High-risk templates|high-risk]]'
.. ' templates and Lua modules',
text = 'This is a permanently [[Wikipedia:Protection policy|protected]] ${PAGETYPE},'
.. ' as it is [[Wikipedia:High-risk templates|high-risk]].',
explanation = 'Please discuss any changes on the ${TALKPAGE}; you may'
.. ' ${EDITREQUEST} to ask an'
.. ' [[Wikipedia:Administrators|administrator]] or'
.. ' [[Wikipedia:Template editor|template editor]] to make an edit if'
.. ' it is [[Help:Minor edit#When to mark an edit as a minor edit'
.. '|uncontroversial]] or supported by'
.. ' [[Wikipedia:Consensus|consensus]]. You can also'
.. ' [[Wikipedia:Requests for page protection|request]] that the page be'
.. ' unprotected.',
tooltip = 'This high-risk ${PAGETYPE} is permanently ${PROTECTIONLEVEL}'
.. ' to prevent vandalism',
alt = 'Permanently protected ${PAGETYPE}',
},
usertalk = {
description = 'For pages protected against disruptive edits by a'
.. ' particular user',
text = '${INTROFRAGMENT} to prevent ${VANDAL} from using it to make disruptive edits,'
.. ' such as abusing the'
.. ' {{[[Template:unblock|unblock]]}} template.',
explanation = 'If you cannot edit this user talk page and you need to'
.. ' make a change or leave a message, you can'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for edits to a protected page'
.. '|request an edit]],'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for reduction in protection level'
.. '|request unprotection]],'
.. ' [[Special:Userlogin|log in]],'
.. ' or [[Special:UserLogin/signup|create an account]].',
},
vandalism = {
description = 'For pages protected against'
.. ' [[Wikipedia:Vandalism|vandalism]]',
text = '${INTROFRAGMENT} due to [[Wikipedia:Vandalism|vandalism]].',
explanation = function (protectionObj, args)
local ret = ''
if protectionObj.level == 'sysop' then
ret = ret .. "This protection is '''not''' an endorsement of the"
.. ' ${CURRENTVERSION}. '
end
return ret .. '${EXPLANATIONBLURB}'
end,
tooltip = '${TOOLTIPFRAGMENT} due to vandalism',
}
},
move = {
dispute = {
description = 'For pages protected against page moves due to'
.. ' disputes over the page title',
explanation = "This protection is '''not''' an endorsement of the"
.. ' ${CURRENTVERSION}. ${EXPLANATIONBLURB}',
image = 'Move-protection-shackle.svg'
},
vandalism = {
description = 'For pages protected against'
.. ' [[Wikipedia:Vandalism#Page-move vandalism'
.. ' |page-move vandalism]]'
}
},
autoreview = {},
upload = {}
},
--------------------------------------------------------------------------------
--
-- GENERAL DATA TABLES
--
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Protection blurbs
--------------------------------------------------------------------------------
-- This table produces the protection blurbs available with the
-- ${PROTECTIONBLURB} parameter. It is sorted by protection action and
-- protection level, and is checked by the module in the following order:
-- 1. page's protection action, page's protection level
-- 2. page's protection action, default protection level
-- 3. "edit" protection action, default protection level
--
-- It is possible to use banner parameters inside this table.
-- *required* - this table needs edit, move, autoreview and upload subtables.
protectionBlurbs = {
edit = {
default = 'This ${PAGETYPE} is currently [[Wikipedia:Protection policy#full|'
.. 'protected]] from editing',
templateeditor = 'This ${PAGETYPE} is currently [[Wikipedia:Protection policy#template'
.. '|protected]] from editing',
autoconfirmed = 'Editing of this ${PAGETYPE} by [[Wikipedia:User access'
.. ' levels#New users|new]] or [[Wikipedia:User access levels#Unregistered'
.. ' users|unregistered]] users is currently [[Wikipedia:Protection'
.. ' policy#semi|disabled]]',
extendedconfirmed = 'This ${PAGETYPE} is currently under [[Wikipedia:Protection'
.. ' policy#extended|extended confirmed protection]]',
},
move = {
default = 'This ${PAGETYPE} is currently [[Wikipedia:Protection policy#Move'
.. ' protection|protected]] from [[Help:Moving a page|page moves]]'
},
autoreview = {
default = 'All edits made to this ${PAGETYPE} by'
.. ' [[Wikipedia:User access levels#New users|new]] or'
.. ' [[Wikipedia:User access levels#Unregistered users|unregistered]]'
.. ' users are currently'
.. ' [[Wikipedia:Pending changes|subject to review]]'
},
upload = {
default = 'Uploading new versions of this ${PAGETYPE} is currently disabled'
}
},
--------------------------------------------------------------------------------
-- Explanation blurbs
--------------------------------------------------------------------------------
-- This table produces the explanation blurbs available with the
-- ${EXPLANATIONBLURB} parameter. It is sorted by protection action,
-- protection level, and whether the page is a talk page or not. If the page is
-- a talk page it will have a talk key of "talk"; otherwise it will have a talk
-- key of "subject". The table is checked in the following order:
-- 1. page's protection action, page's protection level, page's talk key
-- 2. page's protection action, page's protection level, default talk key
-- 3. page's protection action, default protection level, page's talk key
-- 4. page's protection action, default protection level, default talk key
--
-- It is possible to use banner parameters inside this table.
-- *required* - this table needs edit, move, autoreview and upload subtables.
explanationBlurbs = {
edit = {
autoconfirmed = {
subject = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details. If you'
.. ' cannot edit this ${PAGETYPE} and you wish to make a change, you can'
.. ' ${EDITREQUEST}, discuss changes on the ${TALKPAGE},'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for reduction in protection level'
.. '|request unprotection]], [[Special:Userlogin|log in]], or'
.. ' [[Special:UserLogin/signup|create an account]].',
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details. If you'
.. ' cannot edit this ${PAGETYPE} and you wish to make a change, you can'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for reduction in protection level'
.. '|request unprotection]], [[Special:Userlogin|log in]], or'
.. ' [[Special:UserLogin/signup|create an account]].',
},
extendedconfirmed = {
default = 'Extended confirmed protection prevents edits from all unregistered editors'
.. ' and registered users with fewer than 30 days tenure and 500 edits.'
.. ' The [[Wikipedia:Protection policy#extended|policy on community use]]'
.. ' specifies that extended confirmed protection can be applied to combat'
.. ' disruption, if semi-protection has proven to be ineffective.'
.. ' Extended confirmed protection may also be applied to enforce'
.. ' [[Wikipedia:Arbitration Committee|arbitration sanctions]].'
.. ' Please discuss any changes on the ${TALKPAGE}; you may'
.. ' ${EDITREQUEST} to ask for uncontroversial changes supported by'
.. ' [[Wikipedia:Consensus|consensus]].'
},
default = {
subject = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' Please discuss any changes on the ${TALKPAGE}; you'
.. ' may ${EDITREQUEST} to ask an'
.. ' [[Wikipedia:Administrators|administrator]] to make an edit if it'
.. ' is [[Help:Minor edit#When to mark an edit as a minor edit'
.. '|uncontroversial]] or supported by [[Wikipedia:Consensus'
.. '|consensus]]. You may also [[Wikipedia:Requests for'
.. ' page protection#Current requests for reduction in protection level'
.. '|request]] that this page be unprotected.',
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' You may [[Wikipedia:Requests for page'
.. ' protection#Current requests for edits to a protected page|request an'
.. ' edit]] to this page, or [[Wikipedia:Requests for'
.. ' page protection#Current requests for reduction in protection level'
.. '|ask]] for it to be unprotected.'
}
},
move = {
default = {
subject = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' The page may still be edited but cannot be moved'
.. ' until unprotected. Please discuss any suggested moves on the'
.. ' ${TALKPAGE} or at [[Wikipedia:Requested moves]]. You can also'
.. ' [[Wikipedia:Requests for page protection|request]] that the page be'
.. ' unprotected.',
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' The page may still be edited but cannot be moved'
.. ' until unprotected. Please discuss any suggested moves at'
.. ' [[Wikipedia:Requested moves]]. You can also'
.. ' [[Wikipedia:Requests for page protection|request]] that the page be'
.. ' unprotected.'
}
},
autoreview = {
default = {
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' Edits to this ${PAGETYPE} by new and unregistered users'
.. ' will not be visible to readers until they are accepted by'
.. ' a reviewer. To avoid the need for your edits to be'
.. ' reviewed, you may'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for reduction in protection level'
.. '|request unprotection]], [[Special:Userlogin|log in]], or'
.. ' [[Special:UserLogin/signup|create an account]].'
},
},
upload = {
default = {
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' The page may still be edited but new versions of the file'
.. ' cannot be uploaded until it is unprotected. You can'
.. ' request that a new version be uploaded by using a'
.. ' [[Wikipedia:Edit requests|protected edit request]], or you'
.. ' can [[Wikipedia:Requests for page protection|request]]'
.. ' that the file be unprotected.'
}
}
},
--------------------------------------------------------------------------------
-- Protection levels
--------------------------------------------------------------------------------
-- This table provides the data for the ${PROTECTIONLEVEL} parameter, which
-- produces a short label for different protection levels. It is sorted by
-- protection action and protection level, and is checked in the following
-- order:
-- 1. page's protection action, page's protection level
-- 2. page's protection action, default protection level
-- 3. "edit" protection action, default protection level
--
-- It is possible to use banner parameters inside this table.
-- *required* - this table needs edit, move, autoreview and upload subtables.
protectionLevels = {
edit = {
default = 'protected',
templateeditor = 'template-protected',
extendedconfirmed = 'extended-confirmed-protected',
autoconfirmed = 'semi-protected',
},
move = {
default = 'move-protected'
},
autoreview = {
},
upload = {
default = 'upload-protected'
}
},
--------------------------------------------------------------------------------
-- Images
--------------------------------------------------------------------------------
-- This table lists different padlock images for each protection action and
-- protection level. It is used if an image is not specified in any of the
-- banner data tables, and if the page does not satisfy the conditions for using
-- the ['image-filename-indef'] image. It is checked in the following order:
-- 1. page's protection action, page's protection level
-- 2. page's protection action, default protection level
images = {
edit = {
default = 'Full-protection-shackle.svg',
templateeditor = 'Template-protection-shackle.svg',
extendedconfirmed = 'Extended-protection-shackle.svg',
autoconfirmed = 'Semi-protection-shackle.svg'
},
move = {
default = 'Move-protection-shackle.svg',
},
autoreview = {
default = 'Pending-protection-shackle.svg'
},
upload = {
default = 'Upload-protection-shackle.svg'
}
},
-- Pages with a reason specified in this table will show the special "indef"
-- padlock, defined in the 'image-filename-indef' message, if no expiry is set.
indefImageReasons = {
template = true
},
--------------------------------------------------------------------------------
-- Image links
--------------------------------------------------------------------------------
-- This table provides the data for the ${IMAGELINK} parameter, which gets
-- the image link for small padlock icons based on the page's protection action
-- and protection level. It is checked in the following order:
-- 1. page's protection action, page's protection level
-- 2. page's protection action, default protection level
-- 3. "edit" protection action, default protection level
--
-- It is possible to use banner parameters inside this table.
-- *required* - this table needs edit, move, autoreview and upload subtables.
imageLinks = {
edit = {
default = 'Wikipedia:Protection policy#full',
templateeditor = 'Wikipedia:Protection policy#template',
extendedconfirmed = 'Wikipedia:Protection policy#extended',
autoconfirmed = 'Wikipedia:Protection policy#semi'
},
move = {
default = 'Wikipedia:Protection policy#move'
},
autoreview = {
default = 'Wikipedia:Protection policy#pending'
},
upload = {
default = 'Wikipedia:Protection policy#upload'
}
},
--------------------------------------------------------------------------------
-- Padlock indicator names
--------------------------------------------------------------------------------
-- This table provides the "name" attribute for the <indicator> extension tag
-- with which small padlock icons are generated. All indicator tags on a page
-- are displayed in alphabetical order based on this attribute, and with
-- indicator tags with duplicate names, the last tag on the page wins.
-- The attribute is chosen based on the protection action; table keys must be a
-- protection action name or the string "default".
padlockIndicatorNames = {
autoreview = 'pp-autoreview',
default = 'pp-default'
},
--------------------------------------------------------------------------------
-- Protection categories
--------------------------------------------------------------------------------
--[[
-- The protection categories are stored in the protectionCategories table.
-- Keys to this table are made up of the following strings:
--
-- 1. the expiry date
-- 2. the namespace
-- 3. the protection reason (e.g. "dispute" or "vandalism")
-- 4. the protection level (e.g. "sysop" or "autoconfirmed")
-- 5. the action (e.g. "edit" or "move")
--
-- When the module looks up a category in the table, first it will will check to
-- see a key exists that corresponds to all five parameters. For example, a
-- user page semi-protected from vandalism for two weeks would have the key
-- "temp-user-vandalism-autoconfirmed-edit". If no match is found, the module
-- changes the first part of the key to "all" and checks the table again. It
-- keeps checking increasingly generic key combinations until it finds the
-- field, or until it reaches the key "all-all-all-all-all".
--
-- The module uses a binary matrix to determine the order in which to search.
-- This is best demonstrated by a table. In this table, the "0" values
-- represent "all", and the "1" values represent the original data (e.g.
-- "indef" or "file" or "vandalism").
--
-- expiry namespace reason level action
-- order
-- 1 1 1 1 1 1
-- 2 0 1 1 1 1
-- 3 1 0 1 1 1
-- 4 0 0 1 1 1
-- 5 1 1 0 1 1
-- 6 0 1 0 1 1
-- 7 1 0 0 1 1
-- 8 0 0 0 1 1
-- 9 1 1 1 0 1
-- 10 0 1 1 0 1
-- 11 1 0 1 0 1
-- 12 0 0 1 0 1
-- 13 1 1 0 0 1
-- 14 0 1 0 0 1
-- 15 1 0 0 0 1
-- 16 0 0 0 0 1
-- 17 1 1 1 1 0
-- 18 0 1 1 1 0
-- 19 1 0 1 1 0
-- 20 0 0 1 1 0
-- 21 1 1 0 1 0
-- 22 0 1 0 1 0
-- 23 1 0 0 1 0
-- 24 0 0 0 1 0
-- 25 1 1 1 0 0
-- 26 0 1 1 0 0
-- 27 1 0 1 0 0
-- 28 0 0 1 0 0
-- 29 1 1 0 0 0
-- 30 0 1 0 0 0
-- 31 1 0 0 0 0
-- 32 0 0 0 0 0
--
-- In this scheme the action has the highest priority, as it is the last
-- to change, and the expiry has the least priority, as it changes the most.
-- The priorities of the expiry, the protection level and the action are
-- fixed, but the priorities of the reason and the namespace can be swapped
-- through the use of the cfg.bannerDataNamespaceHasPriority table.
--]]
-- If the reason specified to the template is listed in this table,
-- namespace data will take priority over reason data in the protectionCategories
-- table.
reasonsWithNamespacePriority = {
vandalism = true,
},
-- The string to use as a namespace key for the protectionCategories table for each
-- namespace number.
categoryNamespaceKeys = {
[ 2] = 'user',
[ 3] = 'user',
[ 4] = 'project',
[ 6] = 'file',
[ 8] = 'mediawiki',
[ 10] = 'template',
[ 12] = 'project',
[ 14] = 'category',
[100] = 'portal',
[828] = 'module',
},
protectionCategories = {
['all|all|all|all|all'] = 'Wikipedia fully protected pages',
['all|all|office|all|all'] = 'Wikipedia Office-protected pages',
['all|all|reset|all|all'] = 'Wikipedia Office-protected pages',
['all|all|dmca|all|all'] = 'Wikipedia Office-protected pages',
['all|all|mainpage|all|all'] = 'Wikipedia fully protected main page files',
['all|all|all|extendedconfirmed|all'] = 'Wikipedia extended-confirmed-protected pages',
['all|all|ecp|extendedconfirmed|all'] = 'Wikipedia extended-confirmed-protected pages',
['all|template|all|all|edit'] = 'Wikipedia fully protected templates',
['all|all|all|autoconfirmed|edit'] = 'Wikipedia semi-protected pages',
['indef|all|all|autoconfirmed|edit'] = 'Wikipedia indefinitely semi-protected pages',
['all|all|blp|autoconfirmed|edit'] = 'Wikipedia indefinitely semi-protected biographies of living people',
['temp|all|blp|autoconfirmed|edit'] = 'Wikipedia temporarily semi-protected biographies of living people',
['all|all|dispute|autoconfirmed|edit'] = 'Wikipedia pages semi-protected due to dispute',
['all|all|sock|autoconfirmed|edit'] = 'Wikipedia pages semi-protected from banned users',
['all|all|vandalism|autoconfirmed|edit'] = 'Wikipedia pages semi-protected against vandalism',
['all|category|all|autoconfirmed|edit'] = 'Wikipedia semi-protected categories',
['all|file|all|autoconfirmed|edit'] = 'Wikipedia semi-protected files',
['all|portal|all|autoconfirmed|edit'] = 'Wikipedia semi-protected portals',
['all|project|all|autoconfirmed|edit'] = 'Wikipedia semi-protected project pages',
['all|talk|all|autoconfirmed|edit'] = 'Wikipedia semi-protected talk pages',
['all|template|all|autoconfirmed|edit'] = 'Wikipedia semi-protected templates',
['all|user|all|autoconfirmed|edit'] = 'Wikipedia semi-protected user and user talk pages',
['all|all|all|templateeditor|move'] = 'Wikipedia template-protected pages other than templates and modules',
['all|all|all|templateeditor|edit'] = 'Wikipedia template-protected pages other than templates and modules',
['all|template|all|templateeditor|edit'] = 'Wikipedia template-protected templates',
['all|template|all|templateeditor|move'] = 'Wikipedia template-protected templates', -- move-protected templates
['all|all|blp|sysop|edit'] = 'Wikipedia indefinitely protected biographies of living people',
['temp|all|blp|sysop|edit'] = 'Wikipedia temporarily protected biographies of living people',
['all|all|dispute|sysop|edit'] = 'Wikipedia pages protected due to dispute',
['all|all|sock|sysop|edit'] = 'Wikipedia pages protected from banned users',
['all|all|vandalism|sysop|edit'] = 'Wikipedia pages protected against vandalism',
['all|category|all|sysop|edit'] = 'Wikipedia fully protected categories',
['all|file|all|sysop|edit'] = 'Wikipedia fully protected files',
['all|project|all|sysop|edit'] = 'Wikipedia fully protected project pages',
['all|talk|all|sysop|edit'] = 'Wikipedia fully protected talk pages',
['all|template|all|extendedconfirmed|edit'] = 'Wikipedia extended-confirmed-protected templates',
['all|template|all|extendedconfirmed|move'] = 'Wikipedia extended-confirmed-protected templates',
['all|template|all|sysop|edit'] = 'Wikipedia fully protected templates',
['all|user|all|sysop|edit'] = 'Wikipedia fully protected user and user talk pages',
['all|module|all|all|edit'] = 'Wikipedia fully protected modules',
['all|module|all|templateeditor|edit'] = 'Wikipedia template-protected modules',
['all|module|all|extendedconfirmed|edit'] = 'Wikipedia extended-confirmed-protected modules',
['all|module|all|autoconfirmed|edit'] = 'Wikipedia semi-protected modules',
['all|all|all|sysop|move'] = 'Wikipedia move-protected pages',
['indef|all|all|sysop|move'] = 'Wikipedia indefinitely move-protected pages',
['all|all|dispute|sysop|move'] = 'Wikipedia pages move-protected due to dispute',
['all|all|vandalism|sysop|move'] = 'Wikipedia pages move-protected due to vandalism',
['all|portal|all|sysop|move'] = 'Wikipedia move-protected portals',
['all|project|all|sysop|move'] = 'Wikipedia move-protected project pages',
['all|talk|all|sysop|move'] = 'Wikipedia move-protected talk pages',
['all|template|all|sysop|move'] = 'Wikipedia move-protected templates',
['all|user|all|sysop|move'] = 'Wikipedia move-protected user and user talk pages',
['all|all|all|autoconfirmed|autoreview'] = 'Wikipedia pending changes protected pages',
['all|file|all|all|upload'] = 'Wikipedia upload-protected files',
},
--------------------------------------------------------------------------------
-- Expiry category config
--------------------------------------------------------------------------------
-- This table configures the expiry category behaviour for each protection
-- action.
-- * If set to true, setting that action will always categorise the page if
-- an expiry parameter is not set.
-- * If set to false, setting that action will never categorise the page.
-- * If set to nil, the module will categorise the page if:
-- 1) an expiry parameter is not set, and
-- 2) a reason is provided, and
-- 3) the specified reason is not blacklisted in the reasonsWithoutExpiryCheck
-- table.
expiryCheckActions = {
edit = nil,
move = false,
autoreview = true,
upload = false
},
reasonsWithoutExpiryCheck = {
blp = true,
template = true,
},
--------------------------------------------------------------------------------
-- Pagetypes
--------------------------------------------------------------------------------
-- This table produces the page types available with the ${PAGETYPE} parameter.
-- Keys are namespace numbers, or the string "default" for the default value.
pagetypes = {
[0] = 'article',
[6] = 'file',
[10] = 'template',
[14] = 'category',
[828] = 'module',
default = 'page'
},
--------------------------------------------------------------------------------
-- Strings marking indefinite protection
--------------------------------------------------------------------------------
-- This table contains values passed to the expiry parameter that mean the page
-- is protected indefinitely.
indefStrings = {
['indef'] = true,
['indefinite'] = true,
['indefinitely'] = true,
['infinite'] = true,
},
--------------------------------------------------------------------------------
-- Group hierarchy
--------------------------------------------------------------------------------
-- This table maps each group to all groups that have a superset of the original
-- group's page editing permissions.
hierarchy = {
sysop = {},
reviewer = {'sysop'},
filemover = {'sysop'},
templateeditor = {'sysop'},
extendedconfirmed = {'sysop'},
autoconfirmed = {'reviewer', 'filemover', 'templateeditor', 'extendedconfirmed'},
user = {'autoconfirmed'},
['*'] = {'user'}
},
--------------------------------------------------------------------------------
-- Wrapper templates and their default arguments
--------------------------------------------------------------------------------
-- This table contains wrapper templates used with the module, and their
-- default arguments. Templates specified in this table should contain the
-- following invocation, and no other template content:
--
-- {{#invoke:Protection banner|main}}
--
-- If other content is desired, it can be added between
-- <noinclude>...</noinclude> tags.
--
-- When a user calls one of these wrapper templates, they will use the
-- default arguments automatically. However, users can override any of the
-- arguments.
wrappers = {
['Template:Pp'] = {},
['Template:Protection padlock'] = {},
['Template:Pp-extended'] = {'ecp'},
['Template:Pp-blp'] = {'blp'},
-- we don't need Template:Pp-create
['Template:Pp-dispute'] = {'dispute'},
['Template:Pp-main-page'] = {'mainpage'},
['Template:Pp-move'] = {action = 'move', catonly = 'yes'},
['Template:Pp-move-dispute'] = {'dispute', action = 'move', catonly = 'yes'},
-- we don't need Template:Pp-move-indef
['Template:Pp-move-vandalism'] = {'vandalism', action = 'move', catonly = 'yes'},
['Template:Pp-office'] = {'office'},
['Template:Pp-office-dmca'] = {'dmca'},
['Template:Pp-pc'] = {action = 'autoreview', small = true},
['Template:Pp-pc1'] = {action = 'autoreview', small = true},
['Template:Pp-reset'] = {'reset'},
['Template:Pp-semi-indef'] = {small = true},
['Template:Pp-sock'] = {'sock'},
['Template:Pp-template'] = {'template', small = true},
['Template:Pp-upload'] = {action = 'upload'},
['Template:Pp-usertalk'] = {'usertalk'},
['Template:Pp-vandalism'] = {'vandalism'},
},
--------------------------------------------------------------------------------
--
-- MESSAGES
--
--------------------------------------------------------------------------------
msg = {
--------------------------------------------------------------------------------
-- Intro blurb and intro fragment
--------------------------------------------------------------------------------
-- These messages specify what is produced by the ${INTROBLURB} and
-- ${INTROFRAGMENT} parameters. If the protection is temporary they use the
-- intro-blurb-expiry or intro-fragment-expiry, and if not they use
-- intro-blurb-noexpiry or intro-fragment-noexpiry.
-- It is possible to use banner parameters in these messages.
['intro-blurb-expiry'] = '${PROTECTIONBLURB} until ${EXPIRY}.',
['intro-blurb-noexpiry'] = '${PROTECTIONBLURB}.',
['intro-fragment-expiry'] = '${PROTECTIONBLURB} until ${EXPIRY},',
['intro-fragment-noexpiry'] = '${PROTECTIONBLURB}',
--------------------------------------------------------------------------------
-- Tooltip blurb
--------------------------------------------------------------------------------
-- These messages specify what is produced by the ${TOOLTIPBLURB} parameter.
-- If the protection is temporary the tooltip-blurb-expiry message is used, and
-- if not the tooltip-blurb-noexpiry message is used.
-- It is possible to use banner parameters in these messages.
['tooltip-blurb-expiry'] = 'This ${PAGETYPE} is ${PROTECTIONLEVEL} until ${EXPIRY}.',
['tooltip-blurb-noexpiry'] = 'This ${PAGETYPE} is ${PROTECTIONLEVEL}.',
['tooltip-fragment-expiry'] = 'This ${PAGETYPE} is ${PROTECTIONLEVEL} until ${EXPIRY},',
['tooltip-fragment-noexpiry'] = 'This ${PAGETYPE} is ${PROTECTIONLEVEL}',
--------------------------------------------------------------------------------
-- Special explanation blurb
--------------------------------------------------------------------------------
-- An explanation blurb for pages that cannot be unprotected, e.g. for pages
-- in the MediaWiki namespace.
-- It is possible to use banner parameters in this message.
['explanation-blurb-nounprotect'] = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' Please discuss any changes on the ${TALKPAGE}; you'
.. ' may ${EDITREQUEST} to ask an'
.. ' [[Wikipedia:Administrators|administrator]] to make an edit if it'
.. ' is [[Help:Minor edit#When to mark an edit as a minor edit'
.. '|uncontroversial]] or supported by [[Wikipedia:Consensus'
.. '|consensus]].',
--------------------------------------------------------------------------------
-- Protection log display values
--------------------------------------------------------------------------------
-- These messages determine the display values for the protection log link
-- or the pending changes log link produced by the ${PROTECTIONLOG} parameter.
-- It is possible to use banner parameters in these messages.
['protection-log-display'] = 'protection log',
['pc-log-display'] = 'pending changes log',
--------------------------------------------------------------------------------
-- Current version display values
--------------------------------------------------------------------------------
-- These messages determine the display values for the page history link
-- or the move log link produced by the ${CURRENTVERSION} parameter.
-- It is possible to use banner parameters in these messages.
['current-version-move-display'] = 'current title',
['current-version-edit-display'] = 'current version',
--------------------------------------------------------------------------------
-- Talk page
--------------------------------------------------------------------------------
-- This message determines the display value of the talk page link produced
-- with the ${TALKPAGE} parameter.
-- It is possible to use banner parameters in this message.
['talk-page-link-display'] = 'talk page',
--------------------------------------------------------------------------------
-- Edit requests
--------------------------------------------------------------------------------
-- This message determines the display value of the edit request link produced
-- with the ${EDITREQUEST} parameter.
-- It is possible to use banner parameters in this message.
['edit-request-display'] = 'submit an edit request',
--------------------------------------------------------------------------------
-- Expiry date format
--------------------------------------------------------------------------------
-- This is the format for the blurb expiry date. It should be valid input for
-- the first parameter of the #time parser function.
['expiry-date-format'] = 'F j, Y "at" H:i e',
--------------------------------------------------------------------------------
-- Tracking categories
--------------------------------------------------------------------------------
-- These messages determine which tracking categories the module outputs.
['tracking-category-incorrect'] = 'Wikipedia pages with incorrect protection templates',
['tracking-category-template'] = 'Wikipedia template-protected pages other than templates and modules',
--------------------------------------------------------------------------------
-- Images
--------------------------------------------------------------------------------
-- These are images that are not defined by their protection action and protection level.
['image-filename-indef'] = 'Full-protection-shackle.svg',
['image-filename-default'] = 'Transparent.gif',
--------------------------------------------------------------------------------
-- End messages
--------------------------------------------------------------------------------
}
--------------------------------------------------------------------------------
-- End configuration
--------------------------------------------------------------------------------
}
g4f6dkimoikv05ulbf832zu7omkf2fc
740062
740061
2026-05-01T19:16:16Z
Novem Linguae
49714
1 revision imported from [[:en:Module:Protection_banner/config]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740061
Scribunto
text/plain
-- This module provides configuration data for [[Module:Protection banner]].
return {
--------------------------------------------------------------------------------
--
-- BANNER DATA
--
--------------------------------------------------------------------------------
--[[
-- Banner data consists of six fields:
-- * text - the main protection text that appears at the top of protection
-- banners.
-- * explanation - the text that appears below the main protection text, used
-- to explain the details of the protection.
-- * tooltip - the tooltip text you see when you move the mouse over a small
-- padlock icon.
-- * link - the page that the small padlock icon links to.
-- * alt - the alt text for the small padlock icon. This is also used as tooltip
-- text for the large protection banners.
-- * image - the padlock image used in both protection banners and small padlock
-- icons.
--
-- The module checks in three separate tables to find a value for each field.
-- First it checks the banners table, which has values specific to the reason
-- for the page being protected. Then the module checks the defaultBanners
-- table, which has values specific to each protection level. Finally, the
-- module checks the masterBanner table, which holds data for protection
-- templates to use if no data has been found in the previous two tables.
--
-- The values in the banner data can take parameters. These are specified
-- using ${TEXTLIKETHIS} (a dollar sign preceding a parameter name
-- enclosed in curly braces).
--
-- Available parameters:
--
-- ${CURRENTVERSION} - a link to the page history or the move log, with the
-- display message "current-version-edit-display" or
-- "current-version-move-display".
--
-- ${EDITREQUEST} - a link to create an edit request for the current page.
--
-- ${EXPLANATIONBLURB} - an explanation blurb, e.g. "Please discuss any changes
-- on the talk page; you may submit a request to ask an administrator to make
-- an edit if it is minor or supported by consensus."
--
-- ${IMAGELINK} - a link to set the image to, depending on the protection
-- action and protection level.
--
-- ${INTROBLURB} - the PROTECTIONBLURB parameter, plus the expiry if an expiry
-- is set. E.g. "Editing of this page by new or unregistered users is currently
-- disabled until dd Month YYYY."
--
-- ${INTROFRAGMENT} - the same as ${INTROBLURB}, but without final punctuation
-- so that it can be used in run-on sentences.
--
-- ${PAGETYPE} - the type of the page, e.g. "article" or "template".
-- Defined in the cfg.pagetypes table.
--
-- ${PROTECTIONBLURB} - a blurb explaining the protection level of the page, e.g.
-- "Editing of this page by new or unregistered users is currently disabled"
--
-- ${PROTECTIONDATE} - the protection date, if it has been supplied to the
-- template.
--
-- ${PROTECTIONLEVEL} - the protection level, e.g. "fully protected" or
-- "semi-protected".
--
-- ${PROTECTIONLOG} - a link to the protection log or the pending changes log,
-- depending on the protection action.
--
-- ${TALKPAGE} - a link to the talk page. If a section is specified, links
-- straight to that talk page section.
--
-- ${TOOLTIPBLURB} - uses the PAGETYPE, PROTECTIONTYPE and EXPIRY parameters to
-- create a blurb like "This template is semi-protected", or "This article is
-- move-protected until DD Month YYYY".
--
-- ${VANDAL} - links for the specified username (or the root page name)
-- using Module:Vandal-m.
--
-- Functions
--
-- For advanced users, it is possible to use Lua functions instead of strings
-- in the banner config tables. Using functions gives flexibility that is not
-- possible just by using parameters. Functions take two arguments, the
-- protection object and the template arguments, and they must output a string.
--
-- For example:
--
-- text = function (protectionObj, args)
-- if protectionObj.level == 'autoconfirmed' then
-- return 'foo'
-- else
-- return 'bar'
-- end
-- end
--
-- Some protection object properties and methods that may be useful:
-- protectionObj.action - the protection action
-- protectionObj.level - the protection level
-- protectionObj.reason - the protection reason
-- protectionObj.expiry - the expiry. Nil if unset, the string "indef" if set
-- to indefinite, and the protection time in unix time if temporary.
-- protectionObj.protectionDate - the protection date in unix time, or nil if
-- unspecified.
-- protectionObj.bannerConfig - the banner config found by the module. Beware
-- of editing the config field used by the function, as it could create an
-- infinite loop.
-- protectionObj:isProtected - returns a boolean showing whether the page is
-- protected.
-- protectionObj:isTemporary - returns a boolean showing whether the expiry is
-- temporary.
-- protectionObj:isIncorrect - returns a boolean showing whether the protection
-- template is incorrect.
--]]
-- The master banner data, used if no values have been found in banners or
-- defaultBanners.
masterBanner = {
text = '${INTROBLURB}',
explanation = '${EXPLANATIONBLURB}',
tooltip = '${TOOLTIPBLURB}',
link = '${IMAGELINK}',
alt = 'Page ${PROTECTIONLEVEL}'
},
-- The default banner data. This holds banner data for different protection
-- levels.
-- *required* - this table needs edit, move, autoreview and upload subtables.
defaultBanners = {
edit = {},
move = {},
autoreview = {
default = {
alt = 'Page protected with pending changes',
tooltip = 'All edits by unregistered and new users are subject to review prior to becoming visible to unregistered users',
image = 'Pending-protection-shackle.svg'
}
},
upload = {}
},
-- The banner data. This holds banner data for different protection reasons.
-- In fact, the reasons specified in this table control which reasons are
-- valid inputs to the first positional parameter.
--
-- There is also a non-standard "description" field that can be used for items
-- in this table. This is a description of the protection reason for use in the
-- module documentation.
--
-- *required* - this table needs edit, move, autoreview and upload subtables.
banners = {
edit = {
blp = {
description = 'For pages protected to promote compliance with the'
.. ' [[Wikipedia:Biographies of living persons'
.. '|biographies of living persons]] policy',
text = '${INTROFRAGMENT} to promote compliance with'
.. ' [[Wikipedia:Biographies of living persons'
.. "|Wikipedia's policy on the biographies"
.. ' of living people]].',
tooltip = '${TOOLTIPFRAGMENT} to promote compliance with the policy on'
.. ' biographies of living persons',
},
deceased = {
description = 'For user pages of Wikipedia users who are deceased',
text = '${INTROFRAGMENT} to prevent vandalism of a deceased'
.. ' Wikipedian\'s user page.'
.. ' A family member who wishes to edit this user page can use this'
.. ' user\'s ${TALKPAGE} or submit a request to [[Wikipedia:VRT|the'
.. ' Volunteer Response Team]].',
tooltip = '${TOOLTIPFRAGMENT} because this Wikipedian is deceased'
},
dmca = {
description = 'For pages protected by the Wikimedia Foundation'
.. ' due to [[Digital Millennium Copyright Act]] takedown requests',
explanation = function (protectionObj, args)
local ret = 'Pursuant to a rights owner notice under the Digital'
.. ' Millennium Copyright Act (DMCA) regarding some content'
.. ' in this article, the Wikimedia Foundation acted under'
.. ' applicable law and took down and restricted the content'
.. ' in question.'
if args.notice then
ret = ret .. ' A copy of the received notice can be found here: '
.. args.notice .. '.'
end
ret = ret .. ' For more information, including websites discussing'
.. ' how to file a counter-notice, please see'
.. " [[Wikipedia:Office actions]] and the article's ${TALKPAGE}."
.. "'''Do not remove this template from the article until the"
.. " restrictions are withdrawn'''."
return ret
end,
image = 'Office-protection-shackle.svg',
link = 'Wikipedia:Protection policy#office',
},
dispute = {
description = 'For pages protected due to editing disputes',
text = function (protectionObj, args)
-- Find the value of "disputes".
local display = 'disputes'
local disputes
if args.section then
disputes = string.format(
'[[%s:%s#%s|%s]]',
mw.site.namespaces[protectionObj.title.namespace].talk.name,
protectionObj.title.text,
args.section,
display
)
else
disputes = display
end
-- Make the blurb, depending on the expiry.
local msg
if type(protectionObj.expiry) == 'number' then
msg = '${INTROFRAGMENT} or until editing %s have been resolved.'
else
msg = '${INTROFRAGMENT} until editing %s have been resolved.'
end
return string.format(msg, disputes)
end,
explanation = "This protection is '''not''' an endorsement of the"
.. ' ${CURRENTVERSION}. ${EXPLANATIONBLURB}',
tooltip = '${TOOLTIPFRAGMENT} due to editing disputes',
},
ecp = {
description = 'For articles in topic areas authorized by'
.. ' [[Wikipedia:Arbitration Committee|ArbCom]] or'
.. ' meets the criteria for community use',
alt = 'Extended-protected ${PAGETYPE}',
},
mainpage = {
description = 'For pages protected for being displayed on the [[Main Page]]',
text = 'This file is currently'
.. ' [[Wikipedia:This page is protected|protected]] from'
.. ' editing because it is currently or will soon be displayed'
.. ' on the [[Main Page]].',
explanation = 'Images on the Main Page are protected due to their high'
.. ' visibility. Please discuss any necessary changes on the ${TALKPAGE}.'
.. '<br /><span style="font-size:90%;">'
.. "'''Administrators:''' Once this image is definitely off the Main Page,"
.. ' please unprotect this file, or reduce to semi-protection,'
.. ' as appropriate.</span>',
},
office = {
description = 'For pages protected by the Wikimedia Foundation',
text = function (protectionObj, args)
local ret = 'This ${PAGETYPE} is currently under the'
.. ' scrutiny of the'
.. ' [[Wikipedia:Office actions|Wikimedia Foundation Office]]'
.. ' and is protected.'
if protectionObj.protectionDate then
ret = ret .. ' It has been protected since ${PROTECTIONDATE}.'
end
return ret
end,
explanation = "If you can edit this page, please discuss all changes and"
.. " additions on the ${TALKPAGE} first. '''Do not remove protection from this"
.. " page unless you are authorized by the Wikimedia Foundation to do"
.. " so.'''",
image = 'Office-protection-shackle.svg',
link = 'Wikipedia:Protection policy#office',
},
reset = {
description = 'For pages protected by the Wikimedia Foundation and'
.. ' "reset" to a bare-bones version',
text = 'This ${PAGETYPE} is currently under the'
.. ' scrutiny of the'
.. ' [[Wikipedia:Office actions|Wikimedia Foundation Office]]'
.. ' and is protected.',
explanation = function (protectionObj, args)
local ret = ''
if protectionObj.protectionDate then
ret = ret .. 'On ${PROTECTIONDATE} this ${PAGETYPE} was'
else
ret = ret .. 'This ${PAGETYPE} has been'
end
ret = ret .. ' reduced to a'
.. ' simplified, "bare bones" version so that it may be completely'
.. ' rewritten to ensure it meets the policies of'
.. ' [[WP:NPOV|Neutral Point of View]] and [[WP:V|Verifiability]].'
.. ' Standard Wikipedia policies will apply to its rewriting—which'
.. ' will eventually be open to all editors—and will be strictly'
.. ' enforced. The ${PAGETYPE} has been ${PROTECTIONLEVEL} while'
.. ' it is being rebuilt.\n\n'
.. 'Any insertion of material directly from'
.. ' pre-protection revisions of the ${PAGETYPE} will be removed, as'
.. ' will any material added to the ${PAGETYPE} that is not properly'
.. ' sourced. The associated talk page(s) were also cleared on the'
.. " same date.\n\n"
.. "If you can edit this page, please discuss all changes and"
.. " additions on the ${TALKPAGE} first. '''Do not override"
.. " this action, and do not remove protection from this page,"
.. " unless you are authorized by the Wikimedia Foundation"
.. " to do so. No editor may remove this notice.'''"
return ret
end,
image = 'Office-protection-shackle.svg',
link = 'Wikipedia:Protection policy#office',
},
sock = {
description = 'For pages protected due to'
.. ' [[Wikipedia:Sock puppetry|sock puppetry]]',
text = '${INTROFRAGMENT} to prevent [[Wikipedia:Sock puppetry|sock puppets]] of'
.. ' [[Wikipedia:Blocking policy|blocked]] or'
.. ' [[Wikipedia:Banning policy|banned users]]'
.. ' from editing it.',
tooltip = '${TOOLTIPFRAGMENT} to prevent sock puppets of blocked or banned users from'
.. ' editing it',
},
template = {
description = 'For [[Wikipedia:High-risk templates|high-risk]]'
.. ' templates and Lua modules',
text = 'This is a permanently [[Wikipedia:Protection policy|protected]] ${PAGETYPE},'
.. ' as it is [[Wikipedia:High-risk templates|high-risk]].',
explanation = 'Please discuss any changes on the ${TALKPAGE}; you may'
.. ' ${EDITREQUEST} to ask an'
.. ' [[Wikipedia:Administrators|administrator]] or'
.. ' [[Wikipedia:Template editor|template editor]] to make an edit if'
.. ' it is [[Help:Minor edit#When to mark an edit as a minor edit'
.. '|uncontroversial]] or supported by'
.. ' [[Wikipedia:Consensus|consensus]]. You can also'
.. ' [[Wikipedia:Requests for page protection|request]] that the page be'
.. ' unprotected.',
tooltip = 'This high-risk ${PAGETYPE} is permanently ${PROTECTIONLEVEL}'
.. ' to prevent vandalism',
alt = 'Permanently protected ${PAGETYPE}',
},
usertalk = {
description = 'For pages protected against disruptive edits by a'
.. ' particular user',
text = '${INTROFRAGMENT} to prevent ${VANDAL} from using it to make disruptive edits,'
.. ' such as abusing the'
.. ' {{[[Template:unblock|unblock]]}} template.',
explanation = 'If you cannot edit this user talk page and you need to'
.. ' make a change or leave a message, you can'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for edits to a protected page'
.. '|request an edit]],'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for reduction in protection level'
.. '|request unprotection]],'
.. ' [[Special:Userlogin|log in]],'
.. ' or [[Special:UserLogin/signup|create an account]].',
},
vandalism = {
description = 'For pages protected against'
.. ' [[Wikipedia:Vandalism|vandalism]]',
text = '${INTROFRAGMENT} due to [[Wikipedia:Vandalism|vandalism]].',
explanation = function (protectionObj, args)
local ret = ''
if protectionObj.level == 'sysop' then
ret = ret .. "This protection is '''not''' an endorsement of the"
.. ' ${CURRENTVERSION}. '
end
return ret .. '${EXPLANATIONBLURB}'
end,
tooltip = '${TOOLTIPFRAGMENT} due to vandalism',
}
},
move = {
dispute = {
description = 'For pages protected against page moves due to'
.. ' disputes over the page title',
explanation = "This protection is '''not''' an endorsement of the"
.. ' ${CURRENTVERSION}. ${EXPLANATIONBLURB}',
image = 'Move-protection-shackle.svg'
},
vandalism = {
description = 'For pages protected against'
.. ' [[Wikipedia:Vandalism#Page-move vandalism'
.. ' |page-move vandalism]]'
}
},
autoreview = {},
upload = {}
},
--------------------------------------------------------------------------------
--
-- GENERAL DATA TABLES
--
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Protection blurbs
--------------------------------------------------------------------------------
-- This table produces the protection blurbs available with the
-- ${PROTECTIONBLURB} parameter. It is sorted by protection action and
-- protection level, and is checked by the module in the following order:
-- 1. page's protection action, page's protection level
-- 2. page's protection action, default protection level
-- 3. "edit" protection action, default protection level
--
-- It is possible to use banner parameters inside this table.
-- *required* - this table needs edit, move, autoreview and upload subtables.
protectionBlurbs = {
edit = {
default = 'This ${PAGETYPE} is currently [[Wikipedia:Protection policy#full|'
.. 'protected]] from editing',
templateeditor = 'This ${PAGETYPE} is currently [[Wikipedia:Protection policy#template'
.. '|protected]] from editing',
autoconfirmed = 'Editing of this ${PAGETYPE} by [[Wikipedia:User access'
.. ' levels#New users|new]] or [[Wikipedia:User access levels#Unregistered'
.. ' users|unregistered]] users is currently [[Wikipedia:Protection'
.. ' policy#semi|disabled]]',
extendedconfirmed = 'This ${PAGETYPE} is currently under [[Wikipedia:Protection'
.. ' policy#extended|extended confirmed protection]]',
},
move = {
default = 'This ${PAGETYPE} is currently [[Wikipedia:Protection policy#Move'
.. ' protection|protected]] from [[Help:Moving a page|page moves]]'
},
autoreview = {
default = 'All edits made to this ${PAGETYPE} by'
.. ' [[Wikipedia:User access levels#New users|new]] or'
.. ' [[Wikipedia:User access levels#Unregistered users|unregistered]]'
.. ' users are currently'
.. ' [[Wikipedia:Pending changes|subject to review]]'
},
upload = {
default = 'Uploading new versions of this ${PAGETYPE} is currently disabled'
}
},
--------------------------------------------------------------------------------
-- Explanation blurbs
--------------------------------------------------------------------------------
-- This table produces the explanation blurbs available with the
-- ${EXPLANATIONBLURB} parameter. It is sorted by protection action,
-- protection level, and whether the page is a talk page or not. If the page is
-- a talk page it will have a talk key of "talk"; otherwise it will have a talk
-- key of "subject". The table is checked in the following order:
-- 1. page's protection action, page's protection level, page's talk key
-- 2. page's protection action, page's protection level, default talk key
-- 3. page's protection action, default protection level, page's talk key
-- 4. page's protection action, default protection level, default talk key
--
-- It is possible to use banner parameters inside this table.
-- *required* - this table needs edit, move, autoreview and upload subtables.
explanationBlurbs = {
edit = {
autoconfirmed = {
subject = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details. If you'
.. ' cannot edit this ${PAGETYPE} and you wish to make a change, you can'
.. ' ${EDITREQUEST}, discuss changes on the ${TALKPAGE},'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for reduction in protection level'
.. '|request unprotection]], [[Special:Userlogin|log in]], or'
.. ' [[Special:UserLogin/signup|create an account]].',
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details. If you'
.. ' cannot edit this ${PAGETYPE} and you wish to make a change, you can'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for reduction in protection level'
.. '|request unprotection]], [[Special:Userlogin|log in]], or'
.. ' [[Special:UserLogin/signup|create an account]].',
},
extendedconfirmed = {
default = 'Extended confirmed protection prevents edits from all unregistered editors'
.. ' and registered users with fewer than 30 days tenure and 500 edits.'
.. ' The [[Wikipedia:Protection policy#extended|policy on community use]]'
.. ' specifies that extended confirmed protection can be applied to combat'
.. ' disruption, if semi-protection has proven to be ineffective.'
.. ' Extended confirmed protection may also be applied to enforce'
.. ' [[Wikipedia:Arbitration Committee|arbitration sanctions]].'
.. ' Please discuss any changes on the ${TALKPAGE}; you may'
.. ' ${EDITREQUEST} to ask for uncontroversial changes supported by'
.. ' [[Wikipedia:Consensus|consensus]].'
},
default = {
subject = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' Please discuss any changes on the ${TALKPAGE}; you'
.. ' may ${EDITREQUEST} to ask an'
.. ' [[Wikipedia:Administrators|administrator]] to make an edit if it'
.. ' is [[Help:Minor edit#When to mark an edit as a minor edit'
.. '|uncontroversial]] or supported by [[Wikipedia:Consensus'
.. '|consensus]]. You may also [[Wikipedia:Requests for'
.. ' page protection#Current requests for reduction in protection level'
.. '|request]] that this page be unprotected.',
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' You may [[Wikipedia:Requests for page'
.. ' protection#Current requests for edits to a protected page|request an'
.. ' edit]] to this page, or [[Wikipedia:Requests for'
.. ' page protection#Current requests for reduction in protection level'
.. '|ask]] for it to be unprotected.'
}
},
move = {
default = {
subject = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' The page may still be edited but cannot be moved'
.. ' until unprotected. Please discuss any suggested moves on the'
.. ' ${TALKPAGE} or at [[Wikipedia:Requested moves]]. You can also'
.. ' [[Wikipedia:Requests for page protection|request]] that the page be'
.. ' unprotected.',
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' The page may still be edited but cannot be moved'
.. ' until unprotected. Please discuss any suggested moves at'
.. ' [[Wikipedia:Requested moves]]. You can also'
.. ' [[Wikipedia:Requests for page protection|request]] that the page be'
.. ' unprotected.'
}
},
autoreview = {
default = {
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' Edits to this ${PAGETYPE} by new and unregistered users'
.. ' will not be visible to readers until they are accepted by'
.. ' a reviewer. To avoid the need for your edits to be'
.. ' reviewed, you may'
.. ' [[Wikipedia:Requests for page protection'
.. '#Current requests for reduction in protection level'
.. '|request unprotection]], [[Special:Userlogin|log in]], or'
.. ' [[Special:UserLogin/signup|create an account]].'
},
},
upload = {
default = {
default = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' The page may still be edited but new versions of the file'
.. ' cannot be uploaded until it is unprotected. You can'
.. ' request that a new version be uploaded by using a'
.. ' [[Wikipedia:Edit requests|protected edit request]], or you'
.. ' can [[Wikipedia:Requests for page protection|request]]'
.. ' that the file be unprotected.'
}
}
},
--------------------------------------------------------------------------------
-- Protection levels
--------------------------------------------------------------------------------
-- This table provides the data for the ${PROTECTIONLEVEL} parameter, which
-- produces a short label for different protection levels. It is sorted by
-- protection action and protection level, and is checked in the following
-- order:
-- 1. page's protection action, page's protection level
-- 2. page's protection action, default protection level
-- 3. "edit" protection action, default protection level
--
-- It is possible to use banner parameters inside this table.
-- *required* - this table needs edit, move, autoreview and upload subtables.
protectionLevels = {
edit = {
default = 'protected',
templateeditor = 'template-protected',
extendedconfirmed = 'extended-confirmed-protected',
autoconfirmed = 'semi-protected',
},
move = {
default = 'move-protected'
},
autoreview = {
},
upload = {
default = 'upload-protected'
}
},
--------------------------------------------------------------------------------
-- Images
--------------------------------------------------------------------------------
-- This table lists different padlock images for each protection action and
-- protection level. It is used if an image is not specified in any of the
-- banner data tables, and if the page does not satisfy the conditions for using
-- the ['image-filename-indef'] image. It is checked in the following order:
-- 1. page's protection action, page's protection level
-- 2. page's protection action, default protection level
images = {
edit = {
default = 'Full-protection-shackle.svg',
templateeditor = 'Template-protection-shackle.svg',
extendedconfirmed = 'Extended-protection-shackle.svg',
autoconfirmed = 'Semi-protection-shackle.svg'
},
move = {
default = 'Move-protection-shackle.svg',
},
autoreview = {
default = 'Pending-protection-shackle.svg'
},
upload = {
default = 'Upload-protection-shackle.svg'
}
},
-- Pages with a reason specified in this table will show the special "indef"
-- padlock, defined in the 'image-filename-indef' message, if no expiry is set.
indefImageReasons = {
template = true
},
--------------------------------------------------------------------------------
-- Image links
--------------------------------------------------------------------------------
-- This table provides the data for the ${IMAGELINK} parameter, which gets
-- the image link for small padlock icons based on the page's protection action
-- and protection level. It is checked in the following order:
-- 1. page's protection action, page's protection level
-- 2. page's protection action, default protection level
-- 3. "edit" protection action, default protection level
--
-- It is possible to use banner parameters inside this table.
-- *required* - this table needs edit, move, autoreview and upload subtables.
imageLinks = {
edit = {
default = 'Wikipedia:Protection policy#full',
templateeditor = 'Wikipedia:Protection policy#template',
extendedconfirmed = 'Wikipedia:Protection policy#extended',
autoconfirmed = 'Wikipedia:Protection policy#semi'
},
move = {
default = 'Wikipedia:Protection policy#move'
},
autoreview = {
default = 'Wikipedia:Protection policy#pending'
},
upload = {
default = 'Wikipedia:Protection policy#upload'
}
},
--------------------------------------------------------------------------------
-- Padlock indicator names
--------------------------------------------------------------------------------
-- This table provides the "name" attribute for the <indicator> extension tag
-- with which small padlock icons are generated. All indicator tags on a page
-- are displayed in alphabetical order based on this attribute, and with
-- indicator tags with duplicate names, the last tag on the page wins.
-- The attribute is chosen based on the protection action; table keys must be a
-- protection action name or the string "default".
padlockIndicatorNames = {
autoreview = 'pp-autoreview',
default = 'pp-default'
},
--------------------------------------------------------------------------------
-- Protection categories
--------------------------------------------------------------------------------
--[[
-- The protection categories are stored in the protectionCategories table.
-- Keys to this table are made up of the following strings:
--
-- 1. the expiry date
-- 2. the namespace
-- 3. the protection reason (e.g. "dispute" or "vandalism")
-- 4. the protection level (e.g. "sysop" or "autoconfirmed")
-- 5. the action (e.g. "edit" or "move")
--
-- When the module looks up a category in the table, first it will will check to
-- see a key exists that corresponds to all five parameters. For example, a
-- user page semi-protected from vandalism for two weeks would have the key
-- "temp-user-vandalism-autoconfirmed-edit". If no match is found, the module
-- changes the first part of the key to "all" and checks the table again. It
-- keeps checking increasingly generic key combinations until it finds the
-- field, or until it reaches the key "all-all-all-all-all".
--
-- The module uses a binary matrix to determine the order in which to search.
-- This is best demonstrated by a table. In this table, the "0" values
-- represent "all", and the "1" values represent the original data (e.g.
-- "indef" or "file" or "vandalism").
--
-- expiry namespace reason level action
-- order
-- 1 1 1 1 1 1
-- 2 0 1 1 1 1
-- 3 1 0 1 1 1
-- 4 0 0 1 1 1
-- 5 1 1 0 1 1
-- 6 0 1 0 1 1
-- 7 1 0 0 1 1
-- 8 0 0 0 1 1
-- 9 1 1 1 0 1
-- 10 0 1 1 0 1
-- 11 1 0 1 0 1
-- 12 0 0 1 0 1
-- 13 1 1 0 0 1
-- 14 0 1 0 0 1
-- 15 1 0 0 0 1
-- 16 0 0 0 0 1
-- 17 1 1 1 1 0
-- 18 0 1 1 1 0
-- 19 1 0 1 1 0
-- 20 0 0 1 1 0
-- 21 1 1 0 1 0
-- 22 0 1 0 1 0
-- 23 1 0 0 1 0
-- 24 0 0 0 1 0
-- 25 1 1 1 0 0
-- 26 0 1 1 0 0
-- 27 1 0 1 0 0
-- 28 0 0 1 0 0
-- 29 1 1 0 0 0
-- 30 0 1 0 0 0
-- 31 1 0 0 0 0
-- 32 0 0 0 0 0
--
-- In this scheme the action has the highest priority, as it is the last
-- to change, and the expiry has the least priority, as it changes the most.
-- The priorities of the expiry, the protection level and the action are
-- fixed, but the priorities of the reason and the namespace can be swapped
-- through the use of the cfg.bannerDataNamespaceHasPriority table.
--]]
-- If the reason specified to the template is listed in this table,
-- namespace data will take priority over reason data in the protectionCategories
-- table.
reasonsWithNamespacePriority = {
vandalism = true,
},
-- The string to use as a namespace key for the protectionCategories table for each
-- namespace number.
categoryNamespaceKeys = {
[ 2] = 'user',
[ 3] = 'user',
[ 4] = 'project',
[ 6] = 'file',
[ 8] = 'mediawiki',
[ 10] = 'template',
[ 12] = 'project',
[ 14] = 'category',
[100] = 'portal',
[828] = 'module',
},
protectionCategories = {
['all|all|all|all|all'] = 'Wikipedia fully protected pages',
['all|all|office|all|all'] = 'Wikipedia Office-protected pages',
['all|all|reset|all|all'] = 'Wikipedia Office-protected pages',
['all|all|dmca|all|all'] = 'Wikipedia Office-protected pages',
['all|all|mainpage|all|all'] = 'Wikipedia fully protected main page files',
['all|all|all|extendedconfirmed|all'] = 'Wikipedia extended-confirmed-protected pages',
['all|all|ecp|extendedconfirmed|all'] = 'Wikipedia extended-confirmed-protected pages',
['all|template|all|all|edit'] = 'Wikipedia fully protected templates',
['all|all|all|autoconfirmed|edit'] = 'Wikipedia semi-protected pages',
['indef|all|all|autoconfirmed|edit'] = 'Wikipedia indefinitely semi-protected pages',
['all|all|blp|autoconfirmed|edit'] = 'Wikipedia indefinitely semi-protected biographies of living people',
['temp|all|blp|autoconfirmed|edit'] = 'Wikipedia temporarily semi-protected biographies of living people',
['all|all|dispute|autoconfirmed|edit'] = 'Wikipedia pages semi-protected due to dispute',
['all|all|sock|autoconfirmed|edit'] = 'Wikipedia pages semi-protected from banned users',
['all|all|vandalism|autoconfirmed|edit'] = 'Wikipedia pages semi-protected against vandalism',
['all|category|all|autoconfirmed|edit'] = 'Wikipedia semi-protected categories',
['all|file|all|autoconfirmed|edit'] = 'Wikipedia semi-protected files',
['all|portal|all|autoconfirmed|edit'] = 'Wikipedia semi-protected portals',
['all|project|all|autoconfirmed|edit'] = 'Wikipedia semi-protected project pages',
['all|talk|all|autoconfirmed|edit'] = 'Wikipedia semi-protected talk pages',
['all|template|all|autoconfirmed|edit'] = 'Wikipedia semi-protected templates',
['all|user|all|autoconfirmed|edit'] = 'Wikipedia semi-protected user and user talk pages',
['all|all|all|templateeditor|move'] = 'Wikipedia template-protected pages other than templates and modules',
['all|all|all|templateeditor|edit'] = 'Wikipedia template-protected pages other than templates and modules',
['all|template|all|templateeditor|edit'] = 'Wikipedia template-protected templates',
['all|template|all|templateeditor|move'] = 'Wikipedia template-protected templates', -- move-protected templates
['all|all|blp|sysop|edit'] = 'Wikipedia indefinitely protected biographies of living people',
['temp|all|blp|sysop|edit'] = 'Wikipedia temporarily protected biographies of living people',
['all|all|dispute|sysop|edit'] = 'Wikipedia pages protected due to dispute',
['all|all|sock|sysop|edit'] = 'Wikipedia pages protected from banned users',
['all|all|vandalism|sysop|edit'] = 'Wikipedia pages protected against vandalism',
['all|category|all|sysop|edit'] = 'Wikipedia fully protected categories',
['all|file|all|sysop|edit'] = 'Wikipedia fully protected files',
['all|project|all|sysop|edit'] = 'Wikipedia fully protected project pages',
['all|talk|all|sysop|edit'] = 'Wikipedia fully protected talk pages',
['all|template|all|extendedconfirmed|edit'] = 'Wikipedia extended-confirmed-protected templates',
['all|template|all|extendedconfirmed|move'] = 'Wikipedia extended-confirmed-protected templates',
['all|template|all|sysop|edit'] = 'Wikipedia fully protected templates',
['all|user|all|sysop|edit'] = 'Wikipedia fully protected user and user talk pages',
['all|module|all|all|edit'] = 'Wikipedia fully protected modules',
['all|module|all|templateeditor|edit'] = 'Wikipedia template-protected modules',
['all|module|all|extendedconfirmed|edit'] = 'Wikipedia extended-confirmed-protected modules',
['all|module|all|autoconfirmed|edit'] = 'Wikipedia semi-protected modules',
['all|all|all|sysop|move'] = 'Wikipedia move-protected pages',
['indef|all|all|sysop|move'] = 'Wikipedia indefinitely move-protected pages',
['all|all|dispute|sysop|move'] = 'Wikipedia pages move-protected due to dispute',
['all|all|vandalism|sysop|move'] = 'Wikipedia pages move-protected due to vandalism',
['all|portal|all|sysop|move'] = 'Wikipedia move-protected portals',
['all|project|all|sysop|move'] = 'Wikipedia move-protected project pages',
['all|talk|all|sysop|move'] = 'Wikipedia move-protected talk pages',
['all|template|all|sysop|move'] = 'Wikipedia move-protected templates',
['all|user|all|sysop|move'] = 'Wikipedia move-protected user and user talk pages',
['all|all|all|autoconfirmed|autoreview'] = 'Wikipedia pending changes protected pages',
['all|file|all|all|upload'] = 'Wikipedia upload-protected files',
},
--------------------------------------------------------------------------------
-- Expiry category config
--------------------------------------------------------------------------------
-- This table configures the expiry category behaviour for each protection
-- action.
-- * If set to true, setting that action will always categorise the page if
-- an expiry parameter is not set.
-- * If set to false, setting that action will never categorise the page.
-- * If set to nil, the module will categorise the page if:
-- 1) an expiry parameter is not set, and
-- 2) a reason is provided, and
-- 3) the specified reason is not blacklisted in the reasonsWithoutExpiryCheck
-- table.
expiryCheckActions = {
edit = nil,
move = false,
autoreview = true,
upload = false
},
reasonsWithoutExpiryCheck = {
blp = true,
template = true,
},
--------------------------------------------------------------------------------
-- Pagetypes
--------------------------------------------------------------------------------
-- This table produces the page types available with the ${PAGETYPE} parameter.
-- Keys are namespace numbers, or the string "default" for the default value.
pagetypes = {
[0] = 'article',
[6] = 'file',
[10] = 'template',
[14] = 'category',
[828] = 'module',
default = 'page'
},
--------------------------------------------------------------------------------
-- Strings marking indefinite protection
--------------------------------------------------------------------------------
-- This table contains values passed to the expiry parameter that mean the page
-- is protected indefinitely.
indefStrings = {
['indef'] = true,
['indefinite'] = true,
['indefinitely'] = true,
['infinite'] = true,
},
--------------------------------------------------------------------------------
-- Group hierarchy
--------------------------------------------------------------------------------
-- This table maps each group to all groups that have a superset of the original
-- group's page editing permissions.
hierarchy = {
sysop = {},
reviewer = {'sysop'},
filemover = {'sysop'},
templateeditor = {'sysop'},
extendedconfirmed = {'sysop'},
autoconfirmed = {'reviewer', 'filemover', 'templateeditor', 'extendedconfirmed'},
user = {'autoconfirmed'},
['*'] = {'user'}
},
--------------------------------------------------------------------------------
-- Wrapper templates and their default arguments
--------------------------------------------------------------------------------
-- This table contains wrapper templates used with the module, and their
-- default arguments. Templates specified in this table should contain the
-- following invocation, and no other template content:
--
-- {{#invoke:Protection banner|main}}
--
-- If other content is desired, it can be added between
-- <noinclude>...</noinclude> tags.
--
-- When a user calls one of these wrapper templates, they will use the
-- default arguments automatically. However, users can override any of the
-- arguments.
wrappers = {
['Template:Pp'] = {},
['Template:Protection padlock'] = {},
['Template:Pp-extended'] = {'ecp'},
['Template:Pp-blp'] = {'blp'},
-- we don't need Template:Pp-create
['Template:Pp-dispute'] = {'dispute'},
['Template:Pp-main-page'] = {'mainpage'},
['Template:Pp-move'] = {action = 'move', catonly = 'yes'},
['Template:Pp-move-dispute'] = {'dispute', action = 'move', catonly = 'yes'},
-- we don't need Template:Pp-move-indef
['Template:Pp-move-vandalism'] = {'vandalism', action = 'move', catonly = 'yes'},
['Template:Pp-office'] = {'office'},
['Template:Pp-office-dmca'] = {'dmca'},
['Template:Pp-pc'] = {action = 'autoreview', small = true},
['Template:Pp-pc1'] = {action = 'autoreview', small = true},
['Template:Pp-reset'] = {'reset'},
['Template:Pp-semi-indef'] = {small = true},
['Template:Pp-sock'] = {'sock'},
['Template:Pp-template'] = {'template', small = true},
['Template:Pp-upload'] = {action = 'upload'},
['Template:Pp-usertalk'] = {'usertalk'},
['Template:Pp-vandalism'] = {'vandalism'},
},
--------------------------------------------------------------------------------
--
-- MESSAGES
--
--------------------------------------------------------------------------------
msg = {
--------------------------------------------------------------------------------
-- Intro blurb and intro fragment
--------------------------------------------------------------------------------
-- These messages specify what is produced by the ${INTROBLURB} and
-- ${INTROFRAGMENT} parameters. If the protection is temporary they use the
-- intro-blurb-expiry or intro-fragment-expiry, and if not they use
-- intro-blurb-noexpiry or intro-fragment-noexpiry.
-- It is possible to use banner parameters in these messages.
['intro-blurb-expiry'] = '${PROTECTIONBLURB} until ${EXPIRY}.',
['intro-blurb-noexpiry'] = '${PROTECTIONBLURB}.',
['intro-fragment-expiry'] = '${PROTECTIONBLURB} until ${EXPIRY},',
['intro-fragment-noexpiry'] = '${PROTECTIONBLURB}',
--------------------------------------------------------------------------------
-- Tooltip blurb
--------------------------------------------------------------------------------
-- These messages specify what is produced by the ${TOOLTIPBLURB} parameter.
-- If the protection is temporary the tooltip-blurb-expiry message is used, and
-- if not the tooltip-blurb-noexpiry message is used.
-- It is possible to use banner parameters in these messages.
['tooltip-blurb-expiry'] = 'This ${PAGETYPE} is ${PROTECTIONLEVEL} until ${EXPIRY}.',
['tooltip-blurb-noexpiry'] = 'This ${PAGETYPE} is ${PROTECTIONLEVEL}.',
['tooltip-fragment-expiry'] = 'This ${PAGETYPE} is ${PROTECTIONLEVEL} until ${EXPIRY},',
['tooltip-fragment-noexpiry'] = 'This ${PAGETYPE} is ${PROTECTIONLEVEL}',
--------------------------------------------------------------------------------
-- Special explanation blurb
--------------------------------------------------------------------------------
-- An explanation blurb for pages that cannot be unprotected, e.g. for pages
-- in the MediaWiki namespace.
-- It is possible to use banner parameters in this message.
['explanation-blurb-nounprotect'] = 'See the [[Wikipedia:Protection policy|'
.. 'protection policy]] and ${PROTECTIONLOG} for more details.'
.. ' Please discuss any changes on the ${TALKPAGE}; you'
.. ' may ${EDITREQUEST} to ask an'
.. ' [[Wikipedia:Administrators|administrator]] to make an edit if it'
.. ' is [[Help:Minor edit#When to mark an edit as a minor edit'
.. '|uncontroversial]] or supported by [[Wikipedia:Consensus'
.. '|consensus]].',
--------------------------------------------------------------------------------
-- Protection log display values
--------------------------------------------------------------------------------
-- These messages determine the display values for the protection log link
-- or the pending changes log link produced by the ${PROTECTIONLOG} parameter.
-- It is possible to use banner parameters in these messages.
['protection-log-display'] = 'protection log',
['pc-log-display'] = 'pending changes log',
--------------------------------------------------------------------------------
-- Current version display values
--------------------------------------------------------------------------------
-- These messages determine the display values for the page history link
-- or the move log link produced by the ${CURRENTVERSION} parameter.
-- It is possible to use banner parameters in these messages.
['current-version-move-display'] = 'current title',
['current-version-edit-display'] = 'current version',
--------------------------------------------------------------------------------
-- Talk page
--------------------------------------------------------------------------------
-- This message determines the display value of the talk page link produced
-- with the ${TALKPAGE} parameter.
-- It is possible to use banner parameters in this message.
['talk-page-link-display'] = 'talk page',
--------------------------------------------------------------------------------
-- Edit requests
--------------------------------------------------------------------------------
-- This message determines the display value of the edit request link produced
-- with the ${EDITREQUEST} parameter.
-- It is possible to use banner parameters in this message.
['edit-request-display'] = 'submit an edit request',
--------------------------------------------------------------------------------
-- Expiry date format
--------------------------------------------------------------------------------
-- This is the format for the blurb expiry date. It should be valid input for
-- the first parameter of the #time parser function.
['expiry-date-format'] = 'F j, Y "at" H:i e',
--------------------------------------------------------------------------------
-- Tracking categories
--------------------------------------------------------------------------------
-- These messages determine which tracking categories the module outputs.
['tracking-category-incorrect'] = 'Wikipedia pages with incorrect protection templates',
['tracking-category-template'] = 'Wikipedia template-protected pages other than templates and modules',
--------------------------------------------------------------------------------
-- Images
--------------------------------------------------------------------------------
-- These are images that are not defined by their protection action and protection level.
['image-filename-indef'] = 'Full-protection-shackle.svg',
['image-filename-default'] = 'Transparent.gif',
--------------------------------------------------------------------------------
-- End messages
--------------------------------------------------------------------------------
}
--------------------------------------------------------------------------------
-- End configuration
--------------------------------------------------------------------------------
}
g4f6dkimoikv05ulbf832zu7omkf2fc
User:Sam Sailor/test.js
2
98186
740020
739972
2026-05-01T15:32:10Z
Sam Sailor
26820
Test
740020
javascript
text/javascript
//<nowiki>
/* global ComradeLib, mw, $ */
(async function() {
try {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-options.js&action=raw&ctype=text/javascript');
} catch (e) {
console.warn("Comrade: Could not load /comrade-options.js, using fallback defaults.", e);
}
window.ComradeOptions = Object.assign({
version: "0.5.0",
excludedTemplates: [],
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
});
try {
const wikiData = await fetch(url).then(r => r.json());
return wikiData.entities[qid];
} catch (e) {
return null;
}
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
const isRedirect = !!mw.config.get("wgIsRedirect");
const userExclusions = window.comradeQualityExclusions || [];
const allExclusions = [...window.ComradeOptions.excludedTemplates, ...userExclusions];
const exclusionRegexString = allExclusions.map(t => t.replace("Template:", "").replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const exclusionRegex = new RegExp(exclusionRegexString, 'i');
if (isRedirect || exclusionRegex.test(wikitext)) {
console.log("Comrade check skipped: Page is a redirect or excluded via options.");
return;
}
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]].",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += " Also handled [[MOS:BLANKLINE]].";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
tqmdw6mlum43zfigcx55bd2xyo5u9tm
740048
740020
2026-05-01T17:29:04Z
Sam Sailor
26820
Test
740048
javascript
text/javascript
//<nowiki>
/* global ComradeLib, mw, $ */
(async function() {
try {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-options.js&action=raw&ctype=text/javascript');
} catch (e) {
console.warn("Comrade: Could not load /comrade-options.js, using fallback defaults.", e);
}
window.ComradeOptions = Object.assign({
version: "0.5.0",
excludedTemplates: [],
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: qid,
props: 'claims|labels',
languages: 'en',
format: 'json',
origin: '*'
});
try {
const wikiData = await fetch(url).then(r => r.json());
return wikiData.entities[qid];
} catch (e) {
return null;
}
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
ComradeLib.checkLeadCapitalization(currentTitle);
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
const isRedirect = !!mw.config.get("wgIsRedirect");
const userExclusions = window.comradeQualityExclusions || [];
const allExclusions = [...window.ComradeOptions.excludedTemplates, ...userExclusions];
const exclusionRegexString = allExclusions.map(t => t.replace("Template:", "").replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const exclusionRegex = new RegExp(exclusionRegexString, 'i');
if (isRedirect || exclusionRegex.test(wikitext)) {
console.log("Comrade check skipped: Page is a redirect or excluded via options.");
return;
}
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]].",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += " Also handled [[MOS:BLANKLINE]].";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
q3dm2pt8ukvxilg7hfok006868oo417
User:MrJaroslavik/userinfo.js
2
108875
739975
739973
2026-05-01T12:27:54Z
MrJaroslavik
44012
+
739975
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: ',
laact: 'Last action: ',
laadmin: 'Last admin action: ',
lacu: 'Last CU action: ',
laos: 'Last OS action: ',
today: 'today',
yesterday: 'yesterday'
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: ',
today: 'dnes',
yesterday: 'včera'
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (type, lecontinue, currentCount) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (lecontinue) params += '&lecontinue=' + lecontinue;
if (type === 'gn') params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
// Používáme přímý ajax místo zařazení do fronty, aby nedocházelo k zamrznutí
us.ajaxRequest(params, function(uq) {
us.writeThanks(uq, type, currentCount || 0);
});
},
writeThanks: function (uq, type, currentCount) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
currentCount += uq.query.logevents.length;
// Stránkování výsledků, pokud má uživatel více poděkování než limit
if (uq.continue && uq.continue.lecontinue) {
return us.getThanks(type, uq.continue.lecontinue, currentCount);
}
var e = type === 'gn' ? $('#us_thx_gn_val') : $('#us_thx_rd_val');
var userParam = type === 'gn' ? '&user=' + us.user : '&page=User:' + us.user;
// Po sečtení přepíšeme načítací tečky výsledkem
e.text(currentCount).css({'font-style': 'normal', 'cursor': 'pointer'});
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks' + userParam + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// POMOCNÁ FUNKCE PRO VÝPIS AKCÍ Z LOGU (S ODKAZEM A DNES/VČERA)
var renderActionRow = function(valSelector, liSelector, timestamp, url) {
var d = new Date(timestamp);
// Logika pro dnes/včera
var today = new Date(us.now);
var yesterday = new Date(us.now);
yesterday.setDate(yesterday.getDate() - 1);
var diffText = us.getDateDiff(us.now, d);
if (d.toDateString() === today.toDateString()) diffText = msg.today || 'today';
else if (d.toDateString() === yesterday.toDateString()) diffText = msg.yesterday || 'yesterday';
var formatted = us.formatDate(d);
var diffNode = url ? $('<a>', { href: url, text: diffText }) : diffText;
$(valSelector).empty().append(diffNode, ' ', $('<span>', { style: us.styleMissingData, text: '(' + formatted + ')' }));
$(liSelector).show();
};
// A. OBECNÉ A ADMIN AKCE
var adminTypes = ['block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'renameuser', 'gblblock'];
var ignoreActions = ['autopromote', 'autopatrol', 'auto', 'hit']; // Zde je chybějící definice!
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=50',
func: function(res) {
if(!res.query || !res.query.logevents) return;
var logs = res.query.logevents;
// 1. Poslední manuální akce (IGNORUJEME systémové věci z pole ignoreActions)
var realAction = logs.find(function(l) {
return ignoreActions.indexOf(l.action) === -1;
});
if(realAction) {
// Odkaz na logy s filtry vyjmutí (thanks, patrol, tag, newusers) a dynamickým jménem
var actionUrl = mw.util.getUrl('Special:Log') + '?type=&user=' + encodeURIComponent(us.user) + '&page=&excludetempacct=1&wpdate=&tagfilter=&wpfilters%5B%5D=thanks&wpfilters%5B%5D=patrol&wpfilters%5B%5D=tag&wpfilters%5B%5D=newusers&wpFormIdentifier=logeventslist';
renderActionRow('#us_last_action_val', '#us_last_action_li', realAction.timestamp, actionUrl);
}
// 2. Najdi první admin akci (POUZE pokud je uživatel aktuálně admin nebo byrokrat)
var isAdminOrCrat = groups && ($.inArray('sysop', groups) !== -1 || $.inArray('bureaucrat', groups) !== -1);
if (isAdminOrCrat) {
var adm = logs.find(function(l) {
return adminTypes.indexOf(l.type) !== -1 && ignoreActions.indexOf(l.action) === -1;
});
if(adm) {
renderActionRow('#us_last_admin_val', '#us_last_admin_li', adm.timestamp, mw.util.getUrl('Special:Log', { user: us.user }));
}
}
}
});
// B. CU AKCE
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=checkuserlog&culuser=' + us.user + '&cullimit=1',
func: function(res) {
if(res.query && res.query.checkuserlog && res.query.checkuserlog.entries && res.query.checkuserlog.entries[0]) {
var cuUrl = mw.util.getUrl('Special:CheckUserLog', { cuSearch: '', cuInitiator: us.user });
renderActionRow('#us_last_cu_val', '#us_last_cu_li', res.query.checkuserlog.entries[0].timestamp, cuUrl);
}
}
});
}
// C. OS AKCE (Ošetřen oversight i suppress)
if (groups && ($.inArray('oversight', groups) !== -1 || $.inArray('suppress', groups) !== -1)) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var osUrl = mw.util.getUrl('Special:Log', { type: 'suppress', user: us.user });
renderActionRow('#us_last_os_val', '#us_last_os_li', res.query.logevents[0].timestamp, osUrl);
}
}
});
}
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
// Automatické načtení poděkování do fronty dotazů
us.getThanks('rd');
us.getThanks('gn');
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thxRd = $('<a>', { id: 'us_thx_rd_val', title: msg.thxRd, style: us.styleLoading, text: '...' });
var thxGn = $('<a>', { id: 'us_thx_gn_val', title: msg.thxGn, style: us.styleLoading, text: '...' });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:UserPages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li', style: 'display:none' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), msg.thxRd, ': ', thxRd, ' / ', msg.thxGn, ': ', thxGn, $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
if (days < 0) {
months--;
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
if (months < 0) {
years--;
months += 12;
}
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
// Logika pro dnes/včera
var d = new Date(date);
var today = new Date(us.now);
var yesterday = new Date(us.now);
yesterday.setDate(yesterday.getDate() - 1);
var diffText = us.getDateDiff(us.now, d);
if (d.toDateString() === today.toDateString()) diffText = msg.today || 'today';
else if (d.toDateString() === yesterday.toDateString()) diffText = msg.yesterday || 'yesterday';
var diffNode = $('<a>', { href: mw.util.getUrl('Special:Contributions/' + us.user), text: diffText });
us.us_last_edit_loading.replaceWith(diffNode, ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
be2og92mkwzbasdopb1g2y6cfsjlvc0
739983
739975
2026-05-01T13:06:09Z
MrJaroslavik
44012
+
739983
javascript
text/javascript
/**
* @Description: Modified Userstatus.js with Former Groups (Local & Global)
* Original by Steef389, Perhelion.
**/
(function ($, mw) {
'use strict';
var project = window.project || mw.config.get('wgDBname');
var msg, i18n = {
en: {
noReason: 'reason removed',
blockCmt: 'no block comment',
never: 'never',
not: ' not ',
block: 'blocked',
and: '$1 and$2',
count: 'Count',
noedit: 'None (only deleted contributions?)',
nodb: '‹not in database›',
noGrp: 'no extended group',
dates: ['years', 'months', 'days', 'hours', 'minutes', 'seconds'],
date: ['year', 'month', 'day', 'hour', 'minute', 'second'],
nosec: 'less than one ',
curtimeDiff: ' Locale time difference to server time in s: ',
thxGn: 'given',
thxRd: 'received',
nova: 'new',
ago: '$1 ago',
blocklog: 'Block log',
contrib: 'Edits: ',
usub: 'Subpages⬇',
thxGvng: 'Thanksgivings: ',
reviews: 'Active reviews: ',
regdate: 'Registration: ',
laedit: 'Last edited: ',
lala: 'Last log activity',
fiedit: 'First edit: ',
blocks: 'Block-status: ',
loGrp: 'Local user-groups: ',
glGrp: 'Global user-groups: ',
formerLoGrp: 'Former local user-groups: ',
formerGlGrp: 'Former global user-groups: ',
blockEnd: 'Block-end: ',
blocker: 'Blocker: ',
blockReason: 'Block-reason: ',
laact: 'Last action: ',
laadmin: 'Last admin action: ',
lacu: 'Last CU action: ',
laos: 'Last OS action: ',
today: 'today',
yesterday: 'yesterday'
},
cs: {
noReason: 'důvod odstraněn',
blockCmt: 'bez komentáře',
never: 'nikdy',
not: ' ne ',
block: 'zablokován',
and: '$1 a$2',
count: 'Počet',
noedit: 'Žádné (jen smazané?)',
nodb: '‹není v databázi›',
noGrp: 'žádná rozšířená skupina',
dates: ['let', 'měsíců', 'dní', 'hodin', 'minut', 'sekund'],
date: ['rok', 'měsíc', 'den', 'hodina', 'minuta', 'sekunda'],
nosec: 'méně než ',
curtimeDiff: ' Rozdíl lokálního času k serveru v s: ',
thxGn: 'poděkoval',
thxRd: 'obdržel poděkování',
nova: 'nové',
ago: 'před $1',
days: 'dní',
blocklog: 'Kniha zablokování',
contrib: 'Editace: ',
usub: 'Podstránky⬇',
thxGvng: 'Poděkování: ',
reviews: 'Aktivní prověření: ',
regdate: 'Registrace: ',
laedit: 'Poslední editace: ',
lala: 'Poslední aktivita v protokolech',
fiedit: 'První editace: ',
blocks: 'Stav bloku: ',
loGrp: 'Lokální skupiny: ',
glGrp: 'Globální skupiny: ',
formerLoGrp: 'Bývalé lokální skupiny: ',
formerGlGrp: 'Bývalé globální skupiny: ',
blockEnd: 'Konec bloku: ',
blocker: 'Zablokoval: ',
blockReason: 'Důvod bloku: ',
laact: 'Poslední akce: ',
laadmin: 'Poslední admin akce: ',
lacu: 'Poslední CU akce: ',
laos: 'Poslední OS akce: ',
today: 'dnes',
yesterday: 'včera'
}
};
var data;
var us = mw.libs.userstatus = {
name: 'Userstatus',
version: 1.80, // Bumping version for former groups update
lastEditSeconds: false,
styleMissingData: 'color:#999;font-size:90%;',
styleLoading: 'font-style:italic;',
styleBlocked: 'color:#c00',
styleNotBlocked: 'color:#080',
styleFormer: 'margin-left:15px; color:#72777d; font-style:italic; font-size:0.9em;',
viewPatrolNumber: false,
lang: mw.config.get('wgUserLanguage'),
user: mw.config.get('wgTitle'),
cookie: [],
thanks: 0,
patrols: 0,
actions: [],
dbVersion: 0,
implicitGroups: ['*', 'user', 'autoconfirmed'],
getLocalNames: function (groups, specialpage) {
if (groups) {
var arr = [];
specialpage = specialpage || 'ListGroupRights';
for (var i = 0; i < groups.length; ++i) {
var g = groups[i];
var n = us.groupNames[g];
if (n) arr.push('<a href="/wiki/Special:' + specialpage + '#' + g + '">' + n + '</a>');
else arr.push(g);
}
groups = arr.join(', ').replace(/(.*),([^,]*)$/, msg.and);
}
return groups;
},
writeGroups: function (groups) {
groups = us.getLocalNames(groups);
if (!groups) {
groups = $('<span>', {
style: us.styleMissingData + us.styleLoading,
text: msg.noGrp
});
}
us.loading_groups.replaceWith(groups);
// After local groups are written, fetch former local groups
us.fetchFormerRights(false);
},
getGroupNames: function (aw) {
if (!aw || !aw.query) return;
aw = aw.query.allmessages;
var al = aw.length;
var groups = {};
while (al--) {
var an = aw[al];
var name = an.name;
if (/^group-/.test(name)) {
name = RegExp.rightContext || name.substring(6);
groups[name] = an['*'];
}
}
us.groupNames = groups;
us.init();
},
ajaxRequest: function (params, onSuccess, trial) {
var url = us.api;
if (!(params instanceof Object)) {
url += params + '&maxage=2419200&smaxage=2419200';
params = {};
} else {
params.maxlag = 3;
params.maxage = 2419200;
params.smaxage = 2419200;
if (trial) params.maxlag *= trial;
}
var api = $.getJSON(url, params, onSuccess)
.fail(function (jqXHR, status) {
var note = 'Timeout fail, maybe try again!';
if (trial) {
if (!status.textStatus || (status.textStatus !== 'timeout' && jqXHR !== 'maxlag')) note = $('<span>Fehler bitte <a href="' + mw.util.getUrl('User_talk:Perhelion') + '">Perhelion</a> melden.</span>');
mw.notify(note, { title: us.name + ':', type: 'error' });
} else if (status.textStatus === 'timeout' || jqXHR === 'maxlag') {
api.abort();
return us.ajaxRequest(params, onSuccess, 2);
}
});
},
// --- NEW FORMER RIGHTS LOGIC ---
fetchFormerRights: function(isGlobal) {
var apiTarget = isGlobal ? 'https://meta.wikimedia.org/w/api.php' : mw.util.wikiScript('api');
var logType = isGlobal ? 'gblrights' : 'rights';
var userTarget = 'User:' + us.user;
var containerId = isGlobal ? 'us_former_global_groups' : 'us_former_local_groups';
// Globální override pro smazané skupiny (skupina: datum smazání)
var globalOverrides = {
'oathauth-tester': '2026-01-12T10:58:00Z'
};
var params = {
action: 'query',
list: 'logevents',
letype: logType,
letitle: userTarget,
lelimit: 500,
format: 'json',
origin: isGlobal ? '*' : undefined
};
$.getJSON(apiTarget, params, function(res) {
if(!res || !res.query || !res.query.logevents) return;
var logs = res.query.logevents;
var former = {};
var currentGroups = isGlobal ? (data.glGrp || []) : (data.groups || []);
logs.forEach(function(log) {
var p = log.params;
if (!p) return;
var date = log.timestamp;
var oldRaw = p.oldgroups || p.oldGroups || p.remove || p["0"] || "";
var newRaw = p.newgroups || p.newGroups || p.add || p["1"] || "";
var parse = function(val) {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
if (val === "(none)" || val === "") return [];
return val.split(/[,|]\s*/);
}
return [];
};
var oldG = parse(oldRaw);
var newG = parse(newRaw);
// 1. Nejdřív kontrolujeme udělení (v logu jdeme od nejnovějších k nejstarším)
newG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Speciální případ: Skupina byla udělena, uživatel ji nemá a víme, že globálně zanikla
if (currentGroups.indexOf(g) === -1 && globalOverrides[g] && !former[g]) {
former[g] = { to: globalOverrides[g], from: date };
}
// Standardní případ: Už známe konec z oldG a teď jsme našli, kdy to začalo
else if (former[g] && !former[g].from) {
former[g].from = date;
}
});
// 2. Poté kontrolujeme odebrání (standardní cesta)
oldG.forEach(function(g) {
g = g.trim();
if (us.implicitGroups.indexOf(g) !== -1 || !g) return;
// Pokud skupinu nemá a ještě jsme o ní v tomto průchodu neslyšeli
if (currentGroups.indexOf(g) === -1 && !former[g]) {
former[g] = { to: date, from: null };
}
});
});
var entries = [];
Object.keys(former).sort().forEach(function(g) {
var item = former[g];
// Aplikace globálního overridu, pokud chybí datum odebrání a skupina je v seznamu smazaných
if (!item.to && globalOverrides[g]) {
item.to = globalOverrides[g];
}
var dateTo = new Date(item.to);
var dateFrom = item.from ? new Date(item.from) : null;
if(dateFrom && item.to) {
var diffText = us.getDateDiff(dateTo, dateFrom).replace(msg.ago.replace('$1', ''), '').trim();
entries.push(g + ' (' + dateFrom.toISOString().split('T')[0] + ' – ' + dateTo.toISOString().split('T')[0] + ', ' + diffText + ')');
} else if (item.to) {
entries.push(g + ' (do ' + dateTo.toISOString().split('T')[0] + ')');
}
});
if(entries.length > 0) {
$('#' + containerId).show().find('.us_former_content').text(entries.join(', '));
}
});
},
getThanks: function (type, lecontinue, currentCount) {
var params = '&list=logevents&continue=-%7C%7C&leprop=&letype=thanks&ledir=newer&lelimit=max';
if (lecontinue) params += '&lecontinue=' + lecontinue;
if (type === 'gn') params += '&leuser=' + us.user;
else params += '&letitle=User:' + us.user;
// Používáme přímý ajax místo zařazení do fronty, aby nedocházelo k zamrznutí
us.ajaxRequest(params, function(uq) {
us.writeThanks(uq, type, currentCount || 0);
});
},
writeThanks: function (uq, type, currentCount) {
if (!uq.query || !uq.query.logevents) return mw.log.warn(uq);
currentCount += uq.query.logevents.length;
// Stránkování výsledků, pokud má uživatel více poděkování než limit
if (uq.continue && uq.continue.lecontinue) {
return us.getThanks(type, uq.continue.lecontinue, currentCount);
}
var e = type === 'gn' ? $('#us_thx_gn_val') : $('#us_thx_rd_val');
var userParam = type === 'gn' ? '&user=' + us.user : '&page=User:' + us.user;
// Po sečtení přepíšeme načítací tečky výsledkem
e.text(currentCount).css({'font-style': 'normal', 'cursor': 'pointer'});
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special%3ALog&type=thanks' + userParam + '&year=&month=-1&tagfilter=&hide_thanks_log=0';
},
getUploads: function (e) {
var params = '&list=logevents&letype=upload&ledir=newer&lelimit=max&leuser=' + us.user;
function _start(u) {
var text = u.text();
u.prop('title', text);
u.text('');
u.injectSpinner('upload' + text);
u.data('upl', 0);
u.data('del', 0);
}
if (e instanceof Object) {
if (e.target) {
if (us.e) return mw.notify('Synchron requests are not yet supported.', { title: us.name + ':', type: 'error' });
e = e.target;
us.e = $(e);
if (us.e.text() !== msg.nova) {
us.e2 = us.e.nextAll('a').eq(0);
if (us.e2.text() === msg.nova) _start(us.e2); else delete us.e2;
}
_start(us.e);
} else if (e.lecontinue) { params += '&lecontinue=' + e.lecontinue; }
params += '&leprop=ids';
if (us.e.prop('title') === msg.nova) params += '&leaction=upload/upload';
else if (us.e2) params += '|type';
}
us.actions.push({ params: params, func: us.writeUploads });
us.doRequest();
},
writeUploads: function (uq) {
if (!uq.query || !uq.query.logevents || !us.e) return mw.log(uq, us.e);
function _insert(e) {
$.removeSpinner('upload' + e.prop('title'));
e.text('');
e.off('click');
e.append(e.data('del'), $('<span>', { style: us.styleMissingData + us.styleLoading, text: ' (+' + (e.data('upl') - e.data('del')) + ' del.)' }));
e[0].target = '_blank';
e[0].href = '/w/index.php?title=Special:Log&type=upload&user=' + us.user + '&subtype=' + ((e[0].title === msg.nova) ? 'upload' : '');
}
var e = us.e;
var e2 = us.e2;
var aw = uq.query.logevents;
var alen = aw.length, i = 0;
var del = e.data('del');
if (e2) {
var upl = e2.data('upl');
var del2 = e2.data('del');
for (i; i < alen; ++i) {
var a = aw[i];
var c = a.pageid ? 1 : 0;
if (a.action === 'upload') { upl++; del2 += c; }
del += c;
}
e2.data('upl', upl);
e2.data('del', del2);
} else {
for (i; i < alen; ++i) if (aw[i].pageid) del++;
}
e.data('upl', e.data('upl') + alen);
e.data('del', del);
if (uq.continue) return us.getUploads(uq.continue);
_insert(e);
if (e2) { _insert(e2); delete us.e2; }
delete us.e;
},
writeCommonInfo: function (uq) {
var aw = uq.query;
if (!aw || !aw.users[0] || Object.prototype.hasOwnProperty.call(aw, 'missing') || Object.prototype.hasOwnProperty.call(aw.users[0], 'invalid')) return us.statusBox.remove();
aw = aw.users[0];
var edits = aw.editcount,
groups = data.groups || aw.groups,
blocked = data.blockreason || aw.blockexpiry,
gender = data.gender || aw.gender,
uploads = $('<a>', { title: msg.count, text: msg.nova, style: us.styleLoading, click: us.getUploads });
if (aw.registration) data.registration = aw.registration;
us.now = us.getDateFromTimestamp(uq.curtimestamp || us.now || mw.now());
us.user = us.user.replace(/ /g, '_');
us.first = 1;
// POMOCNÁ FUNKCE PRO VÝPIS AKCÍ Z LOGU (S ODKAZEM A DNES/VČERA)
var renderActionRow = function(valSelector, liSelector, timestamp, url) {
var d = new Date(timestamp);
// Logika pro dnes/včera
var today = new Date(us.now);
var yesterday = new Date(us.now);
yesterday.setDate(yesterday.getDate() - 1);
var diffText = us.getDateDiff(us.now, d);
if (d.toDateString() === today.toDateString()) diffText = msg.today || 'today';
else if (d.toDateString() === yesterday.toDateString()) diffText = msg.yesterday || 'yesterday';
var formatted = us.formatDate(d);
var diffNode = url ? $('<a>', { href: url, text: diffText }) : diffText;
$(valSelector).empty().append(diffNode, ' ', $('<span>', { style: us.styleMissingData, text: '(' + formatted + ')' }));
$(liSelector).show();
};
// A. OBECNÉ A ADMIN AKCE
var adminTypes = ['block', 'delete', 'protect', 'rights', 'merge', 'abusefilter', 'renameuser', 'gblblock'];
var ignoreActions = ['autopromote', 'autopatrol', 'auto', 'hit']; // Zde je chybějící definice!
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=50',
func: function(res) {
if(!res.query || !res.query.logevents) return;
var logs = res.query.logevents;
// 1. Poslední manuální akce (IGNORUJEME systémové věci z pole ignoreActions)
var realAction = logs.find(function(l) {
return ignoreActions.indexOf(l.action) === -1;
});
if(realAction) {
// Odkaz na logy s filtry vyjmutí (thanks, patrol, tag, newusers) a dynamickým jménem
var actionUrl = mw.util.getUrl('Special:Log') + '?type=&user=' + encodeURIComponent(us.user) + '&page=&excludetempacct=1&wpdate=&tagfilter=&wpfilters%5B%5D=thanks&wpfilters%5B%5D=patrol&wpfilters%5B%5D=tag&wpfilters%5B%5D=newusers&wpFormIdentifier=logeventslist';
renderActionRow('#us_last_action_val', '#us_last_action_li', realAction.timestamp, actionUrl);
}
// 2. Najdi první admin akci (POUZE pokud je uživatel aktuálně admin nebo byrokrat)
var isAdminOrCrat = groups && ($.inArray('sysop', groups) !== -1 || $.inArray('bureaucrat', groups) !== -1);
if (isAdminOrCrat) {
var adm = logs.find(function(l) {
return adminTypes.indexOf(l.type) !== -1 && ignoreActions.indexOf(l.action) === -1;
});
if(adm) {
renderActionRow('#us_last_admin_val', '#us_last_admin_li', adm.timestamp, mw.util.getUrl('Special:Log', { user: us.user }));
}
}
}
});
// B. CU AKCE
if (groups && $.inArray('checkuser', groups) !== -1) {
us.actions.push({
params: '&list=checkuserlog&culuser=' + us.user + '&cullimit=1',
func: function(res) {
if(res.query && res.query.checkuserlog && res.query.checkuserlog.entries && res.query.checkuserlog.entries[0]) {
var cuUrl = mw.util.getUrl('Special:CheckUserLog', { cuSearch: '', cuInitiator: us.user });
renderActionRow('#us_last_cu_val', '#us_last_cu_li', res.query.checkuserlog.entries[0].timestamp, cuUrl);
}
}
});
}
// C. OS AKCE (Ošetřen oversight i suppress)
if (groups && ($.inArray('oversight', groups) !== -1 || $.inArray('suppress', groups) !== -1)) {
us.actions.push({
params: '&list=logevents&leuser=' + us.user + '&lelimit=1&letype=suppress',
func: function(res) {
if(res.query && res.query.logevents[0]) {
var osUrl = mw.util.getUrl('Special:Log', { type: 'suppress', user: us.user });
renderActionRow('#us_last_os_val', '#us_last_os_li', res.query.logevents[0].timestamp, osUrl);
}
}
});
}
if (edits) {
if (data.editcount && data.lastedit && data.editcount === edits) {
uq.query.usercontribs = [{ timestamp: data.lastedit }];
us.writeLastEdit(uq);
} else {
data.editcount = edits;
us.loading_last_edit.css('display', 'block');
us.actions.push({
params: '&list=usercontribs&ucuser=' + us.user + '&uclimit=1&uccontinue=' + (new Date(us.now).toISOString().replace(/[^\d]/g, '').slice(0, 14) + '|2'),
func: us.writeLastEdit
});
}
$('#t-contributions').remove();
} else { us.writeLastEdit({}); }
if (groups) {
edits = $('<a>', { title: 'Supercount', href: '//tools.wmflabs.org/supercount/index.php?user=' + us.user + '&project=' + location.hostname, text: edits });
if ($.inArray('bot', groups) === -1) {
us.review = $.inArray('editor', groups) !== -1;
if (us.review) {
if (typeof data.reviews === 'number') us.writePatrolCount(data.reviews);
} else if (us.viewPatrolNumber) { us.us_patrolcount_loading.parent().remove(); }
edits = [
edits, ' • ',
$('<a>', { href: '//xtools.wmflabs.org/ec' + mw.config.get('wgServer').substr(1) + '/' + us.user, title: 'XTools', text: 'XTools', style: us.styleLoading, target: '_blank' }), ' • ',
$('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:CentralAuth/' + us.user, title: 'CentralAuth', text: 'CentralAuth', style: us.styleLoading, target: '_blank' }), ' • ',
(project !== 'commonswiki' ? $('<a>', { href: '/wiki/Special:ListFiles/' + us.user, text: 'Upload', title: 'Special:Listfiles', style: us.styleLoading }) : 'Upload'),
(us.lang === 'de' ? '-' : ' ') + msg.count + ': ',
uploads.clone(1).text('total'), ' / ', uploads
];
if (!$('#t-subpages').length) {
edits.push($('<a>', { href: mw.util.getUrl('Special:Prefixindex/User:' + us.user + '/'), text: ' • ' + msg.usub, style: us.styleMissingData + us.styleLoading }));
}
} else if (us.us_patrolcount_loading) { us.us_patrolcount_loading.parent().remove(); }
us.loading_editcount.replaceWith(edits);
if (!aw.implicitgroups && !data.groups) {
aw.implicitgroups = ['*', 'user', 'autoconfirmed'];
if (groups.length < 4) aw.implicitgroups.pop();
}
data.groups = $.grep(groups, function (n) { return $.inArray(n, aw.implicitgroups) === -1; });
us.writeGroups(data.groups);
$('#t-userrights').remove();
}
if (!data.registration) {
us.actions.push({ params: '&list=logevents&leprop=timestamp|type&letype=newusers&lelimit=1&ledir=newer&leend=2005-12-30T00:00:00Z&leuser=' + us.user, func: us.writeRegistration });
if (data.firstedit) {
uq.query.usercontribs = [{ timestamp: data.firstedit }];
us.writeFirstEdit(uq);
} else if (edits) {
us.actions.push({ params: { list: 'usercontribs', ucuser: us.user, uclimit: 1, ucend: '2005-09-08T00:00:00Z', ucdir: 'newer', ucprop: 'timestamp' }, func: us.writeFirstEdit });
}
} else { us.writeRegistration(data.registration); }
us.loading_blocked.replaceWith($('<a>', { style: ((blocked) ? us.styleBlocked : us.styleNotBlocked), id: 'us_block_status_span', href: '/w/index.php?title=Special:Log/block&page=User:' + us.user, title: msg.blocklog, text: ((blocked) ? ' ' : msg.not) + msg.block }));
if (blocked) {
data.blockreason = aw.blockreason || ' ';
us.ul.append([
$('<li>', { id: 'us_block_time' }).append([$('<b>').text('• ' + msg.blockEnd), $.createSpinner('us_block_time_loading')]),
$('<li>', { id: 'us_block_reason' }).append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment, (aw.commenthidden))]),
$('<li>', { id: 'us_blocker' }).append([$('<b>').text('• ' + msg.blocker), $.createSpinner('us_blocker_loading')])
]);
us.actions.push({ params: { list: 'logevents', letitle: 'User:' + us.user, letype: 'block', lelimit: 1 }, func: us.writeBlockDetail });
}
if (data.glGrp && !data.locked) {
uq.query.globaluserinfo = { groups: (data.glGrp.length) ? data.glGrp : null };
us.writeGlobalGroup(uq);
} else {
us.actions.push({ params: '&meta=globaluserinfo&guiprop=groups&guiuser=' + us.user, func: us.writeGlobalGroup });
}
if (gender) {
data.gender = gender;
var genderSn = (gender === 'male') ? ' \u2642' : (gender === 'female' ? ' \u2640' : '');
$('#firstHeading').append($('<span>', { id: 'ps-gender-' + gender, style: 'font-size:80%', text: genderSn }));
}
// Automatické načtení poděkování do fronty dotazů
us.getThanks('rd');
us.getThanks('gn');
if (us.actions.length) us.doRequest();
},
createBox: function () {
var statusBox = $('<div>', { style: 'border-bottom:1px solid #aaa;text-shadow:1px 1px 1px #eff;', id: 'us_box' });
if (mw.config.get('skin') === 'vector') statusBox.css('font-size', '0.8em');
var spanFrag = $.createSpinner();
us.loading_editcount = spanFrag.clone();
us.loading_registration = spanFrag.clone();
us.loading_groups = spanFrag.clone();
us.loading_blocked = spanFrag.clone();
us.us_global_group_loading = spanFrag.clone();
us.us_last_edit_loading = $.createSpinner('contribs');
us.loading_last_edit = $('<li>');
us.us_global_group = $('<li>', { style: 'display: none' });
var thxRd = $('<a>', { id: 'us_thx_rd_val', title: msg.thxRd, style: us.styleLoading, text: '...' });
var thxGn = $('<a>', { id: 'us_thx_gn_val', title: msg.thxGn, style: us.styleLoading, text: '...' });
var $userrights = $('#t-userrights a');
var $contributions = $('#t-contributions a');
if (!$userrights.length) $userrights = $('<a>', { href: '/wiki/Special:UserRights/' + us.user });
if (!$contributions.length) $contributions = $('<a>', { href: '/wiki/Special:Contributions/' + us.user });
var ul = $('<ul>', { style: 'list-style: none' }).append($('<li>', { id: 'us_editcount' }).append([$('<b>').append($contributions.text(msg.contrib)), us.loading_editcount]));
us.us_log_count = $('<span>');
if (us.viewPatrolNumber) {
us.us_patrolcount_loading = $('<a>', { style: us.styleLoading, click: us.getPatrolCount, text: msg.count });
us.us_log_count = $('<span>').append([$('<b>').text(msg.reviews), us.us_patrolcount_loading, ' • ']);
}
ul.append(us.us_log_count)
.append($('<li>', { id: 'us_reg_date' }).append([$('<b>').text(msg.regdate), us.loading_registration, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.toolforge.org/userpages/' + us.user, text: '• Global user pages' })]))
.append($('<li>').append([$('<b>').append($userrights.text(msg.loGrp)), us.loading_groups]))
.append($('<li>', { id: 'us_former_local_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerLoGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.us_global_group.append([$('<b>').append($('<a>', { href: 'https://meta.wikimedia.org/wiki/Special:Log?type=gblrights&user=&page=User:' + us.user, text: msg.glGrp })), us.us_global_group_loading]))
.append($('<li>', { id: 'us_former_global_groups', style: 'display:none;' + us.styleFormer }).append([$('<b>').text(msg.formerGlGrp), $('<span>', { 'class': 'us_former_content' })]))
.append(us.loading_last_edit.append([$('<b>').text(msg.laedit), us.us_last_edit_loading, $('<a>', { target: '_blank', style: 'float:right;' + us.styleMissingData, href: 'https://meta.wikimedia.org/wiki/Special:GlobalContributions/' + us.user, text: '• Global contributions' })]))
.append($('<li>', { id: 'us_last_action_li', style: 'display:none' }).append([$('<b>').text(msg.laact), $('<span>', { id: 'us_last_action_val' })]))
.append($('<li>', { id: 'us_last_admin_li', style: 'display:none' }).append([$('<b>').text(msg.laadmin), $('<span>', { id: 'us_last_admin_val' })]))
.append($('<li>', { id: 'us_last_cu_li', style: 'display:none' }).append([$('<b>').text(msg.lacu), $('<span>', { id: 'us_last_cu_val' })]))
.append($('<li>', { id: 'us_last_os_li', style: 'display:none' }).append([$('<b>').text(msg.laos), $('<span>', { id: 'us_last_os_val' })]));
statusBox.append(ul);
us.ul = $('<ul>', { style: 'list-style:none' }).append($('<li>', { id: 'us_block_status' }).append([$('<b>').text(msg.blocks), us.loading_blocked, ' • ', $('<b>').text(msg.thxGvng), msg.thxRd, ': ', thxRd, ' / ', msg.thxGn, ': ', thxGn, $('<a>', { style: 'float:right;' + us.styleMissingData, title: 'Purge data', text: 'purge', click: us.removeDataStore })]));
statusBox.append(us.ul);
$('#firstHeading').after(statusBox);
us.statusBox = statusBox;
},
removeDataStore: function (e) {
var key = project + us.user, db = window.indexedDB;
e.preventDefault();
if (!db || !us.dbVersion) return mw.cookie.set(us.name + us.user, null);
var request = db.open(us.name, us.dbVersion + 1);
request.onupgradeneeded = function () { db = this.result; if (db.objectStoreNames.contains(key)) db.deleteObjectStore(key); };
request.onsuccess = function () { db = this.result; us.dbVersion = db.version; db.close(); location.reload(); };
},
setCookie: function () {
var domain = (mw.config.get('wgNoticeProject') === 'wikipedia') ? 'wikipedia.org' : '';
var name = us.name + us.user;
var _saveCookie = function (k, d) { mw.cookie.set(name, JSON.stringify(d), { expires: 604800, domain: domain }); };
if (!us.actions[0] && JSON && data.editcount) {
if (us.cookie.length) window.clearTimeout(us.cookie.shift());
var saveData = window.indexedDB ? function (n, JSd, v) {
var key = project + us.user, db = window.indexedDB, request = db.open(us.name, v);
request.onupgradeneeded = function () {
db = this.result;
if (!db.objectStoreNames.contains(key)) {
var store = db.createObjectStore(key, { keyPath: 'name' });
store.createIndex('data', 'data', { unique: false });
}
};
request.onsuccess = function () {
db = this.result; us.dbVersion = db.version;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readwrite').objectStore(key);
store.put({ name: key, data: JSd });
} else { saveData(n, JSd, us.dbVersion + 1); }
};
request.onerror = function() { _saveCookie(n, JSd); };
} : _saveCookie;
us.cookie.push(setTimeout(function () {
data.timestamp = new Date().valueOf();
saveData('', data);
}, 500));
}
},
doRequest: function () {
var action = us.actions.shift();
if (action) us.ajaxRequest(action.params, action.func);
if (us.actions.length) window.setTimeout(function () { us.doRequest(); }, 100);
},
getDateDiff: function (now, date) {
var dStr = [];
if (now > date) {
var years = now.getFullYear() - date.getFullYear();
var months = now.getMonth() - date.getMonth();
var days = now.getDate() - date.getDate();
if (days < 0) {
months--;
var prevMonth = new Date(now.getFullYear(), now.getMonth(), 0).getDate();
days += prevMonth;
}
if (months < 0) {
years--;
months += 12;
}
var units = [years, months, days];
var labelSingle = [msg.date[0], msg.date[1], msg.date[2]];
var labelPlural = [msg.dates[0], msg.dates[1], msg.dates[2]];
for (var i = 0; i < 3; ++i) {
if (units[i] > 0) {
dStr.push(units[i] + ' ' + ((units[i] > 1) ? labelPlural[i] : labelSingle[i]));
}
}
}
var mainDiff = (dStr.length) ? dStr.join(', ').replace(/(.*),([^,]*)/, msg.and).replace(/(.*)/, msg.ago) : msg.nosec + msg.date[2];
return mainDiff;
},
getDateFromTimestamp: function (t) { if (!t) return false; t = new Date(t); return isNaN(t.valueOf()) ? false : t; },
writeRegistration: function (aw) {
if (aw) {
if (aw instanceof Object) {
if (aw.query && aw.query.logevents.length) aw = aw.query.logevents[0].timestamp;
}
if (typeof aw === 'string') {
us.loading_registration.replaceWith(us.formatDate(aw) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(aw)) + ')' }));
data.registration = aw;
return us.setCookie();
}
}
us.loading_registration.replaceWith($('<span>', { style: us.styleMissingData + us.styleLoading, text: msg.nodb }));
},
writeFirstEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs.length) return;
var date = data.firstedit = aw.query.usercontribs[0].timestamp;
$('#us_reg_date').append($('<li>').append([$('<b>', { text: '• ' + msg.fiedit }), us.formatDate(date) + ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.getDateDiff(us.now, us.getDateFromTimestamp(date)) + ')' })]));
},
writeLastEdit: function (aw) {
if (!aw.query || !aw.query.usercontribs || !aw.query.usercontribs.length) return us.us_last_edit_loading.replaceWith($('<span>', { style: us.styleMissingData, text: msg.noedit }));
var date = data.lastedit = aw.query.usercontribs[0].timestamp;
// Logika pro dnes/včera
var d = new Date(date);
var today = new Date(us.now);
var yesterday = new Date(us.now);
yesterday.setDate(yesterday.getDate() - 1);
var diffText = us.getDateDiff(us.now, d);
if (d.toDateString() === today.toDateString()) diffText = msg.today || 'today';
else if (d.toDateString() === yesterday.toDateString()) diffText = msg.yesterday || 'yesterday';
var diffNode = $('<a>', { href: mw.util.getUrl('Special:Contributions/' + us.user), text: diffText });
us.us_last_edit_loading.replaceWith(diffNode, ' ', $('<span>', { style: us.styleMissingData, text: '(' + us.formatDate(date) + ')' }));
data.timediff = mw.now() - us.now;
$('#us_editcount').append($('<span>', { style: 'float:right;' + us.styleMissingData, text: msg.curtimeDiff }).append($('<b>', { text: Math.round(data.timediff / 1000) })));
us.setCookie();
},
writeGlobalGroup: function (aw) {
aw = aw.query.globaluserinfo;
var groups = data.glGrp = aw.groups;
// Pokud lokální registrace chybí, použijeme tu z globálního infa
if (!data.registration && aw.registration) {
us.writeRegistration(aw.registration);
}
if (groups && groups.length) {
us.us_global_group_loading.replaceWith(us.getLocalNames(groups, 'GlobalGroupPermissions'));
us.us_global_group.css('display', 'block');
// Fetch former global rights
us.fetchFormerRights(true);
}
if (aw.locked !== undefined) {
data.locked = 1;
mw.loader.using('mediawiki.ForeignApi').done(function () {
var fApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php');
fApi.get({
action: 'query',
list: 'logevents',
leprop: 'user|timestamp|comment',
letype: 'globalauth',
letitle: 'User:' + us.user,
lelimit: '1'
}).done(us.writeGlobalBlock);
});
}
us.setCookie();
},
writeGlobalBlock: function (aw) {
if (!aw.query || !aw.query.logevents.length) return;
aw = aw.query.logevents[0];
$('#us_block_status_span').replaceWith($('<a>', { style: us.styleBlocked, id: 'us_block_status_span', href: '//meta.wikimedia.org/w/index.php?title=Special:Log/&type=globalauth&user=&page=User%3A' + us.user + '%40global', text: 'locked (global)' }).append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) })));
$('#us_block_status').parent().append([$('<li>').append([$('<b>').text('• ' + msg.blockReason), us.parseComment(aw.comment)]), $('<li>').append([$('<b>').text('• ' + msg.blocker), $('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user })])]);
},
writeBlockDetail: function (aw) {
var duration = 'infinite', expiry = '';
if (aw.query && aw.query.logevents.length) {
aw = aw.query.logevents[0];
if (aw && aw.params) {
duration = aw.params.duration;
expiry = /in(de)?finite/.test(duration) ? msg.never : us.formatDate(aw.params.expiry);
}
$('#us_block_status_span').text(msg.block + ' (' + duration + ')').append($('<span>', { style: us.styleMissingData, text: ' – ' + us.formatDate(aw.timestamp) }));
if (expiry) $('#mw-spinner-us_block_time_loading').replaceWith(expiry); else $('#us_block_time').remove();
$('#mw-spinner-us_block_reason_loading').replaceWith(us.parseComment(aw.comment, aw.commenthidden));
if (aw.user) $('#mw-spinner-us_blocker_loading').replaceWith($('<a>', { 'class': 'mw-userlink', 'title': 'User:' + aw.user, 'href': mw.util.getUrl('User:' + aw.user), 'text': aw.user }));
else $('#mw-spinner-us_blocker_loading').replaceWith(msg.nodb);
}
},
writePatrolCount: function (aw) {
if (aw instanceof Object) {
if (!aw.query || !aw.query.logevents) return;
us.patrols += aw.query.logevents.length;
if (aw.continue) return us.getPatrolCount(aw.continue);
$.removeSpinner('pat');
aw = us.patrols;
}
us.us_patrolcount_loading.replaceWith($('<a>', { href: '/w/index.php?title=Special:Log&type=' + (us.review ? 'review&subtype=accept' : 'patrol&subtype=patrol') + '&user=' + us.user, text: aw }));
data.reviews = aw;
us.setCookie();
},
getPatrolCount: function (e) {
var params = '&list=logevents&leprop=&ledir=newer&lelimit=max&leuser=' + us.user + '&leaction=' + (us.review ? 'review/approve' : 'patrol/patrol');
if (e instanceof Object) {
if (e.target) { $(e.target).injectSpinner('pat'); us.patrols = 0; }
else if (e.lecontinue) params += '&lecontinue=' + e.lecontinue;
}
us.actions.push({ params: params, func: us.writePatrolCount });
us.doRequest();
},
parseComment: function (text, hidden) {
var comment = $('<span>', { 'class': 'comment' });
if (typeof text === 'undefined') return comment.append(hidden ? msg.noReason : 'undefined').css('color', hidden ? '#999' : 'red');
if (!text) return comment.append(msg.blockCmt).css('color', '#999');
var intLink = /(.*?)\[\[((.*?)\|)?(.*?)\]\](.*)/, suche = text, erg;
while ((erg = intLink.exec(suche)) !== null) {
var link = erg[3] || erg[4];
comment.append([erg[1], $('<a>', { href: mw.util.getUrl(link), title: link }).text(erg[4])]);
suche = erg[5];
}
return comment.append(suche);
},
formatDate: function (datum) {
if (!(datum instanceof Date)) datum = new Date(datum);
try {
return datum.toLocaleDateString(us.lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
} catch (e) { return datum.toLocaleString(); }
},
getStoredData: function () {
var key = project + us.user, db, request;
try {
request = indexedDB.open(us.name);
request.onsuccess = function () {
db = this.result;
if (db.objectStoreNames.contains(key)) {
var store = db.transaction(key, 'readonly').objectStore(key);
store.transaction.oncomplete = function () { us.runDataStore(); db.close(); };
store.get(key).onsuccess = function () { if (this.result) data = this.result.data; else us.getCookie(); };
} else { us.getCookie(); }
};
request.onerror = function () { us.getCookie(); };
} catch (e) { us.getCookie(); }
},
getCookie: function () {
data = mw.cookie.get(us.name + us.user);
if (data) try { data = JSON.parse(data); } catch(e) { data = null; }
us.runDataStore();
},
initI18N: function (i18n) {
var chain = mw.language.getFallbackLanguageChain();
for (var i = chain.length - 1; i >= 0; i--) if (chain[i] in i18n) msg = i18n[chain[i]];
},
init: function () {
if (this.statusBox || (this.statusBox = $('#us_box'))[0]) this.statusBox.remove();
this.usprop = 'blockinfo|editcount|gender|groups|implicitgroups|registration';
this.self = mw.config.get('wgUserName') === this.user;
this.initI18N(i18n);
this.createBox();
if (!this.first) {
if (mw.cookie && JSON) { if (window.indexedDB) this.getStoredData(); else this.getCookie(); }
else this.run();
} else this.run();
},
runDataStore: function () {
if (data) {
if (typeof data === 'string') try { data = JSON.parse(data); } catch(e) { data = {}; }
var q = { query: { users: [data] } };
if (data.timestamp) this.now = mw.now() - (data.timediff || 0);
if (data.timestamp && Math.abs((mw.now() - data.timestamp - (data.timediff || 0)) / 1000) < 90) return this.writeCommonInfo(q);
if (data.editcount) this.usprop = 'editcount';
}
data = data || {};
this.run();
},
run: function () {
if (this.self) {
return this.writeCommonInfo({ query: { users: [{ editcount: mw.config.get('wgUserEditCount'), registration: mw.config.get('wgUserRegistration'), groups: mw.config.get('wgUserGroups'), blockreason: '' }] } });
}
us.ajaxRequest({ curtimestamp: 1, list: 'users', ususers: us.user, usprop: us.usprop }, us.writeCommonInfo);
}
};
if ([2, 3].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 && us.user.indexOf('/') === -1 && (!mw.config.get('wgArticleId') || mw.config.get('wgAction') === 'view')) {
$.when(mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.language', 'mediawiki.cookie', 'jquery.spinner', 'mediawiki.ForeignApi']), $.ready).then(function () {
us.api = mw.util.wikiScript('api') + '?action=query&format=json';
us.ajaxRequest('&meta=allmessages&amenableparser=1&amincludelocal=1&refix=group-', us.getGroupNames);
if (!mw.libs.viewerInfo) mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Perhelion/viewerInfo.js&action=raw&ctype=text/javascript');
});
}
}(jQuery, mediaWiki));
k7ffucgb87k3aym9jo6tlc56bzt5eha
Module:Uses TemplateStyles
828
110021
740090
609441
2026-02-16T01:11:16Z
en>JPxG
0
copy slopped edit from sandbox to provide a link to Special:EditPage for the linked pages; looks great to me on all the testcases, i dont think this will bust anything but yolo [[User:Pppery]] feel free to shoot me if i break anything
740090
Scribunto
text/plain
local yesno = require('Module:Yesno')
local mList = require('Module:List')
local mTableTools = require('Module:TableTools')
local mMessageBox = require('Module:Message box')
local TNT = require('Module:TNT')
local p = {}
local function format(msg, ...)
return TNT.format('I18n/Uses TemplateStyles', msg, ...)
end
local function getConfig()
return mw.loadData('Module:Uses TemplateStyles/config')
end
local function makeEditSup(pageName)
return string.format('<sup>[[[Special:Edit/%s|e]]]</sup>', pageName)
end
-- Build just the "(sandbox)" suffix using the i18n message, without duplicating the main link.
local function makeSandboxSuffix(tsSandboxPrefixed)
-- i18n message is: "$1 ([[$2|sandbox]])"
-- Passing empty $1 yields " ([[:...|sandbox]])" -> trim leading whitespace to get "(sandbox)" portion.
local s = format('sandboxlink', '', ':' .. tsSandboxPrefixed)
return (s:gsub('^%s+', ''))
end
local function renderBox(tStyles)
local boxArgs = {
type = 'notice',
small = true,
image = string.format('[[File:Farm-Fresh css add.svg|32px|alt=%s]]', format('logo-alt'))
}
if #tStyles < 1 then
boxArgs.text = string.format('<strong class="error">%s</strong>', format('error-emptylist'))
else
local cfg = getConfig()
local tStylesLinks = {}
for i, ts in ipairs(tStyles) do
local viewLink = string.format('[[:%s]]', ts)
local editSup = makeEditSup(ts)
local out = viewLink .. editSup
-- Optional sandbox link + sandbox edit link
local tsTitle = mw.title.new(ts)
if tsTitle and cfg['sandbox_title'] then
local tsSandboxTitle = mw.title.new(string.format(
'%s:%s/%s/%s',
tsTitle.nsText,
tsTitle.baseText,
cfg['sandbox_title'],
tsTitle.subpageText
))
if tsSandboxTitle and tsSandboxTitle.exists then
local sandboxSuffix = makeSandboxSuffix(tsSandboxTitle.prefixedText)
local sandboxEditSup = makeEditSup(tsSandboxTitle.prefixedText)
out = out .. ' ' .. sandboxSuffix .. ' ' .. sandboxEditSup
end
end
tStylesLinks[i] = out
end
local tStylesList = mList.makeList('bulleted', tStylesLinks)
boxArgs.text = format(
mw.title.getCurrentTitle():inNamespaces(828, 829) and 'header-module' or 'header-template'
) .. '\n' .. tStylesList
end
return mMessageBox.main('mbox', boxArgs)
end
local function renderTrackingCategories(args, tStyles, titleObj)
if yesno(args.nocat) then
return ''
end
local cfg = getConfig()
local cats = {}
-- Error category
if #tStyles < 1 and cfg['error_category'] then
cats[#cats + 1] = cfg['error_category']
end
-- TemplateStyles category
titleObj = titleObj or mw.title.getCurrentTitle()
if (titleObj.namespace == 10 or titleObj.namespace == 828)
and not cfg['subpage_blacklist'][titleObj.subpageText]
then
local category = args.category or cfg['default_category']
if category then
cats[#cats + 1] = category
end
if not yesno(args.noprotcat) and (cfg['protection_conflict_category'] or cfg['padlock_pattern']) then
local currentProt = titleObj.protectionLevels["edit"] and titleObj.protectionLevels["edit"][1] or nil
local addedLevelCat = false
local addedPadlockCat = false
for _, ts in ipairs(tStyles) do
local tsTitleObj = mw.title.new(ts)
local tsProt = tsTitleObj.protectionLevels["edit"] and tsTitleObj.protectionLevels["edit"][1] or nil
if cfg['padlock_pattern'] and tsProt and not addedPadlockCat then
local content = tsTitleObj:getContent()
if not content:find(cfg['padlock_pattern']) then
cats[#cats + 1] = cfg['missing_padlock_category']
addedPadlockCat = true
end
end
if cfg['protection_conflict_category'] and currentProt and tsProt ~= currentProt and not addedLevelCat then
currentProt = cfg['protection_hierarchy'][currentProt] or 0
tsProt = cfg['protection_hierarchy'][tsProt] or 0
if tsProt < currentProt then
addedLevelCat = true
cats[#cats + 1] = cfg['protection_conflict_category']
end
end
end
end
end
for i, cat in ipairs(cats) do
cats[i] = string.format('[[Category:%s]]', cat)
end
return table.concat(cats)
end
function p._main(args)
local cfg = getConfig()
if #args == 0 then
local prefixed = mw.title.getCurrentTitle().prefixedText
prefixed = prefixed:gsub("/doc", "")
args[1] = prefixed .. "/" .. cfg["default_subpage_name"]
end
local tStyles = mTableTools.compressSparseArray(args)
local box = renderBox(tStyles)
local trackingCategories = renderTrackingCategories(args, tStyles)
return box .. trackingCategories
end
function p.main(frame)
local origArgs = frame:getParent().args
local args = {}
for k, v in pairs(origArgs) do
v = v:match('^%s*(.-)%s*$')
if v ~= '' then
args[k] = v
end
end
return p._main(args)
end
return p
fzxzfes95mnfacwuhc6k9zeaixdtemr
740091
740090
2026-05-01T19:28:21Z
Novem Linguae
49714
1 revision imported from [[:en:Module:Uses_TemplateStyles]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740090
Scribunto
text/plain
local yesno = require('Module:Yesno')
local mList = require('Module:List')
local mTableTools = require('Module:TableTools')
local mMessageBox = require('Module:Message box')
local TNT = require('Module:TNT')
local p = {}
local function format(msg, ...)
return TNT.format('I18n/Uses TemplateStyles', msg, ...)
end
local function getConfig()
return mw.loadData('Module:Uses TemplateStyles/config')
end
local function makeEditSup(pageName)
return string.format('<sup>[[[Special:Edit/%s|e]]]</sup>', pageName)
end
-- Build just the "(sandbox)" suffix using the i18n message, without duplicating the main link.
local function makeSandboxSuffix(tsSandboxPrefixed)
-- i18n message is: "$1 ([[$2|sandbox]])"
-- Passing empty $1 yields " ([[:...|sandbox]])" -> trim leading whitespace to get "(sandbox)" portion.
local s = format('sandboxlink', '', ':' .. tsSandboxPrefixed)
return (s:gsub('^%s+', ''))
end
local function renderBox(tStyles)
local boxArgs = {
type = 'notice',
small = true,
image = string.format('[[File:Farm-Fresh css add.svg|32px|alt=%s]]', format('logo-alt'))
}
if #tStyles < 1 then
boxArgs.text = string.format('<strong class="error">%s</strong>', format('error-emptylist'))
else
local cfg = getConfig()
local tStylesLinks = {}
for i, ts in ipairs(tStyles) do
local viewLink = string.format('[[:%s]]', ts)
local editSup = makeEditSup(ts)
local out = viewLink .. editSup
-- Optional sandbox link + sandbox edit link
local tsTitle = mw.title.new(ts)
if tsTitle and cfg['sandbox_title'] then
local tsSandboxTitle = mw.title.new(string.format(
'%s:%s/%s/%s',
tsTitle.nsText,
tsTitle.baseText,
cfg['sandbox_title'],
tsTitle.subpageText
))
if tsSandboxTitle and tsSandboxTitle.exists then
local sandboxSuffix = makeSandboxSuffix(tsSandboxTitle.prefixedText)
local sandboxEditSup = makeEditSup(tsSandboxTitle.prefixedText)
out = out .. ' ' .. sandboxSuffix .. ' ' .. sandboxEditSup
end
end
tStylesLinks[i] = out
end
local tStylesList = mList.makeList('bulleted', tStylesLinks)
boxArgs.text = format(
mw.title.getCurrentTitle():inNamespaces(828, 829) and 'header-module' or 'header-template'
) .. '\n' .. tStylesList
end
return mMessageBox.main('mbox', boxArgs)
end
local function renderTrackingCategories(args, tStyles, titleObj)
if yesno(args.nocat) then
return ''
end
local cfg = getConfig()
local cats = {}
-- Error category
if #tStyles < 1 and cfg['error_category'] then
cats[#cats + 1] = cfg['error_category']
end
-- TemplateStyles category
titleObj = titleObj or mw.title.getCurrentTitle()
if (titleObj.namespace == 10 or titleObj.namespace == 828)
and not cfg['subpage_blacklist'][titleObj.subpageText]
then
local category = args.category or cfg['default_category']
if category then
cats[#cats + 1] = category
end
if not yesno(args.noprotcat) and (cfg['protection_conflict_category'] or cfg['padlock_pattern']) then
local currentProt = titleObj.protectionLevels["edit"] and titleObj.protectionLevels["edit"][1] or nil
local addedLevelCat = false
local addedPadlockCat = false
for _, ts in ipairs(tStyles) do
local tsTitleObj = mw.title.new(ts)
local tsProt = tsTitleObj.protectionLevels["edit"] and tsTitleObj.protectionLevels["edit"][1] or nil
if cfg['padlock_pattern'] and tsProt and not addedPadlockCat then
local content = tsTitleObj:getContent()
if not content:find(cfg['padlock_pattern']) then
cats[#cats + 1] = cfg['missing_padlock_category']
addedPadlockCat = true
end
end
if cfg['protection_conflict_category'] and currentProt and tsProt ~= currentProt and not addedLevelCat then
currentProt = cfg['protection_hierarchy'][currentProt] or 0
tsProt = cfg['protection_hierarchy'][tsProt] or 0
if tsProt < currentProt then
addedLevelCat = true
cats[#cats + 1] = cfg['protection_conflict_category']
end
end
end
end
end
for i, cat in ipairs(cats) do
cats[i] = string.format('[[Category:%s]]', cat)
end
return table.concat(cats)
end
function p._main(args)
local cfg = getConfig()
if #args == 0 then
local prefixed = mw.title.getCurrentTitle().prefixedText
prefixed = prefixed:gsub("/doc", "")
args[1] = prefixed .. "/" .. cfg["default_subpage_name"]
end
local tStyles = mTableTools.compressSparseArray(args)
local box = renderBox(tStyles)
local trackingCategories = renderTrackingCategories(args, tStyles)
return box .. trackingCategories
end
function p.main(frame)
local origArgs = frame:getParent().args
local args = {}
for k, v in pairs(origArgs) do
v = v:match('^%s*(.-)%s*$')
if v ~= '' then
args[k] = v
end
end
return p._main(args)
end
return p
fzxzfes95mnfacwuhc6k9zeaixdtemr
Giorgio Almirante
0
119367
740007
552400
2026-05-01T14:29:51Z
Cryptocurrency777
73698
740007
wikitext
text/x-wiki
A
(not to be confused with Giorgio armani 💎)
cg083j19lraecbcumzwmaisyib177ew
Moon
0
119986
740119
737215
2026-05-01T21:28:20Z
Cryptocurrency777
73698
740119
wikitext
text/x-wiki
It's something in the sky
[[File:Moon.jpg|thumb]]
8gdz06hqszyf77pk2euqcxr0wgjgmn4
World Championships of Ski Mountaineering
0
122035
740123
690838
2026-05-02T00:06:27Z
InternetArchiveBot
34092
Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.5
740123
wikitext
text/x-wiki
{{update|date=August 2018}}
The '''World Championships of Ski Mountaineering''' are biannually held [[ski mountaineering]] competitions.
== History ==
The events were originally sanctioned by the [[International Council for Ski Mountaineering Competitions]] (ISMC). The first official world mastership of the ISMC was carried out in the "[[International observance#2000s|International Year of Mountains]]" (2002), declared by the [[United Nations]]. The championship was held in [[Serre Chevalier]], [[France]], from January 24 to January 27, 2002. Prior the Italian [[Trofeo Mezzalama]] was held as "World Championship of Ski Mountaineering" with the classes "Civilians", "[[Soldier]]s" and "[[Mountain guide]]s" in 1975.<ref name="Rolf Majcen">[[Rolf Majcen]]: [http://www.mountains2b.com/76-Weltmeisterschaften_im_Skibergsteigen-,e_14310,r_1100.htm ''Weltmeisterschaften im Skibergsteigen''] {{Webarchive|url=https://archive.is/20121209103029/http://www.mountains2b.com/76-Weltmeisterschaften_im_Skibergsteigen-,e_14310,r_1100.htm|date=2012-12-09}} (German), February, 2004.</ref> Because the ISMC merged into the [[International Ski Mountaineering Federation]] (ISMF) in 2008, the next championships were sanctioned by the ISMF.<ref>[[Karl Posch]]: [http://www.teamformultimedia.at/_iu_write/cms/editor/page.php?user=askimo&vorlage=vorlage.php&file=index.htm&newsid=718& Weltmeisterschaft Schweiz Tag 5 - Neuer Verband der Schibergsteiger: ISMF] {{Webarchive|url=https://web.archive.org/web/20110706095145/http://www.teamformultimedia.at/_iu_write/cms/editor/page.php?user=askimo&vorlage=vorlage.php&file=index.htm&newsid=718&|date=2011-07-06}} (German), February 27, 2008.</ref> In 2011, the originally planned 9th edition of the [[European Championships of Ski Mountaineering]] at last was held as 6th edition of the World Championships.
{{Expand section|missing medalists|date=April 2009}}
{| class="wikitable"
! colspan="5" | Medalist teams of the 1975 Trofeo Mezzalama
|-
! rowspan="3" | "civilian teams"
| [[File:Med 1.png]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Renzo Meynet]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Osvaldo Ronc]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Mirko Stangalino]]<ref name="Albo d'oro">{{Cite web |url=http://www.old.inalto.org/magazine/trofeo_mezzalama_2005_04.shtml |title=''Albo d'oro'' |access-date=2020-05-28 |archive-date=2017-08-09 |archive-url=https://web.archive.org/web/20170809172431/http://www.old.inalto.org/magazine/trofeo_mezzalama_2005_04.shtml |url-status=dead }}</ref>
|-
! bgcolor="silver" | [[File:Med 2.png]]
| bgcolor="#F7F6A8" |{{flagicon|}}
| bgcolor="#F7F6A8" |{{flagicon|}}
| bgcolor="#F7F6A8" |{{flagicon|}}
|-
! bgcolor="bronze" | [[File:Med 3.png]]
| bgcolor="#F7F6A8" |{{flagicon|}}
| bgcolor="#F7F6A8" |{{flagicon|}}
| bgcolor="#F7F6A8" |{{flagicon|}}
|-
! rowspan="3" | "military teams"
| [[File:Med 1.png]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Angelo Genuin]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Bruno Bonaldi]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Luigi Weiss|Luigi "Gigi" Weiss]]<ref name="Albo d'oro" />
|-
! bgcolor="silver" | [[File:Med 2.png]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Gianfranco Stella]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Aldo Stella (skier)|Aldo Stella]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Leo Vidi]]<ref name="Genuin Angelo">{{Cite web |url=http://www.fondoitalia.it/personaggi/Genuin_Angelo.html |title=''Genuin Angelo'' |access-date=2020-05-28 |archive-date=2011-07-22 |archive-url=https://web.archive.org/web/20110722033827/http://www.fondoitalia.it/personaggi/Genuin_Angelo.html |url-status=dead }}</ref>
|-
! bgcolor="bronze" | [[File:Med 3.png]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Willy Bertin]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Felice Darioli]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Fabrizio Pedranzini]]<ref name="Genuin Angelo" />
|-
! rowspan="3" | "mountain guides"
| [[File:Med 1.png]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Oreste Squinobal]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Arturo Squinobal]]
| bgcolor="#F7F6A8" |{{flagicon|Italy}} [[Lorenzo Squinobal]]<ref name="Albo d'oro" />
|-
! bgcolor="silver" | [[File:Med 2.png]]
| bgcolor="#F7F6A8" |{{flagicon|}}
| bgcolor="#F7F6A8" |{{flagicon|}}
| bgcolor="#F7F6A8" |{{flagicon|}}
|-
! bgcolor="bronze" | [[File:Med 3.png]]
| bgcolor="#F7F6A8" |{{flagicon|}}
| bgcolor="#F7F6A8" |{{flagicon|}}
| bgcolor="#F7F6A8" |{{flagicon|}}
|}
Further venues of the ISMC World Championships were the [[Aran Valley]] (Spain) in 2004, the Italian [[Province of Cuneo]] in 2006, and [[Portes du Soleil]] (Switzerland) in 2008.<ref name="History">[http://theuiaa.org/interna.php?page=Ski_history&change_language=ENG ''History of ski mountaineering''] {{Webarchive|url=https://web.archive.org/web/20131213195504/http://theuiaa.org/interna.php?page=Ski_history&change_language=ENG |date=2013-12-13 }}, UIAA.</ref> The World Championships are supported by the national organizations of the carrying out countries.
== Ratings ==
The disciplines are rated by gender and age groups. In 2002, only individual and team (2 racers) races were held and rated, added with a combined ranking. At the 2004 championship a relay event and a [[vertical race]] competition were added. The men's relay teams were of four racers and the women's teams of three. In the following years all relay teams were of four ski mountaineers. In 2006 the relay race was canceled because of bad snow conditions, and consequently there was no combined ranking. At the 2008 World Masterships a long distance race was added.
The national squads are often mixed with up an coming athletes of the "Espoirs"-level. Some nations do not have squads with enough racers for all disciplines.
== Medalist nations and disciplines ==
(by point-awarding system)<ref name="sports123">[http://sports123.com/mou/index.html ''Mountaineering-World Ski Mountaineering Championships''] {{Webarchive|url=https://web.archive.org/web/20050207011458/http://sports123.com/mou/index.html |date=2005-02-07 }}, ''sports123.com''.</ref>
{| class="wikitable"
! rowspan="2" | year
! rowspan="2" | venue
! rowspan="2" bgcolor="gold" | 1.
! rowspan="2" bgcolor="silver" | 2.
! rowspan="2" bgcolor="#cc9966" | 3.
! colspan="6" | disciplines
|-
| individual
| team
| combination
| relay
| vertical race
| long distance
|-
| [[2002 World Championship of Ski Mountaineering|1st 2002]]
| Serre Chevalier, [[France|FRA]]
| bgcolor="gold" | {{flag|FRA}}
| bgcolor="silver" | {{flag|ITA}}
| bgcolor="#cc9966" | {{flag|SUI}}
! X
! X
! X
! -
! -
! -
|-
| [[2004 World Championship of Ski Mountaineering|2nd 2004]]
| Aran Valley, [[Spain|ESP]]
| bgcolor="gold" | {{flag|SUI}}
| bgcolor="silver" | {{flag|ITA|2003}}
| bgcolor="#cc9966" | {{flag|FRA}}
! X
! X
! X
! X
! X
! -
|-
| [[2006 World Championship of Ski Mountaineering|3rd 2006]]
| Province of Cuneo, [[Italy|ITA]]
| bgcolor="gold" | {{flag|ITA}}
| bgcolor="silver" | {{flag|SUI}}
| bgcolor="#cc9966" | {{flag|FRA}}
! -
! X
! -
! X
! X
! -
|-
| [[2008 World Championship of Ski Mountaineering|4th 2008]]
| Champery, Portes du Soleil, [[Switzerland|SUI]]
| bgcolor="gold" | {{flag|ITA}}
| bgcolor="silver" | {{flag|FRA}}
| bgcolor="#cc9966" | {{flag|SUI}}
! X
! X
! X
! X
! X
! X
|-
| [[2010 World Championship of Ski Mountaineering|5th 2010]]
| Gran Valira, [[Andorra|AND]]
| bgcolor="gold" | {{flag|ITA}}
| bgcolor="silver" | {{flag|FRA}}
| bgcolor="#cc9966" | {{flag|SUI}}
! X
! X
! X
! X
! X
! -
|-
| [[2011 World Championship of Ski Mountaineering|6th 2011]]
| Claut, [[Italia|ITA]]
| bgcolor="gold" | {{flag|FRA}}
| bgcolor="silver" | {{flag|SUI}}
| bgcolor="#cc9966" | {{flag|ITA}}
! X
! X
! X
! X
! X
! -
|-
| [[2013 World Championship of Ski Mountaineering|7th 2013]]
| [[Puy-Saint-Vincent]], [[Pelvoux]], [[France|FRA]]
| bgcolor="gold" | {{flag|ITA}}
| bgcolor="silver" | {{flag|FRA}}
| bgcolor="#cc9966" | {{flag|SUI}}
! X
! X
! X
! X
! X
! -
|-
| [[2015 World Championship of Ski Mountaineering|8th 2015]]
| [[Verbier]], [[Switzerland|SUI]]
| bgcolor="gold" | {{flag|ITA}}
| bgcolor="silver" | {{flag|FRA}}
| bgcolor="#cc9966" | {{flag|SUI}}
! X
! X
! X
! X
! X
! -
|-
|-
| [[2017 World Championship of Ski Mountaineering|9th 2017]]
| [[Tambre]] - [[Piancavallo]], [[Italy|ITA]]
|{{flag|ITA}}
|{{flag|SUI}}
|{{flag|FRA}}
! X
! X
! X
! X
! X
! -
|-
|10th 2019
|Villars-sur-Ollon, SUI
|{{flag|SUI}}
|{{flag|ITA}}
|{{flag|FRA}}
!X
!X
!X
!X
!X
!-
|-
|}
==See also==
*[[ISMF World Cup Ski Mountaineering]]
== References ==
{{reflist|2}}
== External links ==
* [http://www.skieverest.com/skimount2002.htm Ski Mountaineering World Championship 2002] {{Webarchive|url=https://web.archive.org/web/20090329071306/http://www.skieverest.com/skimount2002.htm |date=2009-03-29 }}, president of the UIAA [[Ian McNaught-Davis]]
{{World Championships of Ski Mountaineering}}
{{Main world championships}}
[[Category:World Championships of Ski Mountaineering| ]]
[[Category:Ski mountaineering competitions]]
[[Category:World championships in skiing|Mountaineering]]
5pj9edhplbs7h2n44u71y311odp90gb
Template:AfC submission/doc
10
122523
740086
584342
2026-04-18T14:08:26Z
en>W.andrea
0
/* TemplateData */ +header
740086
wikitext
text/x-wiki
{{Documentation subpage}}
{{#ifeq:{{#invoke:High-use|num|x|demo={{ROOTPAGENAME}}}}|many||{{High-use|demo={{ROOTPAGENAME}}}}}}
{{Wikipedia:Article alerts/Bot use warning}}
{{transclusionless}}
{{lua|Module:AfC submission catcheck}}
{{Uses TemplateStyles|Template:AfC submission/styles.css}}
== Usage ==
This template is meant as a single template for marking the status of an [[WP:Articles for creation|Articles for creation]] submission. To use this template to submit an article for review, use {{Tlsp|submit| ''username'' }}, where ''username'' is the username of the draft's creator (left blank it will assume it is you). The template uses one of the following parameter options:
* {{Tlp|AfC submission|T}} produces the unsubmitted draft banner {{tl|AfC submission/draft}}
** Shortcut: {{Tlps|AfC draft| ''username'' }}, where ''username'' is the username of the draft's creator (left blank it will assume it is you).
* {{Tl|AfC submission}} (no first parameter) produces the pending submission banner {{tl|AfC submission/pending}}
** This may also be produced using {{Tlps|submit|''username''}}, as discussed above.
* {{Tlp|AfC submission|D}} produces the declined banner {{tl|AfC submission/declined}}, or the rejected banner {{tl|AfC submission/rejected}}, depending on whether the <code>reject</code> parameter is passed with some value
* {{Tlp|AfC submission|R}} produces the reviewing banner {{tl|AfC submission/reviewing}}
* When a submission is moved into mainspace, it produces the "created" message {{tl|AfC submission/created}}
== Decline options ==
When declining an AfC submission, this template can take additional parameters to include a decline reason. All decline templates used under the old system (listed {{oldid|Wikipedia:WikiProject Articles for creation/Templates#Decision templates that can be used for declined articles|242565191|here}}) are included in this template. The idea is to streamline the decline process; instead of including three templates, now a reviewer need only add a single parameter to the template to include a decline reason.
To include the decline reason, use {{tlx|AfC submission|D|reason}}, replacing ''reason'' with one of the abbreviations listed at [[Template:AfC submission/comments]]. That page also shows the resulting messages. Some ''reason''s may take an additional parameter ({{tlx|AfC submission|D|reason|Additional parameter}}). These parameters are optional, except the additional parameter for ''reason'' itself.
If no parameter is included after '''D''', or an invalid parameter is entered, the invalid parameter will appear as the decline reason.
== Subpages==
See [[Special:PrefixIndex/Template:AfC submission/]]
* [[Template:AfC submission/tools|/tools]] – the links to help reviewers at the bottom of the various templates
* [[Template:AfC submission/helptools|/helptools]] – the links to policies and ways to get sources seen on several of the templates
* [[Template:AfC submission/old|/old]] – template to mimic the old-style templates
* [[Template:AfC submission/comments|/comments]] – stores the comments used by the various parameters of the decline template
** [[Template:AfC submission/table|/table]] – simplifies the code on the comments page
* [[Template:AfC submission/reject reasons|/reject reasons]] – stores the comments used by the various parameters of the reject template
* The six banners described above:
** [[Template:AfC submission/pending|/pending]]
** [[Template:AfC submission/declined|/declined]]
** [[Template:AfC submission/rejected|/rejected]]
** [[Template:AfC submission/reviewing|/reviewing]]
** [[Template:AfC submission/draft|/draft]]
** [[Template:AfC submission/created|/created]]
* [[Template:AfC submission/misplaced|/misplaced]] – the code used for submissions created in any namespace other than ''Draft''
* [[Template:AfC submission/doc|/doc]] – the documentation you are reading now
* [[Template:AfC submission/submit|/submit]] – code for new submissions
* [[Template:AfC submission/draftnew|/draftnew]] – code for new draft submissions
* Preload templates:
** [[Template:AfC submission/banner preload|/banner preload]]
** [[Template:AfC submission/banner preload (bio)|/banner preload (bio)]]
** [[Template:AfC submission/user talk preload|/user talk preload]]
** [[Template:AfC submission/user talk preload (hold)|/user talk preload (hold)]]
** [[Template:AfC submission/declined/HD_preload|/declined/HD_preload]]
* Editnotice templates:
** [[Template:AfC submission/banner editintro|/banner editintro]]
** [[Template:AfC submission/user talk editintro|/user talk editintro]]
** [[Template:AfC submission/user talk editintro (hold)|/user talk editintro (hold)]]
== TemplateData ==
{{TemplateData header}}
<templatedata>
{
"params": {
"1": {},
"2": {
"label": "comment",
"description": "reason the submission was declined",
"example": "nn",
"suggestedvalues": [
"nn",
"reason",
"neo",
"web",
"prof",
"academic",
"athlete",
"sport",
"music",
"band",
"film",
"book",
"event",
"corp",
"org",
"bio",
"list",
"med",
"geo",
"species",
"astro",
"number",
"creative",
"school",
"cat",
"blank",
"notenglish",
"test",
"redirect",
"van",
"ilc",
"blp",
"cv",
"source",
"rs",
"not",
"news",
"oneevent",
"dict",
"plot",
"joke",
"ai",
"llm",
"resume",
"ecr",
"nosource",
"essay",
"npov",
"advert",
"spam",
"exists",
"duplicate",
"context",
"mergeto",
"afd"
],
"suggested": true
},
"3": {},
"reject": {},
"ns": {},
"ts": {},
"a": {},
"u": {},
"details": {},
"reason2": {},
"details2": {},
"small": {},
"decliner": {},
"declinets": {},
"type": {},
"demo": {}
}
}
</templatedata>
== See also ==
* Templates for user talk pages:
** {{tl|AfC talk}}, for accepted submissions
** {{tl|Afc decline}} and {{tl|Afc reject}}, for declined/rejected submissions
* {{tl|AfC date category}}
<includeonly>{{Sandbox other||
[[Category:Draft namespace templates]]
[[Category:WikiProject Articles for creation templates]]
[[Category:Lua-based templates]]
}}</includeonly>
70cfsg8lampqhtjkdcltmfjts8s9vg5
740087
740086
2026-05-01T19:28:20Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/doc]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740086
wikitext
text/x-wiki
{{Documentation subpage}}
{{#ifeq:{{#invoke:High-use|num|x|demo={{ROOTPAGENAME}}}}|many||{{High-use|demo={{ROOTPAGENAME}}}}}}
{{Wikipedia:Article alerts/Bot use warning}}
{{transclusionless}}
{{lua|Module:AfC submission catcheck}}
{{Uses TemplateStyles|Template:AfC submission/styles.css}}
== Usage ==
This template is meant as a single template for marking the status of an [[WP:Articles for creation|Articles for creation]] submission. To use this template to submit an article for review, use {{Tlsp|submit| ''username'' }}, where ''username'' is the username of the draft's creator (left blank it will assume it is you). The template uses one of the following parameter options:
* {{Tlp|AfC submission|T}} produces the unsubmitted draft banner {{tl|AfC submission/draft}}
** Shortcut: {{Tlps|AfC draft| ''username'' }}, where ''username'' is the username of the draft's creator (left blank it will assume it is you).
* {{Tl|AfC submission}} (no first parameter) produces the pending submission banner {{tl|AfC submission/pending}}
** This may also be produced using {{Tlps|submit|''username''}}, as discussed above.
* {{Tlp|AfC submission|D}} produces the declined banner {{tl|AfC submission/declined}}, or the rejected banner {{tl|AfC submission/rejected}}, depending on whether the <code>reject</code> parameter is passed with some value
* {{Tlp|AfC submission|R}} produces the reviewing banner {{tl|AfC submission/reviewing}}
* When a submission is moved into mainspace, it produces the "created" message {{tl|AfC submission/created}}
== Decline options ==
When declining an AfC submission, this template can take additional parameters to include a decline reason. All decline templates used under the old system (listed {{oldid|Wikipedia:WikiProject Articles for creation/Templates#Decision templates that can be used for declined articles|242565191|here}}) are included in this template. The idea is to streamline the decline process; instead of including three templates, now a reviewer need only add a single parameter to the template to include a decline reason.
To include the decline reason, use {{tlx|AfC submission|D|reason}}, replacing ''reason'' with one of the abbreviations listed at [[Template:AfC submission/comments]]. That page also shows the resulting messages. Some ''reason''s may take an additional parameter ({{tlx|AfC submission|D|reason|Additional parameter}}). These parameters are optional, except the additional parameter for ''reason'' itself.
If no parameter is included after '''D''', or an invalid parameter is entered, the invalid parameter will appear as the decline reason.
== Subpages==
See [[Special:PrefixIndex/Template:AfC submission/]]
* [[Template:AfC submission/tools|/tools]] – the links to help reviewers at the bottom of the various templates
* [[Template:AfC submission/helptools|/helptools]] – the links to policies and ways to get sources seen on several of the templates
* [[Template:AfC submission/old|/old]] – template to mimic the old-style templates
* [[Template:AfC submission/comments|/comments]] – stores the comments used by the various parameters of the decline template
** [[Template:AfC submission/table|/table]] – simplifies the code on the comments page
* [[Template:AfC submission/reject reasons|/reject reasons]] – stores the comments used by the various parameters of the reject template
* The six banners described above:
** [[Template:AfC submission/pending|/pending]]
** [[Template:AfC submission/declined|/declined]]
** [[Template:AfC submission/rejected|/rejected]]
** [[Template:AfC submission/reviewing|/reviewing]]
** [[Template:AfC submission/draft|/draft]]
** [[Template:AfC submission/created|/created]]
* [[Template:AfC submission/misplaced|/misplaced]] – the code used for submissions created in any namespace other than ''Draft''
* [[Template:AfC submission/doc|/doc]] – the documentation you are reading now
* [[Template:AfC submission/submit|/submit]] – code for new submissions
* [[Template:AfC submission/draftnew|/draftnew]] – code for new draft submissions
* Preload templates:
** [[Template:AfC submission/banner preload|/banner preload]]
** [[Template:AfC submission/banner preload (bio)|/banner preload (bio)]]
** [[Template:AfC submission/user talk preload|/user talk preload]]
** [[Template:AfC submission/user talk preload (hold)|/user talk preload (hold)]]
** [[Template:AfC submission/declined/HD_preload|/declined/HD_preload]]
* Editnotice templates:
** [[Template:AfC submission/banner editintro|/banner editintro]]
** [[Template:AfC submission/user talk editintro|/user talk editintro]]
** [[Template:AfC submission/user talk editintro (hold)|/user talk editintro (hold)]]
== TemplateData ==
{{TemplateData header}}
<templatedata>
{
"params": {
"1": {},
"2": {
"label": "comment",
"description": "reason the submission was declined",
"example": "nn",
"suggestedvalues": [
"nn",
"reason",
"neo",
"web",
"prof",
"academic",
"athlete",
"sport",
"music",
"band",
"film",
"book",
"event",
"corp",
"org",
"bio",
"list",
"med",
"geo",
"species",
"astro",
"number",
"creative",
"school",
"cat",
"blank",
"notenglish",
"test",
"redirect",
"van",
"ilc",
"blp",
"cv",
"source",
"rs",
"not",
"news",
"oneevent",
"dict",
"plot",
"joke",
"ai",
"llm",
"resume",
"ecr",
"nosource",
"essay",
"npov",
"advert",
"spam",
"exists",
"duplicate",
"context",
"mergeto",
"afd"
],
"suggested": true
},
"3": {},
"reject": {},
"ns": {},
"ts": {},
"a": {},
"u": {},
"details": {},
"reason2": {},
"details2": {},
"small": {},
"decliner": {},
"declinets": {},
"type": {},
"demo": {}
}
}
</templatedata>
== See also ==
* Templates for user talk pages:
** {{tl|AfC talk}}, for accepted submissions
** {{tl|Afc decline}} and {{tl|Afc reject}}, for declined/rejected submissions
* {{tl|AfC date category}}
<includeonly>{{Sandbox other||
[[Category:Draft namespace templates]]
[[Category:WikiProject Articles for creation templates]]
[[Category:Lua-based templates]]
}}</includeonly>
70cfsg8lampqhtjkdcltmfjts8s9vg5
Template:AfC submission
10
122525
740068
584334
2025-11-27T04:04:55Z
en>Sohom Datta
0
per talk page
740068
wikitext
text/x-wiki
{{SAFESUBST:<noinclude />#invoke:Unsubst||$B=<includeonly>__NONEWSECTIONLINK__</includeonly>{{AfC submission/{{#if:{{NAMESPACE}}
|{{#switch:{{ucfirst:{{{1|}}}}}
|R = reviewing
|D = {{#if:{{{reject|}}}|rejected|declined}}
|T = draft
|#default = pending
}}
|{{#if:{{#ifeq:{{REVISIONID}}|{{REVISIONID:{{FULLPAGENAME}}}}|0|}}{{#ifeq:{{ucfirst:{{{1|}}}}}|D||0}}
|{{#ifeq:{{{ns}}}|0
|misplaced
|created
}}
|declined
}}
}}
|submit={{{ts|}}}
|author={{{a|}}}
|user={{{u|}}}
|reason={{{2|}}}
|details={{{3|{{{details|}}}}}}
|reason2={{{reason2|}}}
|details2={{{details2|}}}
|small={{{small|}}}
|decliner={{{decliner|}}}
|declinets={{{declinets|}}}
|ts={{{ts|}}}
|type={{{type}}}
|1={{{2|}}}
|2={{{3|}}}
|demo={{{demo|}}}
}}<includeonly>{{#if:{{{demo|}}}|| {{#ifeq:{{ucfirst:{{{1|}}}}}|T|| [[Category:{{AfC date category|ts={{{ts|}}}}}|{{If then show|{{SUBPAGENAME}}|{{PAGENAME}}}}]]
{{#invoke:AfC submission catcheck|checkforcats}} }}}}<!--IP tracking-->{{#switch: {{IsIPAddress|{{{u}}}}}|4|6={{#switch:{{ucfirst:{{{1|}}}}}
|R =
|D = {{#if:{{{reject|}}}|[[Category:Rejected draft pages submitted for review by an IP]]|[[Category:Declined draft pages submitted for review by an IP]]}}
|T = [[Category:Unsubmitted draft pages created by an IP]]
|#default = [[Category:Draft pages submitted for review by an IP]]
}}
}}</includeonly>__NOINDEX__}}<noinclude>
{{Documentation}}
</noinclude>
2elue6szvkmdloc6lm1zz77pvvbwsse
740069
740068
2026-05-01T19:28:17Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740068
wikitext
text/x-wiki
{{SAFESUBST:<noinclude />#invoke:Unsubst||$B=<includeonly>__NONEWSECTIONLINK__</includeonly>{{AfC submission/{{#if:{{NAMESPACE}}
|{{#switch:{{ucfirst:{{{1|}}}}}
|R = reviewing
|D = {{#if:{{{reject|}}}|rejected|declined}}
|T = draft
|#default = pending
}}
|{{#if:{{#ifeq:{{REVISIONID}}|{{REVISIONID:{{FULLPAGENAME}}}}|0|}}{{#ifeq:{{ucfirst:{{{1|}}}}}|D||0}}
|{{#ifeq:{{{ns}}}|0
|misplaced
|created
}}
|declined
}}
}}
|submit={{{ts|}}}
|author={{{a|}}}
|user={{{u|}}}
|reason={{{2|}}}
|details={{{3|{{{details|}}}}}}
|reason2={{{reason2|}}}
|details2={{{details2|}}}
|small={{{small|}}}
|decliner={{{decliner|}}}
|declinets={{{declinets|}}}
|ts={{{ts|}}}
|type={{{type}}}
|1={{{2|}}}
|2={{{3|}}}
|demo={{{demo|}}}
}}<includeonly>{{#if:{{{demo|}}}|| {{#ifeq:{{ucfirst:{{{1|}}}}}|T|| [[Category:{{AfC date category|ts={{{ts|}}}}}|{{If then show|{{SUBPAGENAME}}|{{PAGENAME}}}}]]
{{#invoke:AfC submission catcheck|checkforcats}} }}}}<!--IP tracking-->{{#switch: {{IsIPAddress|{{{u}}}}}|4|6={{#switch:{{ucfirst:{{{1|}}}}}
|R =
|D = {{#if:{{{reject|}}}|[[Category:Rejected draft pages submitted for review by an IP]]|[[Category:Declined draft pages submitted for review by an IP]]}}
|T = [[Category:Unsubmitted draft pages created by an IP]]
|#default = [[Category:Draft pages submitted for review by an IP]]
}}
}}</includeonly>__NOINDEX__}}<noinclude>
{{Documentation}}
</noinclude>
2elue6szvkmdloc6lm1zz77pvvbwsse
Template:AfC submission/helptools
10
122528
740080
584320
2025-08-21T19:57:59Z
en>Novem Linguae
0
talk page edit request - removing double bold
740080
wikitext
text/x-wiki
{{#ifeq:{{{help|}}}|no||
----
{{hidden|Where to get help|
* If you need help '''editing or submitting your draft''', please {{Plain link|url={{fullurl:Wikipedia:WikiProject_Articles_for_creation/Help_desk/New question|withJS=MediaWiki:AFCHD-wizard.js&page={{FULLPAGENAMEE}}}}|name='''ask us a question'''}} at the AfC Help Desk or get '''[[Wikipedia:IRC help disclaimer|live help]]''' from experienced editors. These venues are only for help with editing and the submission process, not to get reviews.
* If you need '''feedback on your draft''', or if the review is taking a lot of time, you can try asking for help on the [[Help:Talk pages|talk page]] of a [[Wikipedia:WikiProject#Finding a project|relevant WikiProject]]. Some WikiProjects are more active than others so a speedy reply is not guaranteed.
}}}}
{{#ifeq:{{{improve|}}}|no||
{{hidden|How to improve a draft|
{{columns-list|colwidth=36em|
*[[Wikipedia:Contributing to Wikipedia]] – a basic overview on how to edit Wikipedia.
*[[Help:Wikitext]] – how to use the markup
*[[Help:Referencing for beginners]] – how to include references
*[[Wikipedia:Article development]] – how to develop your article
*[[Wikipedia:Writing better articles]] – how to improve your article
*[[Wikipedia:Verifiability]] – make sure your article includes [[Wikipedia:Reliable sources|reliable]] [[Wikipedia:Independent sources|third-party sources]]
}}
You can also browse [[Wikipedia:Featured articles]] and [[Wikipedia:Good articles]] to find examples of Wikipedia's best writing on topics similar to your proposed article.
}}}}
{{#ifeq:{{{speedyreviews|}}}|no||
{{hidden|Improving your odds of a speedy review|2=
To improve your odds of a faster review, tag your draft with relevant [[WP:WikiProject|WikiProject]] tags using the button below. This will let reviewers know a new draft has been submitted in their area of interest. For instance, if you wrote about a female astronomer, you would want to add the ''Biography'', ''Astronomy'', and ''Women scientists'' tags.
<div style="text-align: center;">{{Clickable button|url=https://en.wikipedia.org/wiki/Wikipedia:WikiProject_Articles_for_creation/Add_WikiProject_tags?withJS=MediaWiki:AFC-add-project-tags.js&title={{urlencode:{{FULLPAGENAME}}|WIKI}} |Add tags to your draft|color=blue|class=mw-ui-progressive}}</div>
}}}}
{{#ifeq:{{{resources|}}}|no||
{{hidden|Editor resources|
*{{automated tools}}
}}}}<noinclude>{{documentation}}</noinclude>
8d9f8c56u4turlrymmthil9oehr2ci5
740081
740080
2026-05-01T19:28:19Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/helptools]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740080
wikitext
text/x-wiki
{{#ifeq:{{{help|}}}|no||
----
{{hidden|Where to get help|
* If you need help '''editing or submitting your draft''', please {{Plain link|url={{fullurl:Wikipedia:WikiProject_Articles_for_creation/Help_desk/New question|withJS=MediaWiki:AFCHD-wizard.js&page={{FULLPAGENAMEE}}}}|name='''ask us a question'''}} at the AfC Help Desk or get '''[[Wikipedia:IRC help disclaimer|live help]]''' from experienced editors. These venues are only for help with editing and the submission process, not to get reviews.
* If you need '''feedback on your draft''', or if the review is taking a lot of time, you can try asking for help on the [[Help:Talk pages|talk page]] of a [[Wikipedia:WikiProject#Finding a project|relevant WikiProject]]. Some WikiProjects are more active than others so a speedy reply is not guaranteed.
}}}}
{{#ifeq:{{{improve|}}}|no||
{{hidden|How to improve a draft|
{{columns-list|colwidth=36em|
*[[Wikipedia:Contributing to Wikipedia]] – a basic overview on how to edit Wikipedia.
*[[Help:Wikitext]] – how to use the markup
*[[Help:Referencing for beginners]] – how to include references
*[[Wikipedia:Article development]] – how to develop your article
*[[Wikipedia:Writing better articles]] – how to improve your article
*[[Wikipedia:Verifiability]] – make sure your article includes [[Wikipedia:Reliable sources|reliable]] [[Wikipedia:Independent sources|third-party sources]]
}}
You can also browse [[Wikipedia:Featured articles]] and [[Wikipedia:Good articles]] to find examples of Wikipedia's best writing on topics similar to your proposed article.
}}}}
{{#ifeq:{{{speedyreviews|}}}|no||
{{hidden|Improving your odds of a speedy review|2=
To improve your odds of a faster review, tag your draft with relevant [[WP:WikiProject|WikiProject]] tags using the button below. This will let reviewers know a new draft has been submitted in their area of interest. For instance, if you wrote about a female astronomer, you would want to add the ''Biography'', ''Astronomy'', and ''Women scientists'' tags.
<div style="text-align: center;">{{Clickable button|url=https://en.wikipedia.org/wiki/Wikipedia:WikiProject_Articles_for_creation/Add_WikiProject_tags?withJS=MediaWiki:AFC-add-project-tags.js&title={{urlencode:{{FULLPAGENAME}}|WIKI}} |Add tags to your draft|color=blue|class=mw-ui-progressive}}</div>
}}}}
{{#ifeq:{{{resources|}}}|no||
{{hidden|Editor resources|
*{{automated tools}}
}}}}<noinclude>{{documentation}}</noinclude>
8d9f8c56u4turlrymmthil9oehr2ci5
Template:AfC submission/pending
10
122529
740070
487129
2026-03-31T07:56:16Z
en>WOSlinker
0
fix lint issues
740070
wikitext
text/x-wiki
{{ombox
| templatestyles = AfC submission/styles.css
| class = afc-submission-pending
| type = notice
| image =[[Image:AFC-Logo.svg|center|75px]]
| text = <div style="text-align: center;"><span class="afc-submission-header">Review waiting, please be patient.</span></div>
{{AfC status/age}} There are {{AfC status/backlog}} [[:Category:Pending AfC submissions|waiting for review]].
----
* If the submission is '''accepted''', then this page will be moved into the article space.<br>
* If the submission is '''declined''', then the reason will be posted here.<br/>
* In the meantime, you can continue to improve this submission by editing normally.
{{AfC submission/helptools}}
{{AfC submission/tools
|submit={{{submit|}}}
|author={{{author|}}}
|user={{{user|}}}
}}
}}{{#if:{{{demo|}}}||{{#ifeq:{{ROOTPAGENAME}}|AfC submission|
|{{NOINDEX}}[[Category:Pending AfC submissions|P{{#if:{{{submit|}}}
|{{#expr:trunc({{{submit}}}/100)}}|3
}}]]
[[Category:{{AfC age category|ts={{{submit|}}}|status=pending}}|{{#if:{{{submit|}}}
|{{#expr:trunc({{{submit}}}/100)}}|3
}}]]
{{#ifexist:{{SUBPAGENAME}}|{{#ifeq:{{lc:{{SUBPAGENAME}}}}|sandbox||[[Category:AfC submissions with the same name as existing articles|{{SUBPAGENAME}}]]}}}}
}}<!--
See https://en.wikipedia.org/w/index.php?title=Wikipedia_talk%3AWikiProject_Articles_for_creation&diff=548201182&oldid=548180058
-->{{#ifeq:{{{type|}}}|template|[[Category:Pending template and disambiguation AfC submissions|{{#if:{{{submit|}}}
|{{#expr:trunc({{{submit}}}/100)}}|3
}}]]}}
{{#ifeq:{{{type|}}}|dab|[[Category:Pending template and disambiguation AfC submissions|{{#if:{{{submit|}}}
|{{#expr:trunc({{{submit}}}/100)}}|3
}}]]}}
{{#ifeq:{{NAMESPACE}}|{{ns:2}}|[[Category:Pending AfC submissions in userspace]]}}{{#ifexpr:{{PAGESIZE:{{FULLPAGENAME}}|R}}<450|[[Category:Pending AfC submissions less than 450 bytes long]]|}}{{#ifeq:{{NAMESPACE}}|{{ns:10}}||{{#ifeq:{{NUMBEROFSECTIONS}}|0|[[Category:Pending AfC submissions without a section]]}}}}}}<noinclude>
{{documentation|Template:AfC_submission/doc}}
</noinclude>
cs5ci3b6dopfhasi6vldefsdzvakfd8
740071
740070
2026-05-01T19:28:18Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/pending]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740070
wikitext
text/x-wiki
{{ombox
| templatestyles = AfC submission/styles.css
| class = afc-submission-pending
| type = notice
| image =[[Image:AFC-Logo.svg|center|75px]]
| text = <div style="text-align: center;"><span class="afc-submission-header">Review waiting, please be patient.</span></div>
{{AfC status/age}} There are {{AfC status/backlog}} [[:Category:Pending AfC submissions|waiting for review]].
----
* If the submission is '''accepted''', then this page will be moved into the article space.<br>
* If the submission is '''declined''', then the reason will be posted here.<br/>
* In the meantime, you can continue to improve this submission by editing normally.
{{AfC submission/helptools}}
{{AfC submission/tools
|submit={{{submit|}}}
|author={{{author|}}}
|user={{{user|}}}
}}
}}{{#if:{{{demo|}}}||{{#ifeq:{{ROOTPAGENAME}}|AfC submission|
|{{NOINDEX}}[[Category:Pending AfC submissions|P{{#if:{{{submit|}}}
|{{#expr:trunc({{{submit}}}/100)}}|3
}}]]
[[Category:{{AfC age category|ts={{{submit|}}}|status=pending}}|{{#if:{{{submit|}}}
|{{#expr:trunc({{{submit}}}/100)}}|3
}}]]
{{#ifexist:{{SUBPAGENAME}}|{{#ifeq:{{lc:{{SUBPAGENAME}}}}|sandbox||[[Category:AfC submissions with the same name as existing articles|{{SUBPAGENAME}}]]}}}}
}}<!--
See https://en.wikipedia.org/w/index.php?title=Wikipedia_talk%3AWikiProject_Articles_for_creation&diff=548201182&oldid=548180058
-->{{#ifeq:{{{type|}}}|template|[[Category:Pending template and disambiguation AfC submissions|{{#if:{{{submit|}}}
|{{#expr:trunc({{{submit}}}/100)}}|3
}}]]}}
{{#ifeq:{{{type|}}}|dab|[[Category:Pending template and disambiguation AfC submissions|{{#if:{{{submit|}}}
|{{#expr:trunc({{{submit}}}/100)}}|3
}}]]}}
{{#ifeq:{{NAMESPACE}}|{{ns:2}}|[[Category:Pending AfC submissions in userspace]]}}{{#ifexpr:{{PAGESIZE:{{FULLPAGENAME}}|R}}<450|[[Category:Pending AfC submissions less than 450 bytes long]]|}}{{#ifeq:{{NAMESPACE}}|{{ns:10}}||{{#ifeq:{{NUMBEROFSECTIONS}}|0|[[Category:Pending AfC submissions without a section]]}}}}}}<noinclude>
{{documentation|Template:AfC_submission/doc}}
</noinclude>
cs5ci3b6dopfhasi6vldefsdzvakfd8
Template:AfC submission/tools
10
122530
740072
584336
2026-03-31T05:17:38Z
en>Oshwah
0
Small grammar and punctuation on warning messages.
740072
wikitext
text/x-wiki
----
{{hidden|Reviewer tools|class=afc-reviewer-tools-collapse|style={{#if:{{#switch:{{FULLPAGENAME}}|Wikipedia:Articles for creation/{{SUBPAGENAME}}|Draft:{{SUBPAGENAME}}|Wikipedia talk:Articles for creation/{{SUBPAGENAME}}=|#default=yes}}{{#if:{{{submit|}}}|yes|}}{{#ifexist:{{{SUBPAGENAME|}}}|yes|}}|padding-bottom: 0;|}}|2=
*<span id="afc-reviewer-tools" class="plainlinks">[[Wikipedia:WikiProject Articles for creation/Reviewing instructions|Instructions]]<!--
-->{{·}}[[Special:WhatLinksHere/{{SUBPAGENAME}}|What links here]]<!--
-->{{·}}[[:{{SUBPAGENAME}}|{{SUBPAGENAME}}]]<!--
--> ([[{{TALKPAGENAME:{{SUBPAGENAME}}}}|talk]]:<!--
--> [{{fullurl:{{TALKPAGENAME:{{SUBPAGENAME}}}}|action=edit&editintro=Template:AfC_submission/banner_editintro&preload=Template:AfC_submission/banner_preload}} +]<!--
-->{{·}}[{{fullurl:{{TALKPAGENAME:{{SUBPAGENAME}}}}|action=edit&editintro=Template:AfC_submission/banner_editintro&preload=Template:AfC_submission/banner_preload_(bio)}} bio])<!--
-->{{·}}([{{fullurl:index.php|title=Special:Log&page={{SUBPAGENAMEE}}}} log])<!--
-->{{·}}{{Copyvios||{{FULLPAGENAME}}}}<!--
-->{{·}}[https://refill.toolforge.org/result.php?page={{FULLPAGENAMEE}}&defaults=y reFill]<!--
-->{{·}}[https://citations.toolforge.org/process_page.php?edit=toolbar&slow=1&user=&page={{FULLPAGENAMEE}} Citation Bot]<!--
-->{{·}}(Search: [[google:{{urlencode:{{SUBPAGENAME}}}}|Google]],<!--
--> [{{fullurl:Special:Search|search={{urlencode:{{SUBPAGENAME}}}}&fulltext=Search}} Wikipedia])<!--
-->{{·}}Submitted {{#if:{{{submit|}}}|{{time ago|{{{submit|}}}}}|???}}<!--
-->{{#if:{{{user|}}}| by [[Special:Contributions/{{{user}}}|{{{user}}}]] ([[User talk:{{{user}}}|talk]]:<!--
--> [{{fullurl:User talk:{{{user}}}|action=edit&editintro=Template:AfC_submission/user_talk_editintro_(declined)&preload=Template:AfC_submission/user_talk_preload_(declined)&preloadtitle={{urlencode:Your submission at [[WP:AfC|Articles for creation]]{{#ifeq:{{TALKSPACE}}|{{ns:Project talk}}|: {{SUBPAGENAME}}}}}}§ion=new}} D]<!--
-->{{·}}[{{fullurl:User talk:{{{user}}}|action=edit&editintro=Template:AfC_submission/user_talk_editintro&preload=Template:AfC_submission/user_talk_preload&preloadtitle={{urlencode:Your submission at [[WP:AfC|Articles for creation]]{{#ifeq:{{TALKSPACE}}|{{ns:Project talk}}|: {{SUBPAGENAME}}}}}}§ion=new}} +]<!--
-->)
}}<!--
-->{{#if:{{{author|}}}
| by {{{author}}}
}}<!--
-->{{·}}Last edited {{time ago|{{REVISIONTIMESTAMP}}}}{{#if:{{REVISIONUSER}}| by [[User:{{REVISIONUSER}}|{{REVISIONUSER}}]]}}</span>}}<!--
-->{{#switch:{{FULLPAGENAME}}
|Draft:{{SUBPAGENAME}}
|Draft talk:{{SUBPAGENAME}}
|Wikipedia:Articles for creation/{{SUBPAGENAME}}
|Wikipedia talk:Articles for creation/{{SUBPAGENAME}}=
|#default={{#ifexist:Draft:{{ucfirst:{{SUBPAGENAME}}}}|
*<small>'''Warning:''' This page should probably be moved{{#ifeq:{{ucfirst:{{SUBPAGENAME}}}}|Sandbox| to the Draft namespace|, but a page already exists at [[Draft:{{ucfirst:{{SUBPAGENAME}}}}]]}}.</small><!--
--><inputbox>
type=move
page={{FULLPAGENAME}}
prefix=Draft:
{{#ifeq:{{ucfirst:{{SUBPAGENAME}}}}|Sandbox||default={{ucfirst:{{SUBPAGENAME}}}}}}
placeholder=Name of the article (without the prefix)
summary=Preferred location for [[WP:AFC|AfC]] submissions
buttonlabel=Move to Draft space
break=no
</inputbox>|
*<small>'''Warning:''' This page should probably be located at [[Draft:{{ucfirst:{{SUBPAGENAME}}}}]] (<span class="plainlinks">[{{fullurl:index.php|title=Special:MovePage/{{FULLPAGENAMEE}}&wpNewTitle=Draft:{{SUBPAGENAMEE}}&wpReason={{urlencode:Preferred location for [[WP:AFC|AfC]] submissions}}}} move]</span>).</small>}}}}<!--
-->{{#if:{{{submit|}}}|
|
*<small>'''Warning:''' This submission is not timestamped. Please replace this template with <span style="white-space:nowrap; font-family: monospace"><nowiki>{{subst:submit}}</nowiki></span>.</small>
|<!-- Not Submitted-->}}<!--
-->{{#ifexist:{{SUBPAGENAME}}|{{#ifeq:{{lc:{{SUBPAGENAME}}}}|sandbox||
*<small>'''Warning:''' The page {{No redirect|{{SUBPAGENAME}}}} {{#ifeq:{{#invoke:redirect|isRedirect|{{SUBPAGENAME}}}}|yes|redirects to {{#invoke:redirect|main|{{SUBPAGENAME}}|bracket=yes}}|already exists}}. Please [[Wikipedia:Administrators' guide/Fixing cut-and-paste moves|ensure it is not a copy]] or that this page is located at the correct title.</small>}}|}}<!--
-->{{#if:{{#invoke:redirect|isRedirect|User talk:{{{user|Example}}}}}|
*<small>'''Warning:''' [[User:{{{user|Example}}}|The user]] who submitted this draft may have been renamed. Please [https://en.wikipedia.org/wiki/Special:Log?type=renameuser&page=User:{{#invoke:String|replace|{{{user|Example}}}| |_}} verify this] and adjust the submission template if necessary before reviewing.</small>[[Category:AfC submissions with a potentially renamed submitter]]
}}<noinclude>
{{documentation}}
</noinclude>
2glyu60x42ah89xlw42x5m0il3021y3
740073
740072
2026-05-01T19:28:18Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/tools]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740072
wikitext
text/x-wiki
----
{{hidden|Reviewer tools|class=afc-reviewer-tools-collapse|style={{#if:{{#switch:{{FULLPAGENAME}}|Wikipedia:Articles for creation/{{SUBPAGENAME}}|Draft:{{SUBPAGENAME}}|Wikipedia talk:Articles for creation/{{SUBPAGENAME}}=|#default=yes}}{{#if:{{{submit|}}}|yes|}}{{#ifexist:{{{SUBPAGENAME|}}}|yes|}}|padding-bottom: 0;|}}|2=
*<span id="afc-reviewer-tools" class="plainlinks">[[Wikipedia:WikiProject Articles for creation/Reviewing instructions|Instructions]]<!--
-->{{·}}[[Special:WhatLinksHere/{{SUBPAGENAME}}|What links here]]<!--
-->{{·}}[[:{{SUBPAGENAME}}|{{SUBPAGENAME}}]]<!--
--> ([[{{TALKPAGENAME:{{SUBPAGENAME}}}}|talk]]:<!--
--> [{{fullurl:{{TALKPAGENAME:{{SUBPAGENAME}}}}|action=edit&editintro=Template:AfC_submission/banner_editintro&preload=Template:AfC_submission/banner_preload}} +]<!--
-->{{·}}[{{fullurl:{{TALKPAGENAME:{{SUBPAGENAME}}}}|action=edit&editintro=Template:AfC_submission/banner_editintro&preload=Template:AfC_submission/banner_preload_(bio)}} bio])<!--
-->{{·}}([{{fullurl:index.php|title=Special:Log&page={{SUBPAGENAMEE}}}} log])<!--
-->{{·}}{{Copyvios||{{FULLPAGENAME}}}}<!--
-->{{·}}[https://refill.toolforge.org/result.php?page={{FULLPAGENAMEE}}&defaults=y reFill]<!--
-->{{·}}[https://citations.toolforge.org/process_page.php?edit=toolbar&slow=1&user=&page={{FULLPAGENAMEE}} Citation Bot]<!--
-->{{·}}(Search: [[google:{{urlencode:{{SUBPAGENAME}}}}|Google]],<!--
--> [{{fullurl:Special:Search|search={{urlencode:{{SUBPAGENAME}}}}&fulltext=Search}} Wikipedia])<!--
-->{{·}}Submitted {{#if:{{{submit|}}}|{{time ago|{{{submit|}}}}}|???}}<!--
-->{{#if:{{{user|}}}| by [[Special:Contributions/{{{user}}}|{{{user}}}]] ([[User talk:{{{user}}}|talk]]:<!--
--> [{{fullurl:User talk:{{{user}}}|action=edit&editintro=Template:AfC_submission/user_talk_editintro_(declined)&preload=Template:AfC_submission/user_talk_preload_(declined)&preloadtitle={{urlencode:Your submission at [[WP:AfC|Articles for creation]]{{#ifeq:{{TALKSPACE}}|{{ns:Project talk}}|: {{SUBPAGENAME}}}}}}§ion=new}} D]<!--
-->{{·}}[{{fullurl:User talk:{{{user}}}|action=edit&editintro=Template:AfC_submission/user_talk_editintro&preload=Template:AfC_submission/user_talk_preload&preloadtitle={{urlencode:Your submission at [[WP:AfC|Articles for creation]]{{#ifeq:{{TALKSPACE}}|{{ns:Project talk}}|: {{SUBPAGENAME}}}}}}§ion=new}} +]<!--
-->)
}}<!--
-->{{#if:{{{author|}}}
| by {{{author}}}
}}<!--
-->{{·}}Last edited {{time ago|{{REVISIONTIMESTAMP}}}}{{#if:{{REVISIONUSER}}| by [[User:{{REVISIONUSER}}|{{REVISIONUSER}}]]}}</span>}}<!--
-->{{#switch:{{FULLPAGENAME}}
|Draft:{{SUBPAGENAME}}
|Draft talk:{{SUBPAGENAME}}
|Wikipedia:Articles for creation/{{SUBPAGENAME}}
|Wikipedia talk:Articles for creation/{{SUBPAGENAME}}=
|#default={{#ifexist:Draft:{{ucfirst:{{SUBPAGENAME}}}}|
*<small>'''Warning:''' This page should probably be moved{{#ifeq:{{ucfirst:{{SUBPAGENAME}}}}|Sandbox| to the Draft namespace|, but a page already exists at [[Draft:{{ucfirst:{{SUBPAGENAME}}}}]]}}.</small><!--
--><inputbox>
type=move
page={{FULLPAGENAME}}
prefix=Draft:
{{#ifeq:{{ucfirst:{{SUBPAGENAME}}}}|Sandbox||default={{ucfirst:{{SUBPAGENAME}}}}}}
placeholder=Name of the article (without the prefix)
summary=Preferred location for [[WP:AFC|AfC]] submissions
buttonlabel=Move to Draft space
break=no
</inputbox>|
*<small>'''Warning:''' This page should probably be located at [[Draft:{{ucfirst:{{SUBPAGENAME}}}}]] (<span class="plainlinks">[{{fullurl:index.php|title=Special:MovePage/{{FULLPAGENAMEE}}&wpNewTitle=Draft:{{SUBPAGENAMEE}}&wpReason={{urlencode:Preferred location for [[WP:AFC|AfC]] submissions}}}} move]</span>).</small>}}}}<!--
-->{{#if:{{{submit|}}}|
|
*<small>'''Warning:''' This submission is not timestamped. Please replace this template with <span style="white-space:nowrap; font-family: monospace"><nowiki>{{subst:submit}}</nowiki></span>.</small>
|<!-- Not Submitted-->}}<!--
-->{{#ifexist:{{SUBPAGENAME}}|{{#ifeq:{{lc:{{SUBPAGENAME}}}}|sandbox||
*<small>'''Warning:''' The page {{No redirect|{{SUBPAGENAME}}}} {{#ifeq:{{#invoke:redirect|isRedirect|{{SUBPAGENAME}}}}|yes|redirects to {{#invoke:redirect|main|{{SUBPAGENAME}}|bracket=yes}}|already exists}}. Please [[Wikipedia:Administrators' guide/Fixing cut-and-paste moves|ensure it is not a copy]] or that this page is located at the correct title.</small>}}|}}<!--
-->{{#if:{{#invoke:redirect|isRedirect|User talk:{{{user|Example}}}}}|
*<small>'''Warning:''' [[User:{{{user|Example}}}|The user]] who submitted this draft may have been renamed. Please [https://en.wikipedia.org/wiki/Special:Log?type=renameuser&page=User:{{#invoke:String|replace|{{{user|Example}}}| |_}} verify this] and adjust the submission template if necessary before reviewing.</small>[[Category:AfC submissions with a potentially renamed submitter]]
}}<noinclude>
{{documentation}}
</noinclude>
2glyu60x42ah89xlw42x5m0il3021y3
Module:Transclusion count/data/A
828
122533
740063
732755
2026-04-26T05:09:46Z
en>Ahechtbot
0
[[Wikipedia:BOT|Bot]]: Updated page.
740063
Scribunto
text/plain
return {
["A-Class"] = 5400,
["ACArt"] = 3800,
["AFB_game_box_end"] = 2500,
["AFB_game_box_start"] = 2500,
["AFB_game_box_start/styles.css"] = 2500,
["AFC_comment"] = 23000,
["AFC_submission"] = 32000,
["AFC_submission_category_header"] = 4600,
["AFD_help"] = 165000,
["AFD_help/styles.css"] = 165000,
["AFI/Picture_box/show_picture"] = 4100,
["AFI_film"] = 9900,
["AFL"] = 2100,
["AFL_Car"] = 2600,
["AFL_Col"] = 2500,
["AFL_Ess"] = 2600,
["AFL_Gee"] = 2600,
["AFL_Haw"] = 2400,
["AFL_Mel"] = 2700,
["AFL_NM"] = 2200,
["AFL_Ric"] = 2500,
["AFL_StK"] = 2700,
["AFL_Tables"] = 12000,
["AFL_Year"] = 2800,
["AFL_player"] = 2100,
["AI-generated"] = 7200,
["ALG"] = 2600,
["AMARB"] = 4400,
["AM_station_data"] = 4400,
["ARBPIA"] = 2900,
["ARE"] = 2200,
["ARG"] = 6900,
["ARM"] = 2100,
["ASIN"] = 5000,
["ASN"] = 3400,
["ATP"] = 5200,
["AUS"] = 15000,
["AUT"] = 10000,
["AZE"] = 2800,
["A_note"] = 5700,
["A_or_an"] = 12000,
["Aan"] = 61000,
["Abbr"] = 963000,
["Abbreviation"] = 2900,
["Abbrlink"] = 16000,
["Abot"] = 29000,
["About"] = 169000,
["Absolute_page_title"] = 4500,
["Acad"] = 6700,
["Access_icon"] = 3300,
["According_to_whom"] = 4700,
["AchievementTable"] = 11000,
["AdSenseSummary"] = 12000,
["Added"] = 2800,
["Adjacent_communities"] = 29000,
["Adjacent_stations"] = 42000,
["Adjacent_stations/styles.css"] = 42000,
["Adjacent_stations_doc"] = 2400,
["Adjstn"] = 2700,
["Admin"] = 15000,
["Admin_help/helped"] = 2200,
["Administrator_note"] = 6800,
["Administrators'_noticeboard_archives_all"] = 2100,
["Administrators'_noticeboard_navbox_all"] = 2100,
["Adminnote"] = 3600,
["Advert"] = 9500,
["Aet"] = 6900,
["AfC_accept/C_percentage"] = 6200,
["AfC_age_category"] = 4400,
["AfC_comment"] = 23000,
["AfC_date_category"] = 263000,
["AfC_status/age"] = 4400,
["AfC_status/backlog"] = 5000,
["AfC_submission"] = 44000,
["AfC_submission/comments"] = 30000,
["AfC_submission/declined"] = 30000,
["AfC_submission/draft"] = 13000,
["AfC_submission/helptools"] = 43000,
["AfC_submission/pending"] = 4400,
["AfC_submission/styles.css"] = 45000,
["AfC_submission/tools"] = 4400,
["AfC_submission_category_header"] = 6700,
["AfC_submission_category_header/day"] = 6400,
["AfC_submission_category_header/td"] = 6400,
["AfC_talk/C_percentage"] = 6200,
["AfC_topic"] = 30000,
["AfD_categories_horizontal_shortnames"] = 5300,
["AfD_count_link"] = 4100,
["Afd-merged-from"] = 10000,
["Afd_bottom/old"] = 412000,
["Afd_top/old"] = 412000,
["Afd_top/old/styles.css"] = 412000,
["Africa_topic"] = 6600,
["After_extra_time"] = 6900,
["Age"] = 29000,
["Age_in_days"] = 5600,
["Age_in_years"] = 4100,
["Age_in_years,_months,_weeks_and_days"] = 5300,
["Age_in_years,_months_and_days"] = 20000,
["Age_in_years_and_days"] = 5000,
["Age_in_years_and_days_nts"] = 4600,
["Agree"] = 2500,
["Ahnentafel"] = 7800,
["Ahnentafel/styles.css"] = 7800,
["Air_Force_Historical_Research_Agency"] = 4400,
["Air_force"] = 7400,
["Air_force/core"] = 7400,
["Aircontent"] = 9500,
["Aircraft_specs"] = 12000,
["Aircraft_specs/convert"] = 12000,
["Aircraft_specs/eng"] = 12000,
["Aircraft_specs/length"] = 12000,
["Aircraft_specs/range"] = 12000,
["Aircraft_specs/speed"] = 12000,
["Airport-dest-list"] = 3400,
["Airport_codes"] = 13000,
["Airport_destination_list"] = 4800,
["Al"] = 80000,
["Album"] = 2900,
["Album_chart"] = 36000,
["Album_cover_fur"] = 53000,
["Album_label_category"] = 2300,
["Album_label_category/core"] = 2300,
["Album_ratings"] = 30000,
["Album_reviews"] = 2400,
["Albums_category"] = 25000,
["Albums_category/core"] = 25000,
["Albums_category/type/default"] = 25000,
["Align"] = 204000,
["Aligned_table"] = 14000,
["AllIrelandByCountyCatNav"] = 3200,
["AllMusic"] = 77000,
["All_Ireland_by_county_category_navigation"] = 3200,
["All_plot"] = 2100,
["Allcaps"] = 11000,
["Allcaps/styles.css"] = 11000,
["Allmusic"] = 11000,
["Allow_wrap"] = 6500,
["Already_done"] = 2800,
["Also"] = 2400,
["Also_known_as"] = 2700,
["Alternating_rows_table"] = 2400,
["Alternating_rows_table/styles.css"] = 2400,
["Alumni"] = 3000,
["Always_substitute"] = 9100,
["Ambox"] = 1480000,
["Ambox_globe"] = 43000,
["Ambox_globe_current_red"] = 41000,
["American_English"] = 23000,
["American_football_roster/Footer"] = 3800,
["American_football_roster/Header"] = 3800,
["American_football_roster/Player"] = 3800,
["Americanfootballbox"] = 4500,
["Anarchism_announcements"] = 4000,
["Anarchism_announcements/shell"] = 4000,
["Anchor"] = 94000,
["Angbr"] = 2700,
["Angbr_IPA"] = 2400,
["Angle_bracket"] = 3900,
["Anglican_navbox_titlestyle"] = 14000,
["Anglicise_rank"] = 544000,
["Animal_tasks"] = 34000,
["Anime_News_Network"] = 12000,
["Anl"] = 2200,
["Annotated_link"] = 16000,
["Anonblock"] = 35000,
["Antonym_of_(dis)establish"] = 16000,
["Apostrophe"] = 97000,
["Arbitration_Committee_candidate/data"] = 108000,
["Archive"] = 340000,
["Archive_bottom"] = 67000,
["Archive_box"] = 18000,
["Archive_list"] = 95000,
["Archive_top"] = 47000,
["Archive_top/styles.css"] = 47000,
["Archive_top_green"] = 14000,
["Archive_top_green/styles.css"] = 14000,
["Archive_top_red"] = 6500,
["Archive_top_red/styles.css"] = 6500,
["Archivebottom"] = 3700,
["Archivebox"] = 2200,
["Archives"] = 55000,
["Archivetop"] = 3700,
["Army"] = 18000,
["Army/core"] = 18000,
["Art_UK_bio"] = 2500,
["Art_UK_bio/plural"] = 2500,
["Article"] = 3000,
["ArticleHistory"] = 27000,
["Article_alerts_box"] = 4100,
["Article_alerts_box/styles.css"] = 4100,
["Article_for_improvement_banner/Picture_box"] = 4500,
["Article_for_improvement_banner/Picture_box/show_picture"] = 4100,
["Article_history"] = 52000,
["Article_links"] = 512000,
["Article_links_2"] = 80000,
["Article_or_page"] = 6600,
["Article_stub_box"] = 2370000,
["Articles_by_Importance"] = 12000,
["Articles_by_Quality"] = 38000,
["Articles_for_creation_links"] = 6800,
["As_of"] = 91000,
["As_written"] = 2500,
["Asbox"] = 2350000,
["Asia_topic"] = 11000,
["Asof"] = 6200,
["Assessed-Class"] = 18000,
["Assignment"] = 6000,
["Assignment_milestones"] = 5300,
["Association_of_Tennis_Professionals"] = 5200,
["AstDys"] = 2800,
["Asterisk"] = 3400,
["Astro_list_redirect_comment"] = 2000,
["AthAbbr"] = 7300,
["Atnhead"] = 6000,
["Atop"] = 7200,
["Atopg"] = 13000,
["Atopr"] = 6100,
["Attached_KML"] = 14000,
["Au"] = 5900,
["AuEduNewbie"] = 2600,
["Audio"] = 41000,
["Audio_sample"] = 3100,
["AustralianFootball"] = 8900,
["Australian_Dictionary_of_Biography"] = 2300,
["Australian_English"] = 3700,
["Australian_Football_League_team"] = 2100,
["Australian_dollar"] = 2200,
["Australian_party_style"] = 6700,
["Australian_politics/name"] = 4700,
["Australian_politics/party_colours"] = 6800,
["Austria_metadata_Wikidata"] = 2100,
["Austria_population_Wikidata"] = 2100,
["Aut"] = 7800,
["Authority_control"] = 2280000,
["Authority_control_(arts)"] = 16000,
["Auto_link"] = 73000,
["Autobiography"] = 2100,
["Autolink"] = 41000,
["Automated_tools"] = 89000,
["Automated_tools/core"] = 89000,
["Automatic_archive_navigator"] = 42000,
["Automatic_archives_blurb"] = 19000,
["Automatic_category_TOC"] = 856000,
["Automatic_category_TOC/core"] = 855000,
["Automatic_taxobox"] = 104000,
["Automatically_generated"] = 2000,
["Aviation_Safety_Network_accident_history"] = 3400,
["Aviation_accidents_and_incidents"] = 2700,
["Avoid_wrap"] = 6000,
["Awaiting_admin"] = 2800,
["Awaitingadmin"] = 2700,
["Award2"] = 2600,
["Awards"] = 2600,
["Awards_table"] = 6200,
["Awards_table/styles.css"] = 6200,
["Ayd"] = 4400,
["Aye"] = 14000,
["Module:A_or_an"] = 12000,
["Module:A_or_an/words"] = 12000,
["Module:About"] = 169000,
["Module:Adjacent_stations"] = 80000,
["Module:Adjacent_stations/Amtrak"] = 2900,
["Module:Adjacent_stations/Deutsche_Bahn"] = 2100,
["Module:Adjacent_stations/Indian_Railways"] = 3500,
["Module:Adjacent_stations/JR_East"] = 2600,
["Module:Adjacent_stations/i18n"] = 80000,
["Module:Administrators'_noticeboard_archives"] = 2100,
["Module:Administrators'_noticeboard_archives/styles.css"] = 2100,
["Module:AfC_submission_catcheck"] = 352000,
["Module:AfC_topic"] = 30000,
["Module:Age"] = 1400000,
["Module:Ahnentafel"] = 7800,
["Module:Airport_destination_list"] = 4800,
["Module:Aligned_dates_list"] = 2700,
["Module:Aligned_table"] = 14000,
["Module:All_Ireland_by_county_category_navigation"] = 3400,
["Module:Anchor"] = 94000,
["Module:Ancient_Egypt_era"] = 2700,
["Module:Ancient_Egypt_era/data"] = 2700,
["Module:Ancient_Egypt_kings"] = 2700,
["Module:Ancient_Egypt_kings/data"] = 2700,
["Module:Ancient_Olympiads"] = 2700,
["Module:Ancient_Olympiads/data"] = 2700,
["Module:Annotated_link"] = 16000,
["Module:Archive"] = 340000,
["Module:Archive/config"] = 340000,
["Module:Archive_list"] = 98000,
["Module:Archives/bots"] = 34000,
["Module:Arguments"] = 37100000,
["Module:Armenian"] = 2700,
["Module:Arrowverse_redirect_category_handler"] = 2100,
["Module:Article_history"] = 52000,
["Module:Article_history/Category"] = 52000,
["Module:Article_history/config"] = 52000,
["Module:Article_history/styles.css"] = 52000,
["Module:Article_stub_box"] = 2380000,
["Module:Article_stub_box/styles.css"] = 2380000,
["Module:Articles_by_class"] = 50000,
["Module:Asbox_stubtree"] = 42000,
["Module:Attached_KML"] = 14000,
["Module:Attached_KML/styles.css"] = 14000,
["Module:Australian_place_map"] = 16000,
["Module:Authority_control"] = 2300000,
["Module:Authority_control/auxiliary"] = 711000,
["Module:Authority_control/config"] = 2300000,
["Module:Auto_date_formatter"] = 15000,
["Module:Automated_taxobox"] = 494000,
["Module:Autotaxobox"] = 645000,
}
6jvk6g9qarl0ua4a808kj3t50mitceb
740064
740063
2026-05-01T19:16:17Z
Novem Linguae
49714
1 revision imported from [[:en:Module:Transclusion_count/data/A]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740063
Scribunto
text/plain
return {
["A-Class"] = 5400,
["ACArt"] = 3800,
["AFB_game_box_end"] = 2500,
["AFB_game_box_start"] = 2500,
["AFB_game_box_start/styles.css"] = 2500,
["AFC_comment"] = 23000,
["AFC_submission"] = 32000,
["AFC_submission_category_header"] = 4600,
["AFD_help"] = 165000,
["AFD_help/styles.css"] = 165000,
["AFI/Picture_box/show_picture"] = 4100,
["AFI_film"] = 9900,
["AFL"] = 2100,
["AFL_Car"] = 2600,
["AFL_Col"] = 2500,
["AFL_Ess"] = 2600,
["AFL_Gee"] = 2600,
["AFL_Haw"] = 2400,
["AFL_Mel"] = 2700,
["AFL_NM"] = 2200,
["AFL_Ric"] = 2500,
["AFL_StK"] = 2700,
["AFL_Tables"] = 12000,
["AFL_Year"] = 2800,
["AFL_player"] = 2100,
["AI-generated"] = 7200,
["ALG"] = 2600,
["AMARB"] = 4400,
["AM_station_data"] = 4400,
["ARBPIA"] = 2900,
["ARE"] = 2200,
["ARG"] = 6900,
["ARM"] = 2100,
["ASIN"] = 5000,
["ASN"] = 3400,
["ATP"] = 5200,
["AUS"] = 15000,
["AUT"] = 10000,
["AZE"] = 2800,
["A_note"] = 5700,
["A_or_an"] = 12000,
["Aan"] = 61000,
["Abbr"] = 963000,
["Abbreviation"] = 2900,
["Abbrlink"] = 16000,
["Abot"] = 29000,
["About"] = 169000,
["Absolute_page_title"] = 4500,
["Acad"] = 6700,
["Access_icon"] = 3300,
["According_to_whom"] = 4700,
["AchievementTable"] = 11000,
["AdSenseSummary"] = 12000,
["Added"] = 2800,
["Adjacent_communities"] = 29000,
["Adjacent_stations"] = 42000,
["Adjacent_stations/styles.css"] = 42000,
["Adjacent_stations_doc"] = 2400,
["Adjstn"] = 2700,
["Admin"] = 15000,
["Admin_help/helped"] = 2200,
["Administrator_note"] = 6800,
["Administrators'_noticeboard_archives_all"] = 2100,
["Administrators'_noticeboard_navbox_all"] = 2100,
["Adminnote"] = 3600,
["Advert"] = 9500,
["Aet"] = 6900,
["AfC_accept/C_percentage"] = 6200,
["AfC_age_category"] = 4400,
["AfC_comment"] = 23000,
["AfC_date_category"] = 263000,
["AfC_status/age"] = 4400,
["AfC_status/backlog"] = 5000,
["AfC_submission"] = 44000,
["AfC_submission/comments"] = 30000,
["AfC_submission/declined"] = 30000,
["AfC_submission/draft"] = 13000,
["AfC_submission/helptools"] = 43000,
["AfC_submission/pending"] = 4400,
["AfC_submission/styles.css"] = 45000,
["AfC_submission/tools"] = 4400,
["AfC_submission_category_header"] = 6700,
["AfC_submission_category_header/day"] = 6400,
["AfC_submission_category_header/td"] = 6400,
["AfC_talk/C_percentage"] = 6200,
["AfC_topic"] = 30000,
["AfD_categories_horizontal_shortnames"] = 5300,
["AfD_count_link"] = 4100,
["Afd-merged-from"] = 10000,
["Afd_bottom/old"] = 412000,
["Afd_top/old"] = 412000,
["Afd_top/old/styles.css"] = 412000,
["Africa_topic"] = 6600,
["After_extra_time"] = 6900,
["Age"] = 29000,
["Age_in_days"] = 5600,
["Age_in_years"] = 4100,
["Age_in_years,_months,_weeks_and_days"] = 5300,
["Age_in_years,_months_and_days"] = 20000,
["Age_in_years_and_days"] = 5000,
["Age_in_years_and_days_nts"] = 4600,
["Agree"] = 2500,
["Ahnentafel"] = 7800,
["Ahnentafel/styles.css"] = 7800,
["Air_Force_Historical_Research_Agency"] = 4400,
["Air_force"] = 7400,
["Air_force/core"] = 7400,
["Aircontent"] = 9500,
["Aircraft_specs"] = 12000,
["Aircraft_specs/convert"] = 12000,
["Aircraft_specs/eng"] = 12000,
["Aircraft_specs/length"] = 12000,
["Aircraft_specs/range"] = 12000,
["Aircraft_specs/speed"] = 12000,
["Airport-dest-list"] = 3400,
["Airport_codes"] = 13000,
["Airport_destination_list"] = 4800,
["Al"] = 80000,
["Album"] = 2900,
["Album_chart"] = 36000,
["Album_cover_fur"] = 53000,
["Album_label_category"] = 2300,
["Album_label_category/core"] = 2300,
["Album_ratings"] = 30000,
["Album_reviews"] = 2400,
["Albums_category"] = 25000,
["Albums_category/core"] = 25000,
["Albums_category/type/default"] = 25000,
["Align"] = 204000,
["Aligned_table"] = 14000,
["AllIrelandByCountyCatNav"] = 3200,
["AllMusic"] = 77000,
["All_Ireland_by_county_category_navigation"] = 3200,
["All_plot"] = 2100,
["Allcaps"] = 11000,
["Allcaps/styles.css"] = 11000,
["Allmusic"] = 11000,
["Allow_wrap"] = 6500,
["Already_done"] = 2800,
["Also"] = 2400,
["Also_known_as"] = 2700,
["Alternating_rows_table"] = 2400,
["Alternating_rows_table/styles.css"] = 2400,
["Alumni"] = 3000,
["Always_substitute"] = 9100,
["Ambox"] = 1480000,
["Ambox_globe"] = 43000,
["Ambox_globe_current_red"] = 41000,
["American_English"] = 23000,
["American_football_roster/Footer"] = 3800,
["American_football_roster/Header"] = 3800,
["American_football_roster/Player"] = 3800,
["Americanfootballbox"] = 4500,
["Anarchism_announcements"] = 4000,
["Anarchism_announcements/shell"] = 4000,
["Anchor"] = 94000,
["Angbr"] = 2700,
["Angbr_IPA"] = 2400,
["Angle_bracket"] = 3900,
["Anglican_navbox_titlestyle"] = 14000,
["Anglicise_rank"] = 544000,
["Animal_tasks"] = 34000,
["Anime_News_Network"] = 12000,
["Anl"] = 2200,
["Annotated_link"] = 16000,
["Anonblock"] = 35000,
["Antonym_of_(dis)establish"] = 16000,
["Apostrophe"] = 97000,
["Arbitration_Committee_candidate/data"] = 108000,
["Archive"] = 340000,
["Archive_bottom"] = 67000,
["Archive_box"] = 18000,
["Archive_list"] = 95000,
["Archive_top"] = 47000,
["Archive_top/styles.css"] = 47000,
["Archive_top_green"] = 14000,
["Archive_top_green/styles.css"] = 14000,
["Archive_top_red"] = 6500,
["Archive_top_red/styles.css"] = 6500,
["Archivebottom"] = 3700,
["Archivebox"] = 2200,
["Archives"] = 55000,
["Archivetop"] = 3700,
["Army"] = 18000,
["Army/core"] = 18000,
["Art_UK_bio"] = 2500,
["Art_UK_bio/plural"] = 2500,
["Article"] = 3000,
["ArticleHistory"] = 27000,
["Article_alerts_box"] = 4100,
["Article_alerts_box/styles.css"] = 4100,
["Article_for_improvement_banner/Picture_box"] = 4500,
["Article_for_improvement_banner/Picture_box/show_picture"] = 4100,
["Article_history"] = 52000,
["Article_links"] = 512000,
["Article_links_2"] = 80000,
["Article_or_page"] = 6600,
["Article_stub_box"] = 2370000,
["Articles_by_Importance"] = 12000,
["Articles_by_Quality"] = 38000,
["Articles_for_creation_links"] = 6800,
["As_of"] = 91000,
["As_written"] = 2500,
["Asbox"] = 2350000,
["Asia_topic"] = 11000,
["Asof"] = 6200,
["Assessed-Class"] = 18000,
["Assignment"] = 6000,
["Assignment_milestones"] = 5300,
["Association_of_Tennis_Professionals"] = 5200,
["AstDys"] = 2800,
["Asterisk"] = 3400,
["Astro_list_redirect_comment"] = 2000,
["AthAbbr"] = 7300,
["Atnhead"] = 6000,
["Atop"] = 7200,
["Atopg"] = 13000,
["Atopr"] = 6100,
["Attached_KML"] = 14000,
["Au"] = 5900,
["AuEduNewbie"] = 2600,
["Audio"] = 41000,
["Audio_sample"] = 3100,
["AustralianFootball"] = 8900,
["Australian_Dictionary_of_Biography"] = 2300,
["Australian_English"] = 3700,
["Australian_Football_League_team"] = 2100,
["Australian_dollar"] = 2200,
["Australian_party_style"] = 6700,
["Australian_politics/name"] = 4700,
["Australian_politics/party_colours"] = 6800,
["Austria_metadata_Wikidata"] = 2100,
["Austria_population_Wikidata"] = 2100,
["Aut"] = 7800,
["Authority_control"] = 2280000,
["Authority_control_(arts)"] = 16000,
["Auto_link"] = 73000,
["Autobiography"] = 2100,
["Autolink"] = 41000,
["Automated_tools"] = 89000,
["Automated_tools/core"] = 89000,
["Automatic_archive_navigator"] = 42000,
["Automatic_archives_blurb"] = 19000,
["Automatic_category_TOC"] = 856000,
["Automatic_category_TOC/core"] = 855000,
["Automatic_taxobox"] = 104000,
["Automatically_generated"] = 2000,
["Aviation_Safety_Network_accident_history"] = 3400,
["Aviation_accidents_and_incidents"] = 2700,
["Avoid_wrap"] = 6000,
["Awaiting_admin"] = 2800,
["Awaitingadmin"] = 2700,
["Award2"] = 2600,
["Awards"] = 2600,
["Awards_table"] = 6200,
["Awards_table/styles.css"] = 6200,
["Ayd"] = 4400,
["Aye"] = 14000,
["Module:A_or_an"] = 12000,
["Module:A_or_an/words"] = 12000,
["Module:About"] = 169000,
["Module:Adjacent_stations"] = 80000,
["Module:Adjacent_stations/Amtrak"] = 2900,
["Module:Adjacent_stations/Deutsche_Bahn"] = 2100,
["Module:Adjacent_stations/Indian_Railways"] = 3500,
["Module:Adjacent_stations/JR_East"] = 2600,
["Module:Adjacent_stations/i18n"] = 80000,
["Module:Administrators'_noticeboard_archives"] = 2100,
["Module:Administrators'_noticeboard_archives/styles.css"] = 2100,
["Module:AfC_submission_catcheck"] = 352000,
["Module:AfC_topic"] = 30000,
["Module:Age"] = 1400000,
["Module:Ahnentafel"] = 7800,
["Module:Airport_destination_list"] = 4800,
["Module:Aligned_dates_list"] = 2700,
["Module:Aligned_table"] = 14000,
["Module:All_Ireland_by_county_category_navigation"] = 3400,
["Module:Anchor"] = 94000,
["Module:Ancient_Egypt_era"] = 2700,
["Module:Ancient_Egypt_era/data"] = 2700,
["Module:Ancient_Egypt_kings"] = 2700,
["Module:Ancient_Egypt_kings/data"] = 2700,
["Module:Ancient_Olympiads"] = 2700,
["Module:Ancient_Olympiads/data"] = 2700,
["Module:Annotated_link"] = 16000,
["Module:Archive"] = 340000,
["Module:Archive/config"] = 340000,
["Module:Archive_list"] = 98000,
["Module:Archives/bots"] = 34000,
["Module:Arguments"] = 37100000,
["Module:Armenian"] = 2700,
["Module:Arrowverse_redirect_category_handler"] = 2100,
["Module:Article_history"] = 52000,
["Module:Article_history/Category"] = 52000,
["Module:Article_history/config"] = 52000,
["Module:Article_history/styles.css"] = 52000,
["Module:Article_stub_box"] = 2380000,
["Module:Article_stub_box/styles.css"] = 2380000,
["Module:Articles_by_class"] = 50000,
["Module:Asbox_stubtree"] = 42000,
["Module:Attached_KML"] = 14000,
["Module:Attached_KML/styles.css"] = 14000,
["Module:Australian_place_map"] = 16000,
["Module:Authority_control"] = 2300000,
["Module:Authority_control/auxiliary"] = 711000,
["Module:Authority_control/config"] = 2300000,
["Module:Auto_date_formatter"] = 15000,
["Module:Automated_taxobox"] = 494000,
["Module:Autotaxobox"] = 645000,
}
6jvk6g9qarl0ua4a808kj3t50mitceb
Template:AfC submission/declined
10
122547
740096
584296
2026-03-31T07:55:23Z
en>WOSlinker
0
fix lint issues
740096
wikitext
text/x-wiki
{{Ombox
| templatestyles = AfC submission/styles.css
| class = afc-submission-declined
| type = notice
| image = none
| imageright = [[File:AFC-Logo_Decline.svg|center|75px]]
| text = <div style="text-align: center;"><span class="afc-submission-header">Submission declined{{#if: {{{decliner|}}}{{{declinets|}}}
|  {{#if: {{{declinets|}}}
| on {{#time: j F Y|{{{declinets}}}}}{{#if: {{{decliner|}}}
|  
}}
}}{{#if: {{{decliner|}}}
|by {{Noping|{{{decliner}}}}} ([[User talk:{{{decliner}}}|talk]])
}}
}}.</span></div>{{#if:{{NAMESPACE}}{{#ifeq:{{REVISIONID}}|{{REVISIONID:{{FULLPAGENAME}}}}||0}} |{{#if: {{{reason|}}}
| <div class="afc-submission-declined-comment">{{AfC submission/comments|cat={{{cat|{{#if:{{{demo|}}}|no}}}}}|{{{reason}}}|{{{details|}}}}}</div>|<br />
}}{{#if: {{{reason2|}}}
| <div class="afc-submission-declined-comment">{{AfC submission/comments|cat={{{cat|{{#if:{{{demo|}}}|no}}}}}|{{{reason2}}}|{{{details2|}}}}}</div>|<!--empty-->
}}}}{{#ifeq: {{lc: {{{small|}}} }} | yes|
| {{blist
|1=If you would like to continue working on the submission, click on the "Edit" tab at the top of the window.
|2=If you have not resolved the issues listed above, your draft will be declined again and potentially deleted.
|3=If you need extra help, please {{Plain link|url={{fullurl:Wikipedia:WikiProject_Articles_for_creation/Help_desk/New question|withJS=MediaWiki:AFCHD-wizard.js&page={{FULLPAGENAMEE}}}}|name='''ask us a question'''}} at the AfC Help Desk or get '''[[Wikipedia:IRC help disclaimer|live help]]''' from experienced editors.
|4=Please do not remove reviewer comments or this notice until the submission is accepted. }}
{{#if:{{NAMESPACE}}|{{AfC submission/helptools}}|}}<includeonly>{{#ifexpr: {{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +6 months }} < {{#time: U}}|<div class="afc-submission-g13">This draft has not been edited in over six months and qualifies to be deleted per [[WP:G13|CSD G13]].</div>{{#if:{{{demo|}}}||[[Category:G13 eligible AfC submissions|{{REVISIONTIMESTAMP}}]]}}}}
{{#ifexpr: ({{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +5 months}} < {{#time: U}})and({{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +6 months}} > {{#time: U}})|{{#if:{{{demo|}}}||[[Category:AfC G13 eligible soon submissions|{{REVISIONTIMESTAMP}}]]}}}}
</includeonly>
}}<small>{{#if: {{{decliner|}}}
|  Declined by {{Noping|{{{decliner}}}}}{{#if: {{{declinets|}}}
|  {{time ago|{{{declinets}}}}}
}}.
|  The reviewer(s) who declined this submission will be listed in the [{{SERVER}}{{localurl:{{NAMESPACE}}:{{PAGENAME}}|action=history}} page history].
}}</small>{{#ifeq: {{lc: {{{small|}}} }} | yes|| <small> Last edited {{#if: {{REVISIONUSER}}
| by [[User:{{encodefirst|{{REVISIONUSER}}}}|{{encodefirst|{{REVISIONUSER}}}}]]
}} {{time ago|{{{revisionts|{{REVISIONTIMESTAMP}}}}}}}. {{#if: {{{user|}}}
| Reviewer: [{{fullurl: User talk:{{{user}}} | action=edit&editintro=Template:AfC_submission/user_talk_editintro_declined&preload=Template:AfC_submission/user_talk_preload_declined&preloadtitle={{urlencode:Your submission at [[WP:AfC|Articles for creation]]{{#ifeq: {{TALKSPACE}} | {{ns:Project talk}} | : {{SUBPAGENAME}} }} }}§ion=new}} Inform author].
}}</small>{{main other||2=<div style="border-top: 1px #AAA solid; margin-top:5px;">{{#invoke:AfC submission catcheck|submitted|This draft has been resubmitted and is currently awaiting re-review.|{{#if:{{REVISIONID}}<noinclude>0</noinclude>|<table><tr><td>{{Clickable button
| Resubmit
| url = {{fullurl:Wikipedia:Articles_for_creation/Submitting|withJS=MediaWiki:AFC-submit-wizard.js&page={{FULLPAGENAMEE}}}}
| class = mw-ui-progressive
}}</td><td>Please note that if the issues are not fixed, the draft will be declined again.</td></tr></table>|Once you save your changes using the "Publish changes" button below, you will be able to resubmit your draft for review by pressing the "Resubmit" button that will appear here.}}}}</div>}}}}
}}<includeonly>{{NOINDEX}}{{user other|{{#if:{{{demo|}}}||[[Category:Declined AfC submissions in userspace]]}}}}{{#if:{{{demo|}}}||[[Category:Declined AfC submissions]]}}
</includeonly><noinclude>
{{documentation}}
</noinclude>
12qkxdn8okhh88my61xvh4nrp39dyod
740097
740096
2026-05-01T19:32:06Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/declined]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740096
wikitext
text/x-wiki
{{Ombox
| templatestyles = AfC submission/styles.css
| class = afc-submission-declined
| type = notice
| image = none
| imageright = [[File:AFC-Logo_Decline.svg|center|75px]]
| text = <div style="text-align: center;"><span class="afc-submission-header">Submission declined{{#if: {{{decliner|}}}{{{declinets|}}}
|  {{#if: {{{declinets|}}}
| on {{#time: j F Y|{{{declinets}}}}}{{#if: {{{decliner|}}}
|  
}}
}}{{#if: {{{decliner|}}}
|by {{Noping|{{{decliner}}}}} ([[User talk:{{{decliner}}}|talk]])
}}
}}.</span></div>{{#if:{{NAMESPACE}}{{#ifeq:{{REVISIONID}}|{{REVISIONID:{{FULLPAGENAME}}}}||0}} |{{#if: {{{reason|}}}
| <div class="afc-submission-declined-comment">{{AfC submission/comments|cat={{{cat|{{#if:{{{demo|}}}|no}}}}}|{{{reason}}}|{{{details|}}}}}</div>|<br />
}}{{#if: {{{reason2|}}}
| <div class="afc-submission-declined-comment">{{AfC submission/comments|cat={{{cat|{{#if:{{{demo|}}}|no}}}}}|{{{reason2}}}|{{{details2|}}}}}</div>|<!--empty-->
}}}}{{#ifeq: {{lc: {{{small|}}} }} | yes|
| {{blist
|1=If you would like to continue working on the submission, click on the "Edit" tab at the top of the window.
|2=If you have not resolved the issues listed above, your draft will be declined again and potentially deleted.
|3=If you need extra help, please {{Plain link|url={{fullurl:Wikipedia:WikiProject_Articles_for_creation/Help_desk/New question|withJS=MediaWiki:AFCHD-wizard.js&page={{FULLPAGENAMEE}}}}|name='''ask us a question'''}} at the AfC Help Desk or get '''[[Wikipedia:IRC help disclaimer|live help]]''' from experienced editors.
|4=Please do not remove reviewer comments or this notice until the submission is accepted. }}
{{#if:{{NAMESPACE}}|{{AfC submission/helptools}}|}}<includeonly>{{#ifexpr: {{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +6 months }} < {{#time: U}}|<div class="afc-submission-g13">This draft has not been edited in over six months and qualifies to be deleted per [[WP:G13|CSD G13]].</div>{{#if:{{{demo|}}}||[[Category:G13 eligible AfC submissions|{{REVISIONTIMESTAMP}}]]}}}}
{{#ifexpr: ({{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +5 months}} < {{#time: U}})and({{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +6 months}} > {{#time: U}})|{{#if:{{{demo|}}}||[[Category:AfC G13 eligible soon submissions|{{REVISIONTIMESTAMP}}]]}}}}
</includeonly>
}}<small>{{#if: {{{decliner|}}}
|  Declined by {{Noping|{{{decliner}}}}}{{#if: {{{declinets|}}}
|  {{time ago|{{{declinets}}}}}
}}.
|  The reviewer(s) who declined this submission will be listed in the [{{SERVER}}{{localurl:{{NAMESPACE}}:{{PAGENAME}}|action=history}} page history].
}}</small>{{#ifeq: {{lc: {{{small|}}} }} | yes|| <small> Last edited {{#if: {{REVISIONUSER}}
| by [[User:{{encodefirst|{{REVISIONUSER}}}}|{{encodefirst|{{REVISIONUSER}}}}]]
}} {{time ago|{{{revisionts|{{REVISIONTIMESTAMP}}}}}}}. {{#if: {{{user|}}}
| Reviewer: [{{fullurl: User talk:{{{user}}} | action=edit&editintro=Template:AfC_submission/user_talk_editintro_declined&preload=Template:AfC_submission/user_talk_preload_declined&preloadtitle={{urlencode:Your submission at [[WP:AfC|Articles for creation]]{{#ifeq: {{TALKSPACE}} | {{ns:Project talk}} | : {{SUBPAGENAME}} }} }}§ion=new}} Inform author].
}}</small>{{main other||2=<div style="border-top: 1px #AAA solid; margin-top:5px;">{{#invoke:AfC submission catcheck|submitted|This draft has been resubmitted and is currently awaiting re-review.|{{#if:{{REVISIONID}}<noinclude>0</noinclude>|<table><tr><td>{{Clickable button
| Resubmit
| url = {{fullurl:Wikipedia:Articles_for_creation/Submitting|withJS=MediaWiki:AFC-submit-wizard.js&page={{FULLPAGENAMEE}}}}
| class = mw-ui-progressive
}}</td><td>Please note that if the issues are not fixed, the draft will be declined again.</td></tr></table>|Once you save your changes using the "Publish changes" button below, you will be able to resubmit your draft for review by pressing the "Resubmit" button that will appear here.}}}}</div>}}}}
}}<includeonly>{{NOINDEX}}{{user other|{{#if:{{{demo|}}}||[[Category:Declined AfC submissions in userspace]]}}}}{{#if:{{{demo|}}}||[[Category:Declined AfC submissions]]}}
</includeonly><noinclude>
{{documentation}}
</noinclude>
12qkxdn8okhh88my61xvh4nrp39dyod
Template:AfC submission/rejected
10
122550
740094
584346
2026-03-31T07:56:34Z
en>WOSlinker
0
fix lint issues
740094
wikitext
text/x-wiki
{{Ombox
| type = notice
| class = afc-submission-rejected
| templatestyles = AfC submission/styles.css
| image = none
| imageright = [[File:Dialog-STOP.svg|center|60px]]
| text = <div style="text-align: center;"><span class="afc-submission-header">Submission rejected{{#if: {{{decliner|}}}{{{declinets|}}}
|  {{#if: {{{declinets|}}}
| on {{#time: j F Y|{{{declinets}}}}}{{#if: {{{decliner|}}}
|  
}}
}}{{#if: {{{decliner|}}}
|by {{Noping|{{{decliner}}}}} ([[User talk:{{{decliner}}}|talk]])
}}
}}.</span></div>{{#if: {{{reason|}}}
|<p>{{AfC submission/reject reasons|cat={{{cat|}}}|{{{reason}}}|{{{details|}}}}}</p><!--
-->{{#if: {{{reason2|}}}|<p>{{AfC submission/reject reasons|cat={{{cat|}}}|{{{reason2}}}|{{{details2|}}}}}</p>|<!--empty-->}}|<br />|
<includeonly>{{#ifexpr: {{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +6 months}} < {{#time: U}}|<div class="afc-submission-g13">This draft has not been edited in over six months and qualifies to be deleted per [[WP:G13|CSD G13]].</div>{{#if:{{{demo|}}}||[[Category:G13 eligible AfC submissions|{{REVISIONTIMESTAMP}}]][[Category:Candidates for speedy deletion as abandoned drafts or AfC submissions|{{REVISIONTIMESTAMP}}]]}}}}
{{#ifexpr: ({{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +5 months}} < {{#time: U}})and({{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +6 months}} > {{#time: U}})|{{#if:{{{demo|}}}||[[Category:AfC G13 eligible soon submissions|{{REVISIONTIMESTAMP}}]]}}}}
</includeonly>
}}<small>{{#if: {{{decliner|}}}
|  Rejected by {{Noping|{{{decliner}}}}}{{#if: {{{declinets|}}}
|  {{time ago|{{{declinets}}}}}
}}.
|  The reviewer(s) who rejected this submission will be listed in the [{{SERVER}}{{localurl:{{NAMESPACE}}:{{PAGENAME}}|action=history}} page history].
}}</small>{{#ifeq: {{lc: {{{small|}}} }} | yes|| <small>Last edited {{#if: {{REVISIONUSER}}
| by [[User:{{encodefirst|{{REVISIONUSER}}}}|{{encodefirst|{{REVISIONUSER}}}}]]
}} {{time ago|{{{revisionts|{{REVISIONTIMESTAMP}}}}}}}. </small>{{main other||2=<div class="afc-submission-advice">{{Clickable button
| Ask for advice
| url = {{fullurl:Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question|withJS=MediaWiki:AFCHD-wizard.js&page={{FULLPAGENAMEE}}}}
| class = mw-ui-progressive
}}</div>}}}}
}}<includeonly>{{NOINDEX}}{{user other|{{#if:{{{demo|}}}||[[Category:Rejected AfC submissions in userspace]]}}}}{{#if:{{{demo|}}}||[[Category:Rejected AfC submissions]]}}
</includeonly><noinclude>
{{documentation}}
</noinclude>
cata9f97fibpgv1dpxsc5y8xq44mvwc
740095
740094
2026-05-01T19:31:54Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/rejected]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740094
wikitext
text/x-wiki
{{Ombox
| type = notice
| class = afc-submission-rejected
| templatestyles = AfC submission/styles.css
| image = none
| imageright = [[File:Dialog-STOP.svg|center|60px]]
| text = <div style="text-align: center;"><span class="afc-submission-header">Submission rejected{{#if: {{{decliner|}}}{{{declinets|}}}
|  {{#if: {{{declinets|}}}
| on {{#time: j F Y|{{{declinets}}}}}{{#if: {{{decliner|}}}
|  
}}
}}{{#if: {{{decliner|}}}
|by {{Noping|{{{decliner}}}}} ([[User talk:{{{decliner}}}|talk]])
}}
}}.</span></div>{{#if: {{{reason|}}}
|<p>{{AfC submission/reject reasons|cat={{{cat|}}}|{{{reason}}}|{{{details|}}}}}</p><!--
-->{{#if: {{{reason2|}}}|<p>{{AfC submission/reject reasons|cat={{{cat|}}}|{{{reason2}}}|{{{details2|}}}}}</p>|<!--empty-->}}|<br />|
<includeonly>{{#ifexpr: {{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +6 months}} < {{#time: U}}|<div class="afc-submission-g13">This draft has not been edited in over six months and qualifies to be deleted per [[WP:G13|CSD G13]].</div>{{#if:{{{demo|}}}||[[Category:G13 eligible AfC submissions|{{REVISIONTIMESTAMP}}]][[Category:Candidates for speedy deletion as abandoned drafts or AfC submissions|{{REVISIONTIMESTAMP}}]]}}}}
{{#ifexpr: ({{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +5 months}} < {{#time: U}})and({{#time: U | {{{revisionts|{{REVISIONTIMESTAMP}}}}} +6 months}} > {{#time: U}})|{{#if:{{{demo|}}}||[[Category:AfC G13 eligible soon submissions|{{REVISIONTIMESTAMP}}]]}}}}
</includeonly>
}}<small>{{#if: {{{decliner|}}}
|  Rejected by {{Noping|{{{decliner}}}}}{{#if: {{{declinets|}}}
|  {{time ago|{{{declinets}}}}}
}}.
|  The reviewer(s) who rejected this submission will be listed in the [{{SERVER}}{{localurl:{{NAMESPACE}}:{{PAGENAME}}|action=history}} page history].
}}</small>{{#ifeq: {{lc: {{{small|}}} }} | yes|| <small>Last edited {{#if: {{REVISIONUSER}}
| by [[User:{{encodefirst|{{REVISIONUSER}}}}|{{encodefirst|{{REVISIONUSER}}}}]]
}} {{time ago|{{{revisionts|{{REVISIONTIMESTAMP}}}}}}}. </small>{{main other||2=<div class="afc-submission-advice">{{Clickable button
| Ask for advice
| url = {{fullurl:Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question|withJS=MediaWiki:AFCHD-wizard.js&page={{FULLPAGENAMEE}}}}
| class = mw-ui-progressive
}}</div>}}}}
}}<includeonly>{{NOINDEX}}{{user other|{{#if:{{{demo|}}}||[[Category:Rejected AfC submissions in userspace]]}}}}{{#if:{{{demo|}}}||[[Category:Rejected AfC submissions]]}}
</includeonly><noinclude>
{{documentation}}
</noinclude>
cata9f97fibpgv1dpxsc5y8xq44mvwc
Template:AfC submission/comments
10
122555
740059
732749
2026-04-27T04:38:02Z
en>Paine Ellsworth
0
per edit request on talk page - fix URL for cv-c parameter (tested with /comments sandbox and testcases)
740059
wikitext
text/x-wiki
{{safesubst:<noinclude />#switch:{{safesubst:<noinclude />lc:{{{1}}}}}
|resume = This draft reads like a resume or curriculum vitae. Wikipedia is an encyclopedia, not a professional networking website or a place to promote yourself or your services. We also [[Wikipedia:Autobiography|strongly discourage writing about yourself]].
Articles about people must meet Wikipedia's [[Wikipedia:Notability (people)|criteria for inclusion for people]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability_(people)#Basic_criteria|significant coverage]]'': discuss the person in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the person, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as resume-like]]}}}}</includeonly>
|ai
|llm = This draft appears to be generated by a [[large language model]] (such as ChatGPT). You [[Wikipedia:NEWLLM|cannot use LLMs]] to generate article content.
LLM-generated pages with [[Wikipedia:G15|certain obvious signs of being machine generated]] may be deleted without notice.
These tools are prone to specific issues that violate our policies:
* ''[[Hallucination (artificial intelligence)|hallucinations]]'': they often invent false information and cite non-existent references.
* ''[[Wikipedia:Neutral point of view|unencyclopedic tone]]'': they tend to be vague, promotional, or essay-like, rather than neutral and factual.
* ''[[Wikipedia:Copyright violations|copyright issues]]'': they may closely paraphrase existing text, leading to copyright violations.
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.
See [[Wikipedia:Large language models|the advice page on large language models]] for more information. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a large language model output]]}}}}</includeonly>
|neo = This draft's references do not show that the neologism meets Wikipedia's [[Wikipedia:Notability|criteria for inclusion]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the term in detail, not just brief mentions or routine use;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as promoted or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.
[[Wikipedia:Neologism|Wikipedia is not a dictionary]] and we do not accept articles that are simply definitions of neologisms.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as non-notable web content]]}}}}</includeonly>
|web = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (web)|criteria for inclusion for web content]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as non-notable web content]]}}}}</includeonly>
|prof
|academic = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (academics)|criteria for inclusion for academics]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (academics)|specific criteria for academics]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable academic topic]]}}}}</includeonly>
|sport
|athlete = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (sports)|criteria for inclusion for sports]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (sports)|specific criteria for sports]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable athletic topic]]}}}}</includeonly>
|music
|m
|band = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (music)|criteria for inclusion for music]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (music)|specific criteria for music]];''
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.
It is often easier to prove the notability of an ''album'' or ''artist'' than an ''individual song'' or ''band member''. If the subject is not yet notable, consider improving a relevant existing article instead. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable musical topic]]}}}}</includeonly>
|film = This draft's references do not show that the film meets Wikipedia's [[Wikipedia:Notability (films)|criteria for inclusion for films]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (films)|specific criteria for films]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the film in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the film, such as press releases, the studio's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable film]]}}}}</includeonly>
|book = This draft's references do not show that the book meets Wikipedia's [[Wikipedia:Notability (books)|criteria for inclusion for books]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (books)|specific criteria for books]]''
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the book in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the book, such as press releases, the author or publisher's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable book]]}}}}</includeonly>
|event = This draft's references do not show that the event meets Wikipedia's [[Wikipedia:Notability (events)|criteria for inclusion for events]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the event in detail, not just brief mentions or routine announcements;
* show ''[[Wikipedia:Notability (events)#Lasting effects|lasting significance]]'': analysis published after the initial breaking news has passed, showing historical impact;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the event, such as press releases, the event or organizers own website, or sponsored content.
Please add references that meet ''all four'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable event]]}}}}</includeonly>
|org
|inc
|corp = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (organizations and companies)|criteria for inclusion for organizations and companies]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability (organizations and companies)#Significant coverage|significant coverage]]'': discuss the subject in detail, excluding routine coverage like product launches, staff appointments, or financial reports and listings in databases or [[listicle]]s;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable organization]]}}}}</includeonly>
|bio = This draft's references do not show that the person meets Wikipedia's [[Wikipedia:Notability (people)|criteria for inclusion for people]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability_(people)#Basic_criteria|significant coverage]]'': discuss the person in detail, not brief mentions or interviews lacking independent analysis;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the person, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable biography]]}}}}</includeonly>
|nn = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability|criteria for inclusion]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as non-notable]]}}}}</includeonly>
|empty
|blank = This draft appears to be blank. If you have not written your text yet, please add your content before resubmitting. If you did write the text, it may be hidden by a formatting error. Please check the "Edit" tab to ensure your text is not inside a comment tag.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as blank]]}}}}</includeonly>
|notenglish
|lang = {{safesubst:<noinclude />#if: {{{2|}}}|This draft is written in {{{2}}}.|}} This is the English Wikipedia and we can only accept articles written in the [[English language]].
* Please provide a high-quality English translation, avoiding [[Wikipedia:Translation|machine translation tools]] (like Google Translate).
{{safesubst:<noinclude />#if: {{safesubst:<noinclude />Mw lang|fn=is_name|{{{2|}}}}}|If you prefer to write in your own language, you may write it in the [[:{{safesubst:<noinclude />mw lang|fn=code_from_name|{{{2}}}}}:|{{{2}}} Wikipedia]].|If you prefer to write in your own language, look for the Wikipedia project in your language on the [https://www.wikipedia.org/ Wikipedia home page].|}}<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as not in English]]}}}}</includeonly>
|test = This draft appears to be a test edit. Please use the [[Wikipedia:Sandbox|sandbox]] to practice editing. Do not submit this draft for review until you have written an article intended for the encyclopedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a test]]}}}}</includeonly>
|redirect = This is not the correct place to request new redirects. Please follow the instructions at [[Wikipedia:Article wizard/Redirects]]. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a redirect]]}}}}</includeonly>
|van = This draft has been declined because it meets one of these criteria:
* ''[[Wikipedia:Attack page|attack page]]'': it is created primarily to disparage, threaten, or harass a subject;
* ''[[Wikipedia:Biographies of living persons|negative biography]]'': it contains unsourced, negative material about a living person;
* ''[[Wikipedia:Vandalism|vandalism]]'': it contains jokes, hoaxes, nonsense, or gibberish.
Drafts that meet [[Wikipedia:Deletion policy|these criteria may be deleted without notice]].
Users who continue to create inappropriate pages may be [[Wikipedia:Blocking policy|blocked from editing Wikipedia]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as being vandalism or attack pages]]}}}}</includeonly>
|cite
|footnote
|ilc = This draft lacks inline citations. Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] requires that all content be supported by [[Wikipedia:Reliable sources|reliable sources]]. We require inline citations (footnotes) to show which source supports which specific statement.
You must place an inline citation directly after:
* quotations;
* any material whose verifiability is likely to be challenged or is contentious;
* any material about living people.
Please edit your draft to support your statements with inline citations. Learn how to create inline citations in the:
* ''Source Editor'': [[Help:Introduction to referencing with Wiki Markup/1|Introduction to referencing with Wiki Markup]].
* ''VisualEditor'': [[Help:Introduction to referencing with VisualEditor/1|Introduction to referencing with VisualEditor]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as needing footnotes]]}}}}</includeonly>
|blp = The text of this submission has been removed. Wikipedia has a strict [[Wikipedia:Biographies of living persons|policy regarding biographies of living people]]. To protect subjects from harm, we remove content that is:
* ''[[Wikipedia:Unsourced|unverified]]'': lacking citations from reliable sources and
* ''[[Wikipedia:Libel|potentially damaging]]'': containing negative, sensitive, or libelous information that cannot be verified.
Your text is preserved in the [[Help:Page history|page history]] tab. You may retrieve it only if you immediately add inline citations to [[Wikipedia:Reliable sources|reliable sources]] that support the material.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as BLP violations]]}}}}</includeonly>
|cv-n
|cv = This draft appears to contain copyrighted material{{safesubst:<noinclude />#ifeq: {{{2|}}}|||{{spaces}}from {{{2}}},}} which has been removed. Wikipedia strictly prohibits [[Wikipedia:Copyright violations|copyright violations]]. Assume all text is copyrighted unless released under a compatible license.
* We can only accept text if it is [[Wikipedia:Copying text from other sources|explicitly released under a compatible free license]] or is in the [[public domain]].
* If the text is not freely licensed, you must rewrite or summarize it entirely in your own words. [[Wikipedia:Close paraphrasing|Changing a few words]] is still considered a copyright violation.
* If you own the text you cannot paste it here unless you formally release it via our [[Wikipedia:Donating copyrighted materials|release process]].
Users who continue to post copyrighted material may be [[Wikipedia:Blocking policy|blocked from editing Wikipedia]].
{{#ifeq:{{NAMESPACENUMBER}}|3||{{pb}}{{huge|{{error|<u>Note to reviewers</u>: do not leave copyright violations sitting in the page history. Please follow the [[Wikipedia:WikiProject Articles for creation/Reviewing instructions/Copyright cleanup instructions|cleanup instructions]].}}}}{{pb}}<span class="sysop-show">''Administrators: if the page has been cleaned and you are seeing this notice, please change the <code>cv</code> to <code>cv-cleaned</code> in the {{t|AfC submission}} call.''</span>}}<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as copyright violations]][[Category:Requested RD1 redactions]]}}}}</includeonly>
|cv-c
|cv-cleaned = This draft appears to contain copyrighted material {{safesubst:<noinclude />#ifeq:{{{2|}}}|||from {{{2}}},}} which has been removed. Wikipedia strictly prohibits [[Wikipedia:Copyright violations|copyright violations]]. Assume all text is copyrighted unless released under a compatible license.
* We can only accept text if it is [[Wikipedia:Copying text from other sources|explicitly released under a compatible free license]] or is in the [[public domain]].
* If the text is not freely licensed, you must rewrite or summarize it entirely in your own words. [[Wikipedia:Close paraphrasing|Changing a few words]] is still considered a copyright violation.
* If you own the text you cannot paste it here unless you formally release it via our [[Wikipedia:Donating copyrighted materials|release process]].
Users who continue to post copyrighted material may be [[Wikipedia:Blocking policy|blocked from editing Wikipedia]].<p>[[File:Symbol opinion vote.svg|20px]] This submission has now been cleaned of the above-noted copyright violation and its history redacted by an administrator to remove the infringement. If re-submitted (and subsequent additions do not reintroduce copyright problems), the content may be assessed on other grounds.</p><includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as copyright violations, but later cleaned]]}}}}</includeonly>
|v
|rs
|source = This draft is not adequately supported by [[Wikipedia:Reliable sources|reliable sources]]. Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] requires that all content be supported by [[Wikipedia:Reliable sources|reliable sources]].
* Reliable sources include: reputable newspapers, magazines, academic journals, and books from respected publishers.
* Unacceptable sources include: personal blogs, social media, [[Predatory publishing|predatory publishers]], most [[Tabloid journalism|tabloids]], and websites where anyone can contribute.
Replace any unreliable sources with high-quality sources. If you cannot find a reliable source for the material, it should be removed.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as lacking reliable third-party sources]]}}}}</includeonly>
|not = This draft is not suitable for Wikipedia. Wikipedia is not a general publishing platform. We do not accept:
* ''essays or original research'': personal views, advocacy, unpublished theories, or pre-prints;
* ''resumes or personal profiles'': CVs, social networking pages, or memorials;
* ''directories or manuals'': travel guides, catalogs, or how-to instructions;
* ''creative writing'': fanfiction, worldbuilding, myths, or things you have made up.
Read [[Wikipedia:What Wikipedia is not|What Wikipedia is not]] to understand what content is excluded.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as not suitable for Wikipedia]]}}}}</includeonly>
|test = This draft appears to be a test edit. Please use the [[Wikipedia:Sandbox|sandbox]] to practice editing. Do not submit this draft for review until you have written an article intended for the encyclopedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a test]]}}}}</includeonly>
|oneevent
|news = This draft appears to be a news report or about a single event. Wikipedia is an encyclopedia, [[Wikipedia:NOT#IINFO|not an indiscriminate collection of information]] or a [[Wikipedia:NOT#NEWS|news service]]. We generally do not accept articles about:
* ''events'' that do not meet our [[Wikipedia:Notability (events)|criteria for inclusion for events]];
* ''people'' known only for a [[Wikipedia:BLP1E|single incident]].
Please add references published after the initial breaking news has passed that demonstrate the event's ''[[Wikipedia:Notability (events)#Lasting effects|lasting significance]]''. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a news report on a single event]]}}}}</includeonly>
|d
|dict = This draft appears to be a dictionary definition. [[Wikipedia:Wikipedia is not a dictionary|Wikipedia is not a dictionary]] and we do not accept articles that are simply definitions of words, acronyms, or slang.
* To improve the draft it must expand on the subject, such as discussing its history, cultural significance, or impact rather than just defining the word;
* To create a dictionary entry you can contribute to our sister project [[wikt:Main_Page|Wiktionary]]. Please ensure the subject meets [[wikt:Wiktionary:Criteria_for_inclusion|Wiktionary's inclusion criteria]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a dictionary definition]]}}}}</includeonly>
|plot = This draft appears to be a plot summary. [[Wikipedia:What Wikipedia is not#PLOT|Wikipedia does not host articles consisting of only plot summaries]]. Articles about fictional subjects must cover their real-world context supported by multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the work in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as all plot]]}}}}</includeonly>
|joke
|hoax = This draft appears to be a joke or a hoax. Wikipedia is an encyclopedia. We do not accept:
* ''[[Wikipedia:Do not create hoaxes|hoaxes]]'': made-up stories presented as fact;
* ''humor'': content intended to be funny rather than informative.
Please use the [[Wikipedia:Sandbox|sandbox]] to practice editing. Do not submit this draft for review until you have written an article intended for the encyclopedia.
Users who continue to create inappropriate pages may be [[Wikipedia:Blocking policy|blocked from editing Wikipedia]]. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as jokes]]}}}}</includeonly>
|essay = This draft reads like an essay or opinion piece. [[Wikipedia:What Wikipedia is not|Wikipedia is not a place for original research or personal opinions]]. The draft should:
* ''[[Wikipedia:No original research#Primary, secondary and tertiary sources|summarize secondary sources]]'': do not offer your own analysis or arguments;
* ''[[Wikipedia:Neutral point of view|be written from a neutral point of view]]'': represent the subject without bias, avoiding praise, criticism, or persuasive or promotional language;
* ''[[Wikipedia:No original research|not contain original research]]'': do not include new theories, unpublished ideas, or personal experiences.
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as an essay]]}}}}</includeonly>
|npov = [[Wikipedia:Neutral point of view|This draft is not written from a neutral point of view]]. Wikipedia articles must be written neutrally in a formal, impersonal, and dispassionate way. They should not read like a blog post, advertisement, or fan page. Rewrite the draft to remove:
* ''promotional language'': see [[Wikipedia:Manual of Style/Words to watch|Words to watch]];
* ''personal commentary'': opinions or direct addresses to the reader;
* ''[[Wikipedia:Tone|informal language]].''
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as not written in a neutral point of view]]}}}}</includeonly>
|adv
|spam
|advert = This draft reads like an advertisement. Wikipedia is an encyclopedia, [[Wikipedia:Spam#Advertisements masquerading as articles|not a platform for promotion or marketing]]. Drafts that are [[Wikipedia:G11|exclusively promotional may be deleted without notice]].
Wikipedia articles must be written neutrally in a formal, impersonal, and dispassionate way. They should not read like a blog post, advertisement, or fan page. Rewrite the draft to remove:
* ''promotional language'': see [[Wikipedia:Manual of Style/Words to watch|Words to watch]];
* ''personal commentary'': opinions or direct addresses to the reader;
* ''[[Wikipedia:Tone|informal language]].''
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.
If you have a [[Wikipedia:Conflict of interest|conflict of interest]] (e.g. you are the subject, an employee, or a relative) or are [[Wikipedia:Paid-contribution disclosure|being paid to edit]], you must disclose this to comply with Wikipedia's [[Wikipedia:Terms of Use|Terms of Use]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as an advertisement]]}}}}</includeonly>
|exists = This draft appears to be a duplicate of an existing article. Wikipedia does not permit multiple articles on the same topic.
* {{safesubst:<noinclude />#ifeq:{{{2|}}}|||To improve the existing article, please edit it directly at [[:{{{2}}}]]. If the article is [[Wikipedia:Protection policy|protected]] and you cannot edit it, propose your changes on the article's [[Help:Talk pages|talk page]].}}
* If you are writing about a different subject with the same name, ask the reviewer to [[Wikipedia:Disambiguation|rename your draft]] to distinguish it. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as already existing]]}}}}</includeonly>
|dup
|duplicate = This draft duplicates another submission{{safesubst:<noinclude />#ifeq: {{{2|}}}|||, [[Draft:{{{2}}}|{{{2}}}]],}} currently submitted for review. To save time, we will review the other submission only. Any future edits or improvements should be made on that submission, not here. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a duplicate]]}}}}</includeonly>
|context = This draft provides insufficient context for those unfamiliar with the subject. Please see the [[Wikipedia:Writing better articles#Provide context for the reader|guide to writing better articles]] for how to improve your writing. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as lacking context]]}}}}</includeonly>
|merge
|mergeto = This draft does not have sufficient content to warrant a standalone article of its own, but it could be merged into the existing article {{safesubst:<noinclude />#if:{{{2|}}}|at [[:{{{2}}}]]|on the same subject}}.
* Edit the existing article to include your text and citations into an appropriate section.
* If the article is [[Wikipedia:Protection policy|protected]] and you cannot edit it, propose your changes on the article's [[Help:Talk pages|Talk page]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as needing to be merged]]}}}}</includeonly>
|cat = This is not the correct place to request new categories. Please follow the instructions at [[Wikipedia:Articles for creation/Categories]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a category]]}}}}</includeonly>
<!-- New templates added below -->
|ecr= This draft covers a [[Wikipedia:Contentious topics|contentious topic]] with an editing restriction. To prevent disruption, Wikipedia's [[Wikipedia:Arbitration Committee|Arbitration Committee]] only permits editors with [[Wikipedia:User groups#Extended confirmed accounts|Extended confirmed]] accounts to write about this topic.
* You are not Extended confirmed. Your account must be at least 30 days old and have at least 500 edits.
* You must wait until your account becomes Extended confirmed before making further edits or resubmitting this draft.
For a list of contentious topics and their restrictions see [[Wikipedia:Contentious topics#List of contentious topics|Contentious topics]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as contentious topics restricted]]}}}}</includeonly>
|ns
|nosource = This draft does not include any sources or inline citations. Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] requires that all content be supported by [[Wikipedia:Reliable sources|reliable sources]]. You should also use inline citations (footnotes) to show which source supports which specific statement.
The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent sources|independent]]'': not connected to the subject, such as press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.
You must place an inline citation directly after:
* quotations;
* any material whose verifiability is likely to be challenged or is contentious;
* any material about living people.
Learn how to create inline citations in the:
* ''Source Editor'': [[Help:Introduction to referencing with Wiki Markup/1|Introduction to referencing with Wiki Markup]].
* ''VisualEditor'': [[Help:Introduction to referencing with VisualEditor/1|Introduction to referencing with VisualEditor]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as unsourced]]}}}}</includeonly>
|list= This draft's references do not show the list meets Wikipedia's [[Wikipedia:Stand-alone lists#Notability|criteria for inclusion for stand-alone lists]]. The draft requires evidence of:
* ''group notability'': sources must discuss the items in the list as a whole, rather than just verifying that the individual items exist;
* ''encyclopedic purpose'': avoid cross-categorisations, such as "List of X of Y", unless sources explicitly discuss this intersection as a distinct subject;
* ''Selection criteria'': an unambiguous, objective definition of what is included to distinguish the list from a [[Wikipedia:What Wikipedia is not#DIR|directory]].
Please add references that meet ''all three'' of these criteria. If no such sources exist, the list is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable list]]}}}}</includeonly>
|med= This draft contains [[Wikipedia:Biomedical information|medical, health, or psychological claims]]. Wikipedia requires a [[Wikipedia:Identifying reliable sources (medicine)|higher standard of sourcing for biomedical content]]. To ensure accuracy, content must be based on reliable secondary sources that reflect current knowledge. The draft should:
* ''have high-quality secondary sources'': such as systematic reviews, meta-analyses, or medical textbooks from respected publishers;
* ''avoid primary sources'': such as direct research papers, clinical trials, or news reports on early studies.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.
<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable medical content]]}}}}</includeonly>
|geo= This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (geographic features)|criteria for inclusion for geographic features]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (geographic features)|specific criteria for geographic features]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine directory listings;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as press releases, tourism boards, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable geographic feature]]}}}}</includeonly>
|species= This draft's references do not show that the species meets Wikipedia's [[Wikipedia:Notability (species)|criteria for inclusion for species]]. The draft requires either:
* evidence that the species meets any of the ''[[Wikipedia:Notability (species)|specific criteria for species]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the species in detail, not just brief mentions;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable scientific outlets with editorial oversight.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable species]]}}}}</includeonly>
|astro= This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (astronomical objects)|criteria for inclusion for astronomical objects]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (astronomical objects)|specific criteria for astronomical objects]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the object in detail, not just brief mentions or database entries;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable scientific outlets with editorial oversight.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.
<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable astronomical object]]}}}}</includeonly>
|number= This draft's references do not show that the number meets Wikipedia's [[Wikipedia:Notability (numbers)|criteria for inclusion for numbers]]. The draft requires either:
* evidence that the number meets any of the ''[[Wikipedia:Notability (numbers)|specific criteria for numbers]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the number in detail;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable mathematical outlets with editorial oversight.
Please add references that meet these criteria. If no such sources exist, the number is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable number]]}}}}</includeonly>
|creative = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (people)#Creative professionals|criteria for inclusion for creative professionals]]. The draft requires:
* evidence that the subject meets the ''[[Wikipedia:Notability|general criteria for inclusion]]'';
* '''or''' evidence that the subject meets any of the ''[[Wikipedia:Notability (people)#Creative professionals|specific criteria for creative professionals]]'';
* '''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that cover the subject or their work and:
** provide ''[[Wikipedia:Notability#significant_coverage|critical attention]]'': professional reviews, excluding user reviews from the general public;
** are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
** are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.
It is often easier to prove the notability of a ''creative work'' than of a ''person''. If the person is not yet notable, but their work has received multiple in-depth professional reviews, consider writing about the work instead.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable creative professional]]}}}}</includeonly>
|school = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (organizations and companies)#Schools|criteria for inclusion for schools]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability (organizations and companies)#Significant coverage|significant coverage]]'': discuss the subject in detail, excluding routine coverage like sports results, league tables, government inspection reports, and listings in databases or [[listicle]]s;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.
Individual faculties, departments, and student clubs are rarely notable on their own. You could add this content to the main educational institution article if one exists instead.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable school]]}}}}</includeonly>
|reason = {{{2}}}<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined with a custom reason]]}}}}</includeonly>
|#default = {{{1}}}
}}<noinclude>
{{Documentation}}
<!-- Categories go on the /doc subpage, and interwikis go on Wikidata. -->
</noinclude>
7r2vq9ht2p1k2r96oragafn3ewsoyxs
740060
740059
2026-05-01T19:16:16Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/comments]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740059
wikitext
text/x-wiki
{{safesubst:<noinclude />#switch:{{safesubst:<noinclude />lc:{{{1}}}}}
|resume = This draft reads like a resume or curriculum vitae. Wikipedia is an encyclopedia, not a professional networking website or a place to promote yourself or your services. We also [[Wikipedia:Autobiography|strongly discourage writing about yourself]].
Articles about people must meet Wikipedia's [[Wikipedia:Notability (people)|criteria for inclusion for people]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability_(people)#Basic_criteria|significant coverage]]'': discuss the person in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the person, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as resume-like]]}}}}</includeonly>
|ai
|llm = This draft appears to be generated by a [[large language model]] (such as ChatGPT). You [[Wikipedia:NEWLLM|cannot use LLMs]] to generate article content.
LLM-generated pages with [[Wikipedia:G15|certain obvious signs of being machine generated]] may be deleted without notice.
These tools are prone to specific issues that violate our policies:
* ''[[Hallucination (artificial intelligence)|hallucinations]]'': they often invent false information and cite non-existent references.
* ''[[Wikipedia:Neutral point of view|unencyclopedic tone]]'': they tend to be vague, promotional, or essay-like, rather than neutral and factual.
* ''[[Wikipedia:Copyright violations|copyright issues]]'': they may closely paraphrase existing text, leading to copyright violations.
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.
See [[Wikipedia:Large language models|the advice page on large language models]] for more information. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a large language model output]]}}}}</includeonly>
|neo = This draft's references do not show that the neologism meets Wikipedia's [[Wikipedia:Notability|criteria for inclusion]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the term in detail, not just brief mentions or routine use;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as promoted or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.
[[Wikipedia:Neologism|Wikipedia is not a dictionary]] and we do not accept articles that are simply definitions of neologisms.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as non-notable web content]]}}}}</includeonly>
|web = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (web)|criteria for inclusion for web content]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as non-notable web content]]}}}}</includeonly>
|prof
|academic = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (academics)|criteria for inclusion for academics]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (academics)|specific criteria for academics]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable academic topic]]}}}}</includeonly>
|sport
|athlete = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (sports)|criteria for inclusion for sports]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (sports)|specific criteria for sports]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable athletic topic]]}}}}</includeonly>
|music
|m
|band = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (music)|criteria for inclusion for music]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (music)|specific criteria for music]];''
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.
It is often easier to prove the notability of an ''album'' or ''artist'' than an ''individual song'' or ''band member''. If the subject is not yet notable, consider improving a relevant existing article instead. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable musical topic]]}}}}</includeonly>
|film = This draft's references do not show that the film meets Wikipedia's [[Wikipedia:Notability (films)|criteria for inclusion for films]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (films)|specific criteria for films]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the film in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the film, such as press releases, the studio's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable film]]}}}}</includeonly>
|book = This draft's references do not show that the book meets Wikipedia's [[Wikipedia:Notability (books)|criteria for inclusion for books]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (books)|specific criteria for books]]''
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the book in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the book, such as press releases, the author or publisher's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable book]]}}}}</includeonly>
|event = This draft's references do not show that the event meets Wikipedia's [[Wikipedia:Notability (events)|criteria for inclusion for events]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the event in detail, not just brief mentions or routine announcements;
* show ''[[Wikipedia:Notability (events)#Lasting effects|lasting significance]]'': analysis published after the initial breaking news has passed, showing historical impact;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the event, such as press releases, the event or organizers own website, or sponsored content.
Please add references that meet ''all four'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable event]]}}}}</includeonly>
|org
|inc
|corp = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (organizations and companies)|criteria for inclusion for organizations and companies]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability (organizations and companies)#Significant coverage|significant coverage]]'': discuss the subject in detail, excluding routine coverage like product launches, staff appointments, or financial reports and listings in databases or [[listicle]]s;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable organization]]}}}}</includeonly>
|bio = This draft's references do not show that the person meets Wikipedia's [[Wikipedia:Notability (people)|criteria for inclusion for people]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability_(people)#Basic_criteria|significant coverage]]'': discuss the person in detail, not brief mentions or interviews lacking independent analysis;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the person, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable biography]]}}}}</includeonly>
|nn = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability|criteria for inclusion]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as non-notable]]}}}}</includeonly>
|empty
|blank = This draft appears to be blank. If you have not written your text yet, please add your content before resubmitting. If you did write the text, it may be hidden by a formatting error. Please check the "Edit" tab to ensure your text is not inside a comment tag.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as blank]]}}}}</includeonly>
|notenglish
|lang = {{safesubst:<noinclude />#if: {{{2|}}}|This draft is written in {{{2}}}.|}} This is the English Wikipedia and we can only accept articles written in the [[English language]].
* Please provide a high-quality English translation, avoiding [[Wikipedia:Translation|machine translation tools]] (like Google Translate).
{{safesubst:<noinclude />#if: {{safesubst:<noinclude />Mw lang|fn=is_name|{{{2|}}}}}|If you prefer to write in your own language, you may write it in the [[:{{safesubst:<noinclude />mw lang|fn=code_from_name|{{{2}}}}}:|{{{2}}} Wikipedia]].|If you prefer to write in your own language, look for the Wikipedia project in your language on the [https://www.wikipedia.org/ Wikipedia home page].|}}<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as not in English]]}}}}</includeonly>
|test = This draft appears to be a test edit. Please use the [[Wikipedia:Sandbox|sandbox]] to practice editing. Do not submit this draft for review until you have written an article intended for the encyclopedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a test]]}}}}</includeonly>
|redirect = This is not the correct place to request new redirects. Please follow the instructions at [[Wikipedia:Article wizard/Redirects]]. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a redirect]]}}}}</includeonly>
|van = This draft has been declined because it meets one of these criteria:
* ''[[Wikipedia:Attack page|attack page]]'': it is created primarily to disparage, threaten, or harass a subject;
* ''[[Wikipedia:Biographies of living persons|negative biography]]'': it contains unsourced, negative material about a living person;
* ''[[Wikipedia:Vandalism|vandalism]]'': it contains jokes, hoaxes, nonsense, or gibberish.
Drafts that meet [[Wikipedia:Deletion policy|these criteria may be deleted without notice]].
Users who continue to create inappropriate pages may be [[Wikipedia:Blocking policy|blocked from editing Wikipedia]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as being vandalism or attack pages]]}}}}</includeonly>
|cite
|footnote
|ilc = This draft lacks inline citations. Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] requires that all content be supported by [[Wikipedia:Reliable sources|reliable sources]]. We require inline citations (footnotes) to show which source supports which specific statement.
You must place an inline citation directly after:
* quotations;
* any material whose verifiability is likely to be challenged or is contentious;
* any material about living people.
Please edit your draft to support your statements with inline citations. Learn how to create inline citations in the:
* ''Source Editor'': [[Help:Introduction to referencing with Wiki Markup/1|Introduction to referencing with Wiki Markup]].
* ''VisualEditor'': [[Help:Introduction to referencing with VisualEditor/1|Introduction to referencing with VisualEditor]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as needing footnotes]]}}}}</includeonly>
|blp = The text of this submission has been removed. Wikipedia has a strict [[Wikipedia:Biographies of living persons|policy regarding biographies of living people]]. To protect subjects from harm, we remove content that is:
* ''[[Wikipedia:Unsourced|unverified]]'': lacking citations from reliable sources and
* ''[[Wikipedia:Libel|potentially damaging]]'': containing negative, sensitive, or libelous information that cannot be verified.
Your text is preserved in the [[Help:Page history|page history]] tab. You may retrieve it only if you immediately add inline citations to [[Wikipedia:Reliable sources|reliable sources]] that support the material.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as BLP violations]]}}}}</includeonly>
|cv-n
|cv = This draft appears to contain copyrighted material{{safesubst:<noinclude />#ifeq: {{{2|}}}|||{{spaces}}from {{{2}}},}} which has been removed. Wikipedia strictly prohibits [[Wikipedia:Copyright violations|copyright violations]]. Assume all text is copyrighted unless released under a compatible license.
* We can only accept text if it is [[Wikipedia:Copying text from other sources|explicitly released under a compatible free license]] or is in the [[public domain]].
* If the text is not freely licensed, you must rewrite or summarize it entirely in your own words. [[Wikipedia:Close paraphrasing|Changing a few words]] is still considered a copyright violation.
* If you own the text you cannot paste it here unless you formally release it via our [[Wikipedia:Donating copyrighted materials|release process]].
Users who continue to post copyrighted material may be [[Wikipedia:Blocking policy|blocked from editing Wikipedia]].
{{#ifeq:{{NAMESPACENUMBER}}|3||{{pb}}{{huge|{{error|<u>Note to reviewers</u>: do not leave copyright violations sitting in the page history. Please follow the [[Wikipedia:WikiProject Articles for creation/Reviewing instructions/Copyright cleanup instructions|cleanup instructions]].}}}}{{pb}}<span class="sysop-show">''Administrators: if the page has been cleaned and you are seeing this notice, please change the <code>cv</code> to <code>cv-cleaned</code> in the {{t|AfC submission}} call.''</span>}}<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as copyright violations]][[Category:Requested RD1 redactions]]}}}}</includeonly>
|cv-c
|cv-cleaned = This draft appears to contain copyrighted material {{safesubst:<noinclude />#ifeq:{{{2|}}}|||from {{{2}}},}} which has been removed. Wikipedia strictly prohibits [[Wikipedia:Copyright violations|copyright violations]]. Assume all text is copyrighted unless released under a compatible license.
* We can only accept text if it is [[Wikipedia:Copying text from other sources|explicitly released under a compatible free license]] or is in the [[public domain]].
* If the text is not freely licensed, you must rewrite or summarize it entirely in your own words. [[Wikipedia:Close paraphrasing|Changing a few words]] is still considered a copyright violation.
* If you own the text you cannot paste it here unless you formally release it via our [[Wikipedia:Donating copyrighted materials|release process]].
Users who continue to post copyrighted material may be [[Wikipedia:Blocking policy|blocked from editing Wikipedia]].<p>[[File:Symbol opinion vote.svg|20px]] This submission has now been cleaned of the above-noted copyright violation and its history redacted by an administrator to remove the infringement. If re-submitted (and subsequent additions do not reintroduce copyright problems), the content may be assessed on other grounds.</p><includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as copyright violations, but later cleaned]]}}}}</includeonly>
|v
|rs
|source = This draft is not adequately supported by [[Wikipedia:Reliable sources|reliable sources]]. Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] requires that all content be supported by [[Wikipedia:Reliable sources|reliable sources]].
* Reliable sources include: reputable newspapers, magazines, academic journals, and books from respected publishers.
* Unacceptable sources include: personal blogs, social media, [[Predatory publishing|predatory publishers]], most [[Tabloid journalism|tabloids]], and websites where anyone can contribute.
Replace any unreliable sources with high-quality sources. If you cannot find a reliable source for the material, it should be removed.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as lacking reliable third-party sources]]}}}}</includeonly>
|not = This draft is not suitable for Wikipedia. Wikipedia is not a general publishing platform. We do not accept:
* ''essays or original research'': personal views, advocacy, unpublished theories, or pre-prints;
* ''resumes or personal profiles'': CVs, social networking pages, or memorials;
* ''directories or manuals'': travel guides, catalogs, or how-to instructions;
* ''creative writing'': fanfiction, worldbuilding, myths, or things you have made up.
Read [[Wikipedia:What Wikipedia is not|What Wikipedia is not]] to understand what content is excluded.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as not suitable for Wikipedia]]}}}}</includeonly>
|test = This draft appears to be a test edit. Please use the [[Wikipedia:Sandbox|sandbox]] to practice editing. Do not submit this draft for review until you have written an article intended for the encyclopedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a test]]}}}}</includeonly>
|oneevent
|news = This draft appears to be a news report or about a single event. Wikipedia is an encyclopedia, [[Wikipedia:NOT#IINFO|not an indiscriminate collection of information]] or a [[Wikipedia:NOT#NEWS|news service]]. We generally do not accept articles about:
* ''events'' that do not meet our [[Wikipedia:Notability (events)|criteria for inclusion for events]];
* ''people'' known only for a [[Wikipedia:BLP1E|single incident]].
Please add references published after the initial breaking news has passed that demonstrate the event's ''[[Wikipedia:Notability (events)#Lasting effects|lasting significance]]''. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a news report on a single event]]}}}}</includeonly>
|d
|dict = This draft appears to be a dictionary definition. [[Wikipedia:Wikipedia is not a dictionary|Wikipedia is not a dictionary]] and we do not accept articles that are simply definitions of words, acronyms, or slang.
* To improve the draft it must expand on the subject, such as discussing its history, cultural significance, or impact rather than just defining the word;
* To create a dictionary entry you can contribute to our sister project [[wikt:Main_Page|Wiktionary]]. Please ensure the subject meets [[wikt:Wiktionary:Criteria_for_inclusion|Wiktionary's inclusion criteria]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a dictionary definition]]}}}}</includeonly>
|plot = This draft appears to be a plot summary. [[Wikipedia:What Wikipedia is not#PLOT|Wikipedia does not host articles consisting of only plot summaries]]. Articles about fictional subjects must cover their real-world context supported by multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the work in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as all plot]]}}}}</includeonly>
|joke
|hoax = This draft appears to be a joke or a hoax. Wikipedia is an encyclopedia. We do not accept:
* ''[[Wikipedia:Do not create hoaxes|hoaxes]]'': made-up stories presented as fact;
* ''humor'': content intended to be funny rather than informative.
Please use the [[Wikipedia:Sandbox|sandbox]] to practice editing. Do not submit this draft for review until you have written an article intended for the encyclopedia.
Users who continue to create inappropriate pages may be [[Wikipedia:Blocking policy|blocked from editing Wikipedia]]. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as jokes]]}}}}</includeonly>
|essay = This draft reads like an essay or opinion piece. [[Wikipedia:What Wikipedia is not|Wikipedia is not a place for original research or personal opinions]]. The draft should:
* ''[[Wikipedia:No original research#Primary, secondary and tertiary sources|summarize secondary sources]]'': do not offer your own analysis or arguments;
* ''[[Wikipedia:Neutral point of view|be written from a neutral point of view]]'': represent the subject without bias, avoiding praise, criticism, or persuasive or promotional language;
* ''[[Wikipedia:No original research|not contain original research]]'': do not include new theories, unpublished ideas, or personal experiences.
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as an essay]]}}}}</includeonly>
|npov = [[Wikipedia:Neutral point of view|This draft is not written from a neutral point of view]]. Wikipedia articles must be written neutrally in a formal, impersonal, and dispassionate way. They should not read like a blog post, advertisement, or fan page. Rewrite the draft to remove:
* ''promotional language'': see [[Wikipedia:Manual of Style/Words to watch|Words to watch]];
* ''personal commentary'': opinions or direct addresses to the reader;
* ''[[Wikipedia:Tone|informal language]].''
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as not written in a neutral point of view]]}}}}</includeonly>
|adv
|spam
|advert = This draft reads like an advertisement. Wikipedia is an encyclopedia, [[Wikipedia:Spam#Advertisements masquerading as articles|not a platform for promotion or marketing]]. Drafts that are [[Wikipedia:G11|exclusively promotional may be deleted without notice]].
Wikipedia articles must be written neutrally in a formal, impersonal, and dispassionate way. They should not read like a blog post, advertisement, or fan page. Rewrite the draft to remove:
* ''promotional language'': see [[Wikipedia:Manual of Style/Words to watch|Words to watch]];
* ''personal commentary'': opinions or direct addresses to the reader;
* ''[[Wikipedia:Tone|informal language]].''
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.
If you have a [[Wikipedia:Conflict of interest|conflict of interest]] (e.g. you are the subject, an employee, or a relative) or are [[Wikipedia:Paid-contribution disclosure|being paid to edit]], you must disclose this to comply with Wikipedia's [[Wikipedia:Terms of Use|Terms of Use]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as an advertisement]]}}}}</includeonly>
|exists = This draft appears to be a duplicate of an existing article. Wikipedia does not permit multiple articles on the same topic.
* {{safesubst:<noinclude />#ifeq:{{{2|}}}|||To improve the existing article, please edit it directly at [[:{{{2}}}]]. If the article is [[Wikipedia:Protection policy|protected]] and you cannot edit it, propose your changes on the article's [[Help:Talk pages|talk page]].}}
* If you are writing about a different subject with the same name, ask the reviewer to [[Wikipedia:Disambiguation|rename your draft]] to distinguish it. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as already existing]]}}}}</includeonly>
|dup
|duplicate = This draft duplicates another submission{{safesubst:<noinclude />#ifeq: {{{2|}}}|||, [[Draft:{{{2}}}|{{{2}}}]],}} currently submitted for review. To save time, we will review the other submission only. Any future edits or improvements should be made on that submission, not here. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a duplicate]]}}}}</includeonly>
|context = This draft provides insufficient context for those unfamiliar with the subject. Please see the [[Wikipedia:Writing better articles#Provide context for the reader|guide to writing better articles]] for how to improve your writing. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as lacking context]]}}}}</includeonly>
|merge
|mergeto = This draft does not have sufficient content to warrant a standalone article of its own, but it could be merged into the existing article {{safesubst:<noinclude />#if:{{{2|}}}|at [[:{{{2}}}]]|on the same subject}}.
* Edit the existing article to include your text and citations into an appropriate section.
* If the article is [[Wikipedia:Protection policy|protected]] and you cannot edit it, propose your changes on the article's [[Help:Talk pages|Talk page]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as needing to be merged]]}}}}</includeonly>
|cat = This is not the correct place to request new categories. Please follow the instructions at [[Wikipedia:Articles for creation/Categories]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a category]]}}}}</includeonly>
<!-- New templates added below -->
|ecr= This draft covers a [[Wikipedia:Contentious topics|contentious topic]] with an editing restriction. To prevent disruption, Wikipedia's [[Wikipedia:Arbitration Committee|Arbitration Committee]] only permits editors with [[Wikipedia:User groups#Extended confirmed accounts|Extended confirmed]] accounts to write about this topic.
* You are not Extended confirmed. Your account must be at least 30 days old and have at least 500 edits.
* You must wait until your account becomes Extended confirmed before making further edits or resubmitting this draft.
For a list of contentious topics and their restrictions see [[Wikipedia:Contentious topics#List of contentious topics|Contentious topics]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as contentious topics restricted]]}}}}</includeonly>
|ns
|nosource = This draft does not include any sources or inline citations. Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] requires that all content be supported by [[Wikipedia:Reliable sources|reliable sources]]. You should also use inline citations (footnotes) to show which source supports which specific statement.
The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine announcements;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent sources|independent]]'': not connected to the subject, such as press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.
You must place an inline citation directly after:
* quotations;
* any material whose verifiability is likely to be challenged or is contentious;
* any material about living people.
Learn how to create inline citations in the:
* ''Source Editor'': [[Help:Introduction to referencing with Wiki Markup/1|Introduction to referencing with Wiki Markup]].
* ''VisualEditor'': [[Help:Introduction to referencing with VisualEditor/1|Introduction to referencing with VisualEditor]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as unsourced]]}}}}</includeonly>
|list= This draft's references do not show the list meets Wikipedia's [[Wikipedia:Stand-alone lists#Notability|criteria for inclusion for stand-alone lists]]. The draft requires evidence of:
* ''group notability'': sources must discuss the items in the list as a whole, rather than just verifying that the individual items exist;
* ''encyclopedic purpose'': avoid cross-categorisations, such as "List of X of Y", unless sources explicitly discuss this intersection as a distinct subject;
* ''Selection criteria'': an unambiguous, objective definition of what is included to distinguish the list from a [[Wikipedia:What Wikipedia is not#DIR|directory]].
Please add references that meet ''all three'' of these criteria. If no such sources exist, the list is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable list]]}}}}</includeonly>
|med= This draft contains [[Wikipedia:Biomedical information|medical, health, or psychological claims]]. Wikipedia requires a [[Wikipedia:Identifying reliable sources (medicine)|higher standard of sourcing for biomedical content]]. To ensure accuracy, content must be based on reliable secondary sources that reflect current knowledge. The draft should:
* ''have high-quality secondary sources'': such as systematic reviews, meta-analyses, or medical textbooks from respected publishers;
* ''avoid primary sources'': such as direct research papers, clinical trials, or news reports on early studies.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.
<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable medical content]]}}}}</includeonly>
|geo= This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (geographic features)|criteria for inclusion for geographic features]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (geographic features)|specific criteria for geographic features]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the subject in detail, not just brief mentions or routine directory listings;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as press releases, tourism boards, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable geographic feature]]}}}}</includeonly>
|species= This draft's references do not show that the species meets Wikipedia's [[Wikipedia:Notability (species)|criteria for inclusion for species]]. The draft requires either:
* evidence that the species meets any of the ''[[Wikipedia:Notability (species)|specific criteria for species]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the species in detail, not just brief mentions;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable scientific outlets with editorial oversight.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable species]]}}}}</includeonly>
|astro= This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (astronomical objects)|criteria for inclusion for astronomical objects]]. The draft requires either:
* evidence that the subject meets any of the ''[[Wikipedia:Notability (astronomical objects)|specific criteria for astronomical objects]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the object in detail, not just brief mentions or database entries;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable scientific outlets with editorial oversight.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.
<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable astronomical object]]}}}}</includeonly>
|number= This draft's references do not show that the number meets Wikipedia's [[Wikipedia:Notability (numbers)|criteria for inclusion for numbers]]. The draft requires either:
* evidence that the number meets any of the ''[[Wikipedia:Notability (numbers)|specific criteria for numbers]]'';
'''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability#significant_coverage|significant coverage]]'': discuss the number in detail;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable mathematical outlets with editorial oversight.
Please add references that meet these criteria. If no such sources exist, the number is not yet suitable for Wikipedia.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable number]]}}}}</includeonly>
|creative = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (people)#Creative professionals|criteria for inclusion for creative professionals]]. The draft requires:
* evidence that the subject meets the ''[[Wikipedia:Notability|general criteria for inclusion]]'';
* '''or''' evidence that the subject meets any of the ''[[Wikipedia:Notability (people)#Creative professionals|specific criteria for creative professionals]]'';
* '''or''' multiple published [[Wikipedia:Secondary|secondary]] sources that cover the subject or their work and:
** provide ''[[Wikipedia:Notability#significant_coverage|critical attention]]'': professional reviews, excluding user reviews from the general public;
** are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
** are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as interviews, press releases, the subject's own website, or sponsored content.
Please add references that meet these criteria. If none exist, the subject is not yet suitable for Wikipedia.
It is often easier to prove the notability of a ''creative work'' than of a ''person''. If the person is not yet notable, but their work has received multiple in-depth professional reviews, consider writing about the work instead.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable creative professional]]}}}}</includeonly>
|school = This draft's references do not show that the subject meets Wikipedia's [[Wikipedia:Notability (organizations and companies)#Schools|criteria for inclusion for schools]]. The draft requires multiple published [[Wikipedia:Secondary|secondary]] sources that:
* provide ''[[Wikipedia:Notability (organizations and companies)#Significant coverage|significant coverage]]'': discuss the subject in detail, excluding routine coverage like sports results, league tables, government inspection reports, and listings in databases or [[listicle]]s;
* are ''[[Wikipedia:Reliable sources|reliable]]'': from reputable outlets with editorial oversight;
* are ''[[Wikipedia:Independent_sources|independent]]'': not connected to the subject, such as press releases, the subject's own website, or sponsored content.
Please add references that meet ''all three'' of these criteria. If none exist, the subject is not yet suitable for Wikipedia.
Individual faculties, departments, and student clubs are rarely notable on their own. You could add this content to the main educational institution article if one exists instead.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined as a non-notable school]]}}}}</includeonly>
|reason = {{{2}}}<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||{{safesubst:<noinclude />#switch:{{NAMESPACENUMBER}}|2|118=[[Category:AfC submissions declined with a custom reason]]}}}}</includeonly>
|#default = {{{1}}}
}}<noinclude>
{{Documentation}}
<!-- Categories go on the /doc subpage, and interwikis go on Wikidata. -->
</noinclude>
7r2vq9ht2p1k2r96oragafn3ewsoyxs
Template:AfC submission/comments/doc
10
122556
740065
732757
2026-03-14T22:32:04Z
en>HyperAnd
0
Display new templates in documentation
740065
wikitext
text/x-wiki
{{Documentation subpage}}
<!-- Categories go at the bottom of this page and interwikis go in Wikidata. -->
{{High-use}}
These are the possible automated comments when using the {{tlx|AfC submission|D}} template:
{| class="wikitable"
! code || Comment
|-
! colspan=2 | Notability {{anchor|neo}}
{{AFC submission/table|neo}} {{anchor|web}}
{{AFC submission/table|web}} {{anchor|prof|academic}}
{{AFC submission/table|prof|academic}} {{anchors|athlete|sport}}
{{AFC submission/table|athlete|sport}} {{anchors|music|m|band}}
{{AFC submission/table|music|m|band}} {{anchor|film}}
{{AFC submission/table|film}} {{anchors|corp|inc|org}}
{{AFC submission/table|book}}
{{AFC submission/table|event}}
{{AFC submission/table|corp|inc|org}} {{anchor|bio}}
{{AFC submission/table|bio}} {{anchor|nn}}
{{AFC submission/table|nn}}
{{AFC submission/table|list}}
{{AFC submission/table|med}}
{{AFC submission/table|geo}}
{{AFC submission/table|species}}
{{AFC submission/table|astro}}
{{AFC submission/table|number}}
{{AFC submission/table|creative}}
{{AFC submission/table|school}}
|-
! colspan=2 | Invalid submissions {{anchors|cat}}
{{AFC submission/table|cat}} {{anchor|empty|blank}}
{{AFC submission/table|empty|blank}} {{anchor|lang}}
{{AFC submission/table|lang|notenglish|input=language}} {{anchor|test}}
{{AFC submission/table|test}} {{anchor|redirect}}
{{AFC submission/table|redirect}}
|-
! colspan=2 | BLP and vandalism {{anchor|van}}
{{AFC submission/table|van}} {{anchor|ilc}}
{{AFC submission/table|ilc}} {{anchor|blp}}
{{AFC submission/table|blp}}
|-
! colspan=2 | Submission content {{anchors|cv|cv-n}}
{{AFC submission/table|cv|cv-n}} {{anchors|cv_2|cv-n_2}}
{{AFC submission/table|cv|cv-n|input=Website}} {{anchor|cv-cleaned|cv-c}}
{{AFC submission/table|cv-cleaned|cv-c}} {{anchor|cv-bot}}
{{AFC submission/table|cv-bot}} {{anchors|source|v|rs}}
{{AFC submission/table|source|v|rs}} {{anchor|not}}
{{AFC submission/table|not}} {{anchors|news|oneevent}}
{{AFC submission/table|news|oneevent}} {{anchors|dict|d}}
{{AFC submission/table|dict|d}} {{anchor|plot}}
{{AFC submission/table|plot}} {{anchor|plot_2}}
{{AFC submission/table|plot|input=Pagename}} {{anchor|joke}}
{{AFC submission/table|joke}}
{{AFC submission/table|ai|llm}} {{anchors|ai|llm}}
{{AFC submission/table|resume}}
{{AFC submission/table|ecr}}
{{AFC submission/table|nosource}}
|-
! colspan=2 | Prose issues {{anchor|essay}}
{{AFC submission/table|essay}} {{anchor|npov}}
{{AFC submission/table|npov}} {{anchors|advert|spam|adv}}
{{AFC submission/table|advert|spam|adv}}
|-
! colspan=2 | Duplicate articles {{anchor|exists}}
{{AFC submission/table|exists}} {{anchor|exists_2}}
{{AFC submission/table|exists|input=Pagename}} {{anchors|duplicate|dup}}
{{AFC submission/table|duplicate|dup}} {{anchors|duplicate_2|dup_2}}
{{AFC submission/table|duplicate|dup|input=Title}}
|-
! colspan=2 | Other {{anchor|context}}
{{AFC submission/table|context}} {{anchors|mergeto|merge}}
{{AFC submission/table|mergeto|merge}} {{anchor|r}}
{{AFC submission/table|mergeto|merge|input=Pagename}} {{anchor|afd}}
{{AFC submission/table|afd}} {{anchor|reason}}
{{AFC submission/table|reason|input=Reason}}
|}
<includeonly>{{Sandbox other||
<!-- Categories go here, and interwikis go in Wikidata -->
{{DEFAULTSORT:*}}
[[Category:WikiProject Articles for creation]]
}}</includeonly>
rmabeuudsfa9ze1f9xwa9zz65hpknzc
740066
740065
2026-05-01T19:16:18Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/comments/doc]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740065
wikitext
text/x-wiki
{{Documentation subpage}}
<!-- Categories go at the bottom of this page and interwikis go in Wikidata. -->
{{High-use}}
These are the possible automated comments when using the {{tlx|AfC submission|D}} template:
{| class="wikitable"
! code || Comment
|-
! colspan=2 | Notability {{anchor|neo}}
{{AFC submission/table|neo}} {{anchor|web}}
{{AFC submission/table|web}} {{anchor|prof|academic}}
{{AFC submission/table|prof|academic}} {{anchors|athlete|sport}}
{{AFC submission/table|athlete|sport}} {{anchors|music|m|band}}
{{AFC submission/table|music|m|band}} {{anchor|film}}
{{AFC submission/table|film}} {{anchors|corp|inc|org}}
{{AFC submission/table|book}}
{{AFC submission/table|event}}
{{AFC submission/table|corp|inc|org}} {{anchor|bio}}
{{AFC submission/table|bio}} {{anchor|nn}}
{{AFC submission/table|nn}}
{{AFC submission/table|list}}
{{AFC submission/table|med}}
{{AFC submission/table|geo}}
{{AFC submission/table|species}}
{{AFC submission/table|astro}}
{{AFC submission/table|number}}
{{AFC submission/table|creative}}
{{AFC submission/table|school}}
|-
! colspan=2 | Invalid submissions {{anchors|cat}}
{{AFC submission/table|cat}} {{anchor|empty|blank}}
{{AFC submission/table|empty|blank}} {{anchor|lang}}
{{AFC submission/table|lang|notenglish|input=language}} {{anchor|test}}
{{AFC submission/table|test}} {{anchor|redirect}}
{{AFC submission/table|redirect}}
|-
! colspan=2 | BLP and vandalism {{anchor|van}}
{{AFC submission/table|van}} {{anchor|ilc}}
{{AFC submission/table|ilc}} {{anchor|blp}}
{{AFC submission/table|blp}}
|-
! colspan=2 | Submission content {{anchors|cv|cv-n}}
{{AFC submission/table|cv|cv-n}} {{anchors|cv_2|cv-n_2}}
{{AFC submission/table|cv|cv-n|input=Website}} {{anchor|cv-cleaned|cv-c}}
{{AFC submission/table|cv-cleaned|cv-c}} {{anchor|cv-bot}}
{{AFC submission/table|cv-bot}} {{anchors|source|v|rs}}
{{AFC submission/table|source|v|rs}} {{anchor|not}}
{{AFC submission/table|not}} {{anchors|news|oneevent}}
{{AFC submission/table|news|oneevent}} {{anchors|dict|d}}
{{AFC submission/table|dict|d}} {{anchor|plot}}
{{AFC submission/table|plot}} {{anchor|plot_2}}
{{AFC submission/table|plot|input=Pagename}} {{anchor|joke}}
{{AFC submission/table|joke}}
{{AFC submission/table|ai|llm}} {{anchors|ai|llm}}
{{AFC submission/table|resume}}
{{AFC submission/table|ecr}}
{{AFC submission/table|nosource}}
|-
! colspan=2 | Prose issues {{anchor|essay}}
{{AFC submission/table|essay}} {{anchor|npov}}
{{AFC submission/table|npov}} {{anchors|advert|spam|adv}}
{{AFC submission/table|advert|spam|adv}}
|-
! colspan=2 | Duplicate articles {{anchor|exists}}
{{AFC submission/table|exists}} {{anchor|exists_2}}
{{AFC submission/table|exists|input=Pagename}} {{anchors|duplicate|dup}}
{{AFC submission/table|duplicate|dup}} {{anchors|duplicate_2|dup_2}}
{{AFC submission/table|duplicate|dup|input=Title}}
|-
! colspan=2 | Other {{anchor|context}}
{{AFC submission/table|context}} {{anchors|mergeto|merge}}
{{AFC submission/table|mergeto|merge}} {{anchor|r}}
{{AFC submission/table|mergeto|merge|input=Pagename}} {{anchor|afd}}
{{AFC submission/table|afd}} {{anchor|reason}}
{{AFC submission/table|reason|input=Reason}}
|}
<includeonly>{{Sandbox other||
<!-- Categories go here, and interwikis go in Wikidata -->
{{DEFAULTSORT:*}}
[[Category:WikiProject Articles for creation]]
}}</includeonly>
rmabeuudsfa9ze1f9xwa9zz65hpknzc
User talk:Novem Linguae
3
122929
740108
721322
2026-05-01T20:19:40Z
Novem Linguae
49714
Notification: Your [[Draft:NovemTest|Articles for Creation submission]] has been rejected
740108
wikitext
text/x-wiki
{{Talk header}}
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (November 27) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This submission appears to [[WP:Spam#Advertisements masquerading as articles|read more like an advertisement]] than an entry in an encyclopedia. Encyclopedia articles need to be written from a [[Wikipedia:Neutral point of view|neutral point of view]], and should refer to a range of [[Wikipedia:Reliable sources|independent, reliable, published sources]], not just to materials produced by the creator of the subject being discussed. This is important so that the article can meet Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] and the [[Wikipedia:Notability|notability]] of the subject can be established. If you still feel that this subject is worthy of inclusion in Wikipedia, please rewrite your submission to comply with these policies.|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:NovemTest]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:NovemTest '''Articles for creation help desk''']</span>, on the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:NovemTest '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 07:20, 27 November 2024 (UTC)</div><!--Template:AfC decline-->
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:Jon Snow (fictional character)|Jon Snow (fictional character)]] (March 18) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by JoBoGamer was:
{{divbox|gray|3=This submission is not suitable for Wikipedia. Please read [[Wikipedia:What Wikipedia is not|"What Wikipedia is not"]] for more information.|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:Jon Snow (fictional character)]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:Jon_Snow_(fictional_character) '''Articles for creation help desk''']</span>, on the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=User_talk:JoBoGamer&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:Jon_Snow_(fictional_character) '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
[[User:JoBoGamer|JoBoGamer]] ([[User talk:JoBoGamer|talk]]) 10:57, 18 March 2025 (UTC)</div><!--Template:AfC decline-->
{| style="margin: 0.4em 2em;"
|- style="vertical-align: top;"
| [[File:WP teahouse logo 2.png|alt=Teahouse logo]]
| <div style="background-color:#e1e6db; color: #393D38; padding: 1em; font-size: 1.1em; border-radius:10px;box-shadow:-2px -2px 1px #8e8a78;">Hello, '''Novem Linguae'''!
Having an article draft declined at Articles for Creation can be disappointing. If you are wondering why your article submission was declined, please post a question at the '''[[Wikipedia:WikiProject Articles for creation/Help desk|Articles for creation help desk]]'''. If you have any ''other'' questions about your editing experience, we'd love to help you at the '''[[Wikipedia:Teahouse|Teahouse]]''', a friendly space on Wikipedia where experienced editors lend a hand to help new editors like yourself! See you there! [[User:JoBoGamer|JoBoGamer]] ([[User talk:JoBoGamer|talk]]) 10:57, 18 March 2025 (UTC)</div>
|}<!-- Wikipedia:Teahouse/AfC Invitation -->
[[Category:Wikipedians who have received a Teahouse invitation through AfC]]
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:Test|Test]] (May 21) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This is not the correct place to request new categories. Please follow the instructions at [[Wikipedia:Articles for creation/Categories]]. Thank you.|}}<!--
--
--> The comment the reviewer left was:
{{divbox|blue|3=Test}} Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:Test]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:Test '''Articles for creation help desk''']</span>, on the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:Test '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 02:30, 21 May 2025 (UTC)</div><!--Template:AfC decline-->
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[NovemTest]] has been accepted ==
{{subst:Afc talk|NovemTest|class=|sig=–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 02:42, 21 May 2025 (UTC)}}
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:Mainspace]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in <strong class="error">No contentious topic code has been specified</strong>. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, <strong class="error">No contentious topic code has been specified</strong>. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:Mainspace|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions <strong class="error">No contentious topic code has been specified</strong>.<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> [[User:Chaotic Enby|Chaotic Enby]] ([[User talk:Chaotic Enby|talk]]) 16:42, 3 November 2025 (UTC)
== Deletion discussion about [[Mainspace]] ==
{{subst:AfD-notice-NPF|Mainspace|Mainspace (18th nomination)}}
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 13:49, 30 December 2025 (UTC)
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (May 1) ==
{{subst:Afc reject|full=Draft:NovemTest|reason=n|details=|reason2=|details2=|comment=|sig=yes}} –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:19, 1 May 2026 (UTC)
scash0trsky4aodb8vymwoskmhoun1s
740111
740108
2026-05-01T20:21:43Z
Novem Linguae
49714
Notification: Your [[Draft:NovemTest|Articles for Creation submission]] has been declined
740111
wikitext
text/x-wiki
{{Talk header}}
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (November 27) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This submission appears to [[WP:Spam#Advertisements masquerading as articles|read more like an advertisement]] than an entry in an encyclopedia. Encyclopedia articles need to be written from a [[Wikipedia:Neutral point of view|neutral point of view]], and should refer to a range of [[Wikipedia:Reliable sources|independent, reliable, published sources]], not just to materials produced by the creator of the subject being discussed. This is important so that the article can meet Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] and the [[Wikipedia:Notability|notability]] of the subject can be established. If you still feel that this subject is worthy of inclusion in Wikipedia, please rewrite your submission to comply with these policies.|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:NovemTest]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:NovemTest '''Articles for creation help desk''']</span>, on the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:NovemTest '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 07:20, 27 November 2024 (UTC)</div><!--Template:AfC decline-->
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:Jon Snow (fictional character)|Jon Snow (fictional character)]] (March 18) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by JoBoGamer was:
{{divbox|gray|3=This submission is not suitable for Wikipedia. Please read [[Wikipedia:What Wikipedia is not|"What Wikipedia is not"]] for more information.|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:Jon Snow (fictional character)]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:Jon_Snow_(fictional_character) '''Articles for creation help desk''']</span>, on the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=User_talk:JoBoGamer&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:Jon_Snow_(fictional_character) '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
[[User:JoBoGamer|JoBoGamer]] ([[User talk:JoBoGamer|talk]]) 10:57, 18 March 2025 (UTC)</div><!--Template:AfC decline-->
{| style="margin: 0.4em 2em;"
|- style="vertical-align: top;"
| [[File:WP teahouse logo 2.png|alt=Teahouse logo]]
| <div style="background-color:#e1e6db; color: #393D38; padding: 1em; font-size: 1.1em; border-radius:10px;box-shadow:-2px -2px 1px #8e8a78;">Hello, '''Novem Linguae'''!
Having an article draft declined at Articles for Creation can be disappointing. If you are wondering why your article submission was declined, please post a question at the '''[[Wikipedia:WikiProject Articles for creation/Help desk|Articles for creation help desk]]'''. If you have any ''other'' questions about your editing experience, we'd love to help you at the '''[[Wikipedia:Teahouse|Teahouse]]''', a friendly space on Wikipedia where experienced editors lend a hand to help new editors like yourself! See you there! [[User:JoBoGamer|JoBoGamer]] ([[User talk:JoBoGamer|talk]]) 10:57, 18 March 2025 (UTC)</div>
|}<!-- Wikipedia:Teahouse/AfC Invitation -->
[[Category:Wikipedians who have received a Teahouse invitation through AfC]]
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:Test|Test]] (May 21) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This is not the correct place to request new categories. Please follow the instructions at [[Wikipedia:Articles for creation/Categories]]. Thank you.|}}<!--
--
--> The comment the reviewer left was:
{{divbox|blue|3=Test}} Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:Test]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:Test '''Articles for creation help desk''']</span>, on the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:Test '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 02:30, 21 May 2025 (UTC)</div><!--Template:AfC decline-->
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[NovemTest]] has been accepted ==
{{subst:Afc talk|NovemTest|class=|sig=–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 02:42, 21 May 2025 (UTC)}}
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:Mainspace]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in <strong class="error">No contentious topic code has been specified</strong>. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, <strong class="error">No contentious topic code has been specified</strong>. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:Mainspace|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions <strong class="error">No contentious topic code has been specified</strong>.<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> [[User:Chaotic Enby|Chaotic Enby]] ([[User talk:Chaotic Enby|talk]]) 16:42, 3 November 2025 (UTC)
== Deletion discussion about [[Mainspace]] ==
{{subst:AfD-notice-NPF|Mainspace|Mainspace (18th nomination)}}
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 13:49, 30 December 2025 (UTC)
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (May 1) ==
{{subst:Afc reject|full=Draft:NovemTest|reason=n|details=|reason2=|details2=|comment=|sig=yes}} –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:19, 1 May 2026 (UTC)
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (May 1) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This draft reads like an advertisement. Wikipedia is an encyclopedia, [[Wikipedia:Spam#Advertisements masquerading as articles|not a platform for promotion or marketing]]. Drafts that are [[Wikipedia:G11|exclusively promotional may be deleted without notice]].
Wikipedia articles must be written neutrally in a formal, impersonal, and dispassionate way. They should not read like a blog post, advertisement, or fan page. Rewrite the draft to remove:
* ''promotional language'': see [[Wikipedia:Manual of Style/Words to watch|Words to watch]];
* ''personal commentary'': opinions or direct addresses to the reader;
* ''[[Wikipedia:Tone|informal language]].''
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.
If you have a [[Wikipedia:Conflict of interest|conflict of interest]] (e.g. you are the subject, an employee, or a relative) or are [[Wikipedia:Paid-contribution disclosure|being paid to edit]], you must disclose this to comply with Wikipedia's [[Wikipedia:Terms of Use|Terms of Use]].|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:NovemTest]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:NovemTest '''Articles for creation help desk''']</span>, on the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:NovemTest '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:21, 1 May 2026 (UTC)</div><!--Template:AfC decline-->
e73us5kmag77ahgbye57u5y1q6031da
740114
740111
2026-05-01T20:23:54Z
Novem Linguae
49714
Notification: Your [[Draft:NovemTest|Articles for Creation submission]] has been declined
740114
wikitext
text/x-wiki
{{Talk header}}
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (November 27) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This submission appears to [[WP:Spam#Advertisements masquerading as articles|read more like an advertisement]] than an entry in an encyclopedia. Encyclopedia articles need to be written from a [[Wikipedia:Neutral point of view|neutral point of view]], and should refer to a range of [[Wikipedia:Reliable sources|independent, reliable, published sources]], not just to materials produced by the creator of the subject being discussed. This is important so that the article can meet Wikipedia's [[Wikipedia:Verifiability|verifiability policy]] and the [[Wikipedia:Notability|notability]] of the subject can be established. If you still feel that this subject is worthy of inclusion in Wikipedia, please rewrite your submission to comply with these policies.|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:NovemTest]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:NovemTest '''Articles for creation help desk''']</span>, on the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:NovemTest '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 07:20, 27 November 2024 (UTC)</div><!--Template:AfC decline-->
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:Jon Snow (fictional character)|Jon Snow (fictional character)]] (March 18) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by JoBoGamer was:
{{divbox|gray|3=This submission is not suitable for Wikipedia. Please read [[Wikipedia:What Wikipedia is not|"What Wikipedia is not"]] for more information.|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:Jon Snow (fictional character)]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:Jon_Snow_(fictional_character) '''Articles for creation help desk''']</span>, on the <span class="plainlinks">[//test.wikipedia.org/w/index.php?title=User_talk:JoBoGamer&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:Jon_Snow_(fictional_character) '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
[[User:JoBoGamer|JoBoGamer]] ([[User talk:JoBoGamer|talk]]) 10:57, 18 March 2025 (UTC)</div><!--Template:AfC decline-->
{| style="margin: 0.4em 2em;"
|- style="vertical-align: top;"
| [[File:WP teahouse logo 2.png|alt=Teahouse logo]]
| <div style="background-color:#e1e6db; color: #393D38; padding: 1em; font-size: 1.1em; border-radius:10px;box-shadow:-2px -2px 1px #8e8a78;">Hello, '''Novem Linguae'''!
Having an article draft declined at Articles for Creation can be disappointing. If you are wondering why your article submission was declined, please post a question at the '''[[Wikipedia:WikiProject Articles for creation/Help desk|Articles for creation help desk]]'''. If you have any ''other'' questions about your editing experience, we'd love to help you at the '''[[Wikipedia:Teahouse|Teahouse]]''', a friendly space on Wikipedia where experienced editors lend a hand to help new editors like yourself! See you there! [[User:JoBoGamer|JoBoGamer]] ([[User talk:JoBoGamer|talk]]) 10:57, 18 March 2025 (UTC)</div>
|}<!-- Wikipedia:Teahouse/AfC Invitation -->
[[Category:Wikipedians who have received a Teahouse invitation through AfC]]
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:Test|Test]] (May 21) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This is not the correct place to request new categories. Please follow the instructions at [[Wikipedia:Articles for creation/Categories]]. Thank you.|}}<!--
--
--> The comment the reviewer left was:
{{divbox|blue|3=Test}} Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:Test]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:Test '''Articles for creation help desk''']</span>, on the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:Test '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 02:30, 21 May 2025 (UTC)</div><!--Template:AfC decline-->
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[NovemTest]] has been accepted ==
{{subst:Afc talk|NovemTest|class=|sig=–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 02:42, 21 May 2025 (UTC)}}
==[[Wikipedia:Criteria for speedy deletion|Speedy deletion]] nomination of [[:Mainspace]]==
[[File:Information icon4.svg|48px|left|alt=|link=]]
Thank you for your interest in <strong class="error">No contentious topic code has been specified</strong>. Due to past disruption, a [[WP:GS|special set of rules]] apply to anyone editing in this topic area. Specifically, <strong class="error">No contentious topic code has been specified</strong>. Since you currently do not meet the requirements to contribute to this topic area, the page you created has been nominated for speedy deletion under [[WP:G5|G5]].
If you think this page does not fall under the general sanctions described, you may '''contest the nomination''' by [[:Mainspace|visiting the page]] and clicking the button labelled "Contest this speedy deletion". This will give you the opportunity to explain why you believe the page should not be deleted. However, be aware that once a page is tagged for speedy deletion, it may be deleted without delay. Please do not remove the speedy deletion tag from the page yourself, but do not hesitate to add information in line with [[Wikipedia:List of policies|Wikipedia's policies and guidelines]]. Additionally you may wish to read about the topic restrictions <strong class="error">No contentious topic code has been specified</strong>.<!-- Template:Db-gs-notice --><!-- Template:Db-csd-notice-custom --> [[User:Chaotic Enby|Chaotic Enby]] ([[User talk:Chaotic Enby|talk]]) 16:42, 3 November 2025 (UTC)
== Deletion discussion about [[Mainspace]] ==
{{subst:AfD-notice-NPF|Mainspace|Mainspace (18th nomination)}}
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 13:49, 30 December 2025 (UTC)
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (May 1) ==
{{subst:Afc reject|full=Draft:NovemTest|reason=n|details=|reason2=|details2=|comment=|sig=yes}} –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:19, 1 May 2026 (UTC)
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (May 1) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This draft reads like an advertisement. Wikipedia is an encyclopedia, [[Wikipedia:Spam#Advertisements masquerading as articles|not a platform for promotion or marketing]]. Drafts that are [[Wikipedia:G11|exclusively promotional may be deleted without notice]].
Wikipedia articles must be written neutrally in a formal, impersonal, and dispassionate way. They should not read like a blog post, advertisement, or fan page. Rewrite the draft to remove:
* ''promotional language'': see [[Wikipedia:Manual of Style/Words to watch|Words to watch]];
* ''personal commentary'': opinions or direct addresses to the reader;
* ''[[Wikipedia:Tone|informal language]].''
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.
If you have a [[Wikipedia:Conflict of interest|conflict of interest]] (e.g. you are the subject, an employee, or a relative) or are [[Wikipedia:Paid-contribution disclosure|being paid to edit]], you must disclose this to comply with Wikipedia's [[Wikipedia:Terms of Use|Terms of Use]].|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:NovemTest]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:NovemTest '''Articles for creation help desk''']</span>, on the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:NovemTest '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:21, 1 May 2026 (UTC)</div><!--Template:AfC decline-->
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (May 1) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This draft reads like an advertisement. Wikipedia is an encyclopedia, [[Wikipedia:Spam#Advertisements masquerading as articles|not a platform for promotion or marketing]]. Drafts that are [[Wikipedia:G11|exclusively promotional may be deleted without notice]].
Wikipedia articles must be written neutrally in a formal, impersonal, and dispassionate way. They should not read like a blog post, advertisement, or fan page. Rewrite the draft to remove:
* ''promotional language'': see [[Wikipedia:Manual of Style/Words to watch|Words to watch]];
* ''personal commentary'': opinions or direct addresses to the reader;
* ''[[Wikipedia:Tone|informal language]].''
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.
If you have a [[Wikipedia:Conflict of interest|conflict of interest]] (e.g. you are the subject, an employee, or a relative) or are [[Wikipedia:Paid-contribution disclosure|being paid to edit]], you must disclose this to comply with Wikipedia's [[Wikipedia:Terms of Use|Terms of Use]].|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:NovemTest]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:NovemTest '''Articles for creation help desk''']</span>, on the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:NovemTest '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:23, 1 May 2026 (UTC)</div><!--Template:AfC decline-->
nkyidxoszwe9cet4qrzhnk8npfxcazb
740115
740114
2026-05-01T20:24:08Z
Novem Linguae
49714
Blanked the page
740115
wikitext
text/x-wiki
phoiac9h4m842xq45sp7s6u21eteeq1
740118
740115
2026-05-01T20:24:49Z
Novem Linguae
49714
Notification: Your [[Draft:NovemTest|Articles for Creation submission]] has been declined
740118
wikitext
text/x-wiki
== Your submission at [[Wikipedia:Articles for creation|Articles for creation]]: [[Draft:NovemTest|NovemTest]] (May 1) ==
<div style="border: solid 1px #FCC; background-color: #F8EEBC; padding: 0.5em 1em; color: #000; margin: 1.5em; width: 90%;"> [[File:AFC-Logo_Decline.svg|50px|left]]Your recent article submission to [[Wikipedia:Articles for creation|Articles for Creation]] has been reviewed. Unfortunately, it has not been accepted at this time.<nowiki> </nowiki>The reason left by Novem Linguae was:
{{divbox|gray|3=This draft reads like an advertisement. Wikipedia is an encyclopedia, [[Wikipedia:Spam#Advertisements masquerading as articles|not a platform for promotion or marketing]]. Drafts that are [[Wikipedia:G11|exclusively promotional may be deleted without notice]].
Wikipedia articles must be written neutrally in a formal, impersonal, and dispassionate way. They should not read like a blog post, advertisement, or fan page. Rewrite the draft to remove:
* ''promotional language'': see [[Wikipedia:Manual of Style/Words to watch|Words to watch]];
* ''personal commentary'': opinions or direct addresses to the reader;
* ''[[Wikipedia:Tone|informal language]].''
Instead, only [[Wikipedia:Writing better articles#Information style and tone|summarize in your own words a range of independent, reliable, published sources]] that discuss the subject.
If you have a [[Wikipedia:Conflict of interest|conflict of interest]] (e.g. you are the subject, an employee, or a relative) or are [[Wikipedia:Paid-contribution disclosure|being paid to edit]], you must disclose this to comply with Wikipedia's [[Wikipedia:Terms of Use|Terms of Use]].|}}<!--
--
--> Please check the submission for any additional comments left by the reviewer. You are encouraged to edit the submission to address the issues raised and resubmit ''after they have been resolved''.
{{clear}}
* If you would like to continue working on the submission, go to [[Draft:NovemTest]] and click on the "Edit" tab at the top of the window.
* If you do not edit your draft in the next 6 months, it will be considered abandoned and [[Wikipedia:Criteria for speedy deletion#G13. Abandoned drafts and Articles for creation submissions|may be deleted]].
* If you need any assistance, or have experienced any [[Wikipedia:Articles for creation/Scam warning|untoward behavior]] associated with this submission, you can ask for help at the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=Wikipedia:WikiProject_Articles_for_creation/Help_desk/New_question&withJS=MediaWiki:AFCHD-wizard.js&page=Draft:NovemTest '''Articles for creation help desk''']</span>, on the <span class="plainlinks" >[//test.wikipedia.org/w/index.php?title=User_talk:Novem_Linguae&action=edit§ion=new&nosummary=1&preload=Template:AfC_decline/HD_preload&preloadparams%5B%5D=Draft:NovemTest '''reviewer's talk page''']</span> or use [[Wikipedia:IRC help disclaimer|Wikipedia's real-time chat help from experienced editors]].
–[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:24, 1 May 2026 (UTC)</div><!--Template:AfC decline-->
{| style="margin: 0.4em 2em;"
|- style="vertical-align: top;"
| [[File:WP teahouse logo 2.png|alt=Teahouse logo]]
| <div style="background-color:#e1e6db; color: #393D38; padding: 1em; font-size: 1.1em; border-radius:10px;box-shadow:-2px -2px 1px #8e8a78;">Hello, '''Novem Linguae'''!
Having an article draft declined at Articles for Creation can be disappointing. If you are wondering why your article submission was declined, please post a question at the '''[[Wikipedia:WikiProject Articles for creation/Help desk|Articles for creation help desk]]'''. If you have any ''other'' questions about your editing experience, we'd love to help you at the '''[[Wikipedia:Teahouse|Teahouse]]''', a friendly space on Wikipedia where experienced editors lend a hand to help new editors like yourself! See you there! –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:24, 1 May 2026 (UTC)</div>
|}<!-- Wikipedia:Teahouse/AfC Invitation -->
[[Category:Wikipedians who have received a Teahouse invitation through AfC]]
99t3h5sld9bsu5h4psw0a35v4ox7dfz
User:SongVĩ.Bot II
2
124239
740038
739887
2026-05-01T17:00:15Z
SongVĩ.Bot II
52414
[[User:SongVĩ.Bot II|Task 0]]: Đã 1586 ngày...
740038
wikitext
text/x-wiki
Cập nhật lần cuối: 02-05-2026
Đã 1586 ngày...
qra80wmeq4yrskj1nkcuok9r8tc0nr5
Template:Transclusionless
10
125588
740088
665105
2026-04-09T03:27:09Z
en>Andrybak
0
remove pages like [[Template:Parameter typo documentation/testcases]] from [[Category:Wikipedia transclusionless templates]]
740088
wikitext
text/x-wiki
{{Mbox|text=This template {{Yesno|{{{should|}}}|yes=should|no=may}} have no [[Wikipedia:Transclusion|transclusions]]. This is because {{{reason|it is substituted by a tool or script, it is used as part of a short-term or less active Wikipedia process, or for some other reason}}}.}}<!--
--><includeonly>__EXPECTUNUSEDTEMPLATE__{{#switch: {{SUBPAGENAME}} | testcases |doc |sandbox= |{{module other | [[Category:Wikipedia transclusionless modules]] | [[Category:Wikipedia transclusionless templates]] }}}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
lf3v59v95zajhzsjgic1v3ohe6skaaf
740089
740088
2026-05-01T19:28:21Z
Novem Linguae
49714
1 revision imported from [[:en:Template:Transclusionless]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740088
wikitext
text/x-wiki
{{Mbox|text=This template {{Yesno|{{{should|}}}|yes=should|no=may}} have no [[Wikipedia:Transclusion|transclusions]]. This is because {{{reason|it is substituted by a tool or script, it is used as part of a short-term or less active Wikipedia process, or for some other reason}}}.}}<!--
--><includeonly>__EXPECTUNUSEDTEMPLATE__{{#switch: {{SUBPAGENAME}} | testcases |doc |sandbox= |{{module other | [[Category:Wikipedia transclusionless modules]] | [[Category:Wikipedia transclusionless templates]] }}}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
lf3v59v95zajhzsjgic1v3ohe6skaaf
Template:AfC submission/reject reasons
10
153905
740098
584352
2026-04-04T14:43:56Z
en>Novem Linguae
0
re-add accidentally deleted code
740098
wikitext
text/x-wiki
{{safesubst:<noinclude />#switch:{{safesubst:<noinclude />lc:{{{1}}}}}
|n = The subject [[Wikipedia:Notability|does not meet Wikipedia's criteria for inclusion]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as non-notable]]}}</includeonly>
|e = The subject is [[Wikipedia:What Wikipedia is not|contrary to the purpose of Wikipedia]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as unencyclopedic]]}}</includeonly>
|j = This draft appears to be a joke or a hoax. Wikipedia is an encyclopedia. We do not accept:
* ''[[Wikipedia:Do not create hoaxes|hoaxes]]'': made-up stories presented as fact;
* ''humor'': content intended to be funny rather than informative.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as jokes or hoaxes]]}}</includeonly>
|blp = This draft appears to be an [[Wikipedia:Attack page|attack page]], created primarily to disparage, threaten, or harass a subject. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as attack pages]]}}</includeonly>
|#default = {{{1}}}
}}<noinclude>
{{Documentation}}
<!-- Categories go on the /doc subpage, and interwikis go on Wikidata. -->
</noinclude>
jhr0t6jfwoa6lzr5l7hfcouvemymdqf
740099
740098
2026-05-01T19:43:36Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/reject_reasons]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740098
wikitext
text/x-wiki
{{safesubst:<noinclude />#switch:{{safesubst:<noinclude />lc:{{{1}}}}}
|n = The subject [[Wikipedia:Notability|does not meet Wikipedia's criteria for inclusion]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as non-notable]]}}</includeonly>
|e = The subject is [[Wikipedia:What Wikipedia is not|contrary to the purpose of Wikipedia]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as unencyclopedic]]}}</includeonly>
|j = This draft appears to be a joke or a hoax. Wikipedia is an encyclopedia. We do not accept:
* ''[[Wikipedia:Do not create hoaxes|hoaxes]]'': made-up stories presented as fact;
* ''humor'': content intended to be funny rather than informative.<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as jokes or hoaxes]]}}</includeonly>
|blp = This draft appears to be an [[Wikipedia:Attack page|attack page]], created primarily to disparage, threaten, or harass a subject. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as attack pages]]}}</includeonly>
|#default = {{{1}}}
}}<noinclude>
{{Documentation}}
<!-- Categories go on the /doc subpage, and interwikis go on Wikidata. -->
</noinclude>
jhr0t6jfwoa6lzr5l7hfcouvemymdqf
740102
740099
2026-05-01T19:46:34Z
en>Novem Linguae
0
debug
740102
wikitext
text/x-wiki
{{safesubst:<noinclude />#switch:{{safesubst:<noinclude />lc:{{{1}}}}}
|n = The subject [[Wikipedia:Notability|does not meet Wikipedia's criteria for inclusion]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as non-notable]]}}</includeonly>
|e = The subject is [[Wikipedia:What Wikipedia is not|contrary to the purpose of Wikipedia]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as unencyclopedic]]}}</includeonly>
|j = This draft appears to be a joke or a hoax. Wikipedia is an encyclopedia. We do not accept:
* ''[[Wikipedia:Do not create hoaxes|hoaxes]]'': made-up stories presented as fact;
* ''humor'': content intended to be funny rather than informative.
{{pb}}<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as jokes or hoaxes]]}}</includeonly>
|blp = This draft appears to be an [[Wikipedia:Attack page|attack page]], created primarily to disparage, threaten, or harass a subject. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as attack pages]]}}</includeonly>
|#default = {{{1}}}
}}<noinclude>
{{Documentation}}
<!-- Categories go on the /doc subpage, and interwikis go on Wikidata. -->
</noinclude>
h19clfnjelo0sbf08g0wdls5ek80fav
740103
740102
2026-05-01T19:59:23Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/reject_reasons]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740102
wikitext
text/x-wiki
{{safesubst:<noinclude />#switch:{{safesubst:<noinclude />lc:{{{1}}}}}
|n = The subject [[Wikipedia:Notability|does not meet Wikipedia's criteria for inclusion]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as non-notable]]}}</includeonly>
|e = The subject is [[Wikipedia:What Wikipedia is not|contrary to the purpose of Wikipedia]].<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as unencyclopedic]]}}</includeonly>
|j = This draft appears to be a joke or a hoax. Wikipedia is an encyclopedia. We do not accept:
* ''[[Wikipedia:Do not create hoaxes|hoaxes]]'': made-up stories presented as fact;
* ''humor'': content intended to be funny rather than informative.
{{pb}}<includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as jokes or hoaxes]]}}</includeonly>
|blp = This draft appears to be an [[Wikipedia:Attack page|attack page]], created primarily to disparage, threaten, or harass a subject. <includeonly>{{safesubst:<noinclude />#ifeq:{{{cat|}}}|no||[[Category:AfC submissions rejected as attack pages]]}}</includeonly>
|#default = {{{1}}}
}}<noinclude>
{{Documentation}}
<!-- Categories go on the /doc subpage, and interwikis go on Wikidata. -->
</noinclude>
h19clfnjelo0sbf08g0wdls5ek80fav
Template:AfC submission/reject reasons/doc
10
153906
740100
584354
2026-04-04T11:32:30Z
en>Primefac
0
/* Usage */ upd.
740100
wikitext
text/x-wiki
{{Documentation subpage}}
<!-- Please place categories where indicated at the bottom of this page and interwikis at Wikidata (see [[Wikipedia:Wikidata]]) -->
== Usage ==
Helper template for {{tl|AFC submission/rejected}}, to be used ''only'' by that template. This template is similar to {{tl|AFC submission/comments}}, but with rejection comments instead of decline comments.
These are the possible comments when using {{tlx|AFC submission|D|reject{{=}}yes}}:
{| class="wikitable"
! code || Comment
|-
| ''n'' || {{AFC submission/reject reasons|n|cat=no}}
|-
| ''e'' || {{AFC submission/reject reasons|e|cat=no}}
|-
| ''j'' || {{AFC submission/reject reasons|j|cat=no}}
|-
| ''blp'' || {{AFC submission/reject reasons|blp|cat=no}}
|}
== See also ==
* {{tl|AFC submission}}
<includeonly>{{sandbox other||
<!-- Categories below this line, please; interwikis at Wikidata -->
[[Category:WikiProject Articles for creation]]
}}</includeonly>
cl9tjj04onpcqif6dykwb6ujtsox30t
740101
740100
2026-05-01T19:43:36Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/reject_reasons/doc]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740100
wikitext
text/x-wiki
{{Documentation subpage}}
<!-- Please place categories where indicated at the bottom of this page and interwikis at Wikidata (see [[Wikipedia:Wikidata]]) -->
== Usage ==
Helper template for {{tl|AFC submission/rejected}}, to be used ''only'' by that template. This template is similar to {{tl|AFC submission/comments}}, but with rejection comments instead of decline comments.
These are the possible comments when using {{tlx|AFC submission|D|reject{{=}}yes}}:
{| class="wikitable"
! code || Comment
|-
| ''n'' || {{AFC submission/reject reasons|n|cat=no}}
|-
| ''e'' || {{AFC submission/reject reasons|e|cat=no}}
|-
| ''j'' || {{AFC submission/reject reasons|j|cat=no}}
|-
| ''blp'' || {{AFC submission/reject reasons|blp|cat=no}}
|}
== See also ==
* {{tl|AFC submission}}
<includeonly>{{sandbox other||
<!-- Categories below this line, please; interwikis at Wikidata -->
[[Category:WikiProject Articles for creation]]
}}</includeonly>
cl9tjj04onpcqif6dykwb6ujtsox30t
Draft:NovemTest
118
155404
740067
735309
2026-05-01T19:25:59Z
Novem Linguae
49714
Rejecting submission: j - Topic is a joke or hoax
740067
wikitext
text/x-wiki
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
idkdkwpkqbugy4hnhzk2lt5pmhgyjrn
740104
740067
2026-05-01T19:59:42Z
Novem Linguae
49714
Submitting
740104
wikitext
text/x-wiki
{{AFC submission||ns=118|u=Novem Linguae|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
kizgjzpbk936twvve7ju3b74y4wmr4f
740105
740104
2026-05-01T20:00:13Z
Novem Linguae
49714
Rejecting submission: blp - Topic is an attack page
740105
wikitext
text/x-wiki
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
n9ujol71qda0dbqre7elwtkeohbbfl7
740106
740105
2026-05-01T20:01:37Z
Novem Linguae
49714
Submitting
740106
wikitext
text/x-wiki
{{AFC submission||ns=118|u=Novem Linguae|ts=20260501200137}} <!-- Do not remove this line! -->
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
9rjyzaowicpgcmwte2v0qg5yfkjl3in
740107
740106
2026-05-01T20:19:39Z
Novem Linguae
49714
Rejecting submission: n - Topic is not notable
740107
wikitext
text/x-wiki
{{AFC submission|d|n|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501201939|reject=yes|ts=20260501200137}} <!-- Do not remove this line! -->
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|small=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
7p4rpc1zd1cdb6oy5t1lkanuab2yw3f
740109
740107
2026-05-01T20:21:06Z
Novem Linguae
49714
Submitting
740109
wikitext
text/x-wiki
{{AFC submission||ns=118|u=Novem Linguae|ts=20260501202106}} <!-- Do not remove this line! -->
{{AFC submission|d|n|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501201939|reject=yes|ts=20260501200137}} <!-- Do not remove this line! -->
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|small=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
sue369xi7y2vu1amg4jr4q8eqlrg1lo
740110
740109
2026-05-01T20:21:42Z
Novem Linguae
49714
Declining submission: adv - Submission reads like an advertisement
740110
wikitext
text/x-wiki
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202142|ts=20260501202106}} <!-- Do not remove this line! -->
{{AFC submission|d|n|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501201939|reject=yes|small=yes|ts=20260501200137}} <!-- Do not remove this line! -->
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|small=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
c3xkytpqf5fii9iln4c5sg3gxb4acxs
740112
740110
2026-05-01T20:23:41Z
Novem Linguae
49714
Submitting
740112
wikitext
text/x-wiki
{{AFC submission||ns=118|u=Novem Linguae|ts=20260501202341}} <!-- Do not remove this line! -->
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202142|ts=20260501202106}} <!-- Do not remove this line! -->
{{AFC submission|d|n|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501201939|reject=yes|small=yes|ts=20260501200137}} <!-- Do not remove this line! -->
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|small=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
tqikhcduazgai0wwm5yrx2h2uo3c3n6
740113
740112
2026-05-01T20:23:53Z
Novem Linguae
49714
Declining submission: adv - Submission reads like an advertisement
740113
wikitext
text/x-wiki
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202353|ts=20260501202341}} <!-- Do not remove this line! -->
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202142|small=yes|ts=20260501202106}} <!-- Do not remove this line! -->
{{AFC submission|d|n|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501201939|reject=yes|small=yes|ts=20260501200137}} <!-- Do not remove this line! -->
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|small=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
8urfjwvxve8y6yqbhwmnunhtzxjc644
740116
740113
2026-05-01T20:24:36Z
Novem Linguae
49714
Submitting
740116
wikitext
text/x-wiki
{{AFC submission||ns=118|u=Novem Linguae|ts=20260501202436}} <!-- Do not remove this line! -->
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202353|ts=20260501202341}} <!-- Do not remove this line! -->
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202142|small=yes|ts=20260501202106}} <!-- Do not remove this line! -->
{{AFC submission|d|n|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501201939|reject=yes|small=yes|ts=20260501200137}} <!-- Do not remove this line! -->
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|small=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
1gefpp5srdtd1r2mj28q9a6kf4k3fhr
740117
740116
2026-05-01T20:24:48Z
Novem Linguae
49714
Declining submission: adv - Submission reads like an advertisement
740117
wikitext
text/x-wiki
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202448|ts=20260501202436}} <!-- Do not remove this line! -->
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202353|small=yes|ts=20260501202341}} <!-- Do not remove this line! -->
{{AFC submission|d|adv|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501202142|small=yes|ts=20260501202106}} <!-- Do not remove this line! -->
{{AFC submission|d|n|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501201939|reject=yes|small=yes|ts=20260501200137}} <!-- Do not remove this line! -->
{{AFC submission|d|blp|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501200013|reject=yes|small=yes|ts=20260501195942}} <!-- Do not remove this line! -->
{{AFC submission|d|j|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260501192559|reject=yes|small=yes|ts=20260314173031}} <!-- Do not remove this line! -->
{{AFC submission|d|creative|ns=118|u=Novem Linguae|decliner=Novem Linguae|declinets=20260314172810|small=yes|ts=20260314172453}} <!-- Do not remove this line! -->
{{AFC submission|d|ns|ns=118|decliner=Novem Linguae|declinets=20260314172427|small=yes|ts=}} <!-- Do not remove this line! -->
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 20:00, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 19:25, 1 May 2026 (UTC)}}
{{AFC comment|1=Test –[[User:Novem Linguae|<span style="color:blue">'''Novem Linguae'''</span>]] <small>([[User talk:Novem Linguae|talk]])</small> 17:24, 14 March 2026 (UTC)}}
----
Test
<!-- Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment Long hidden HTML comment -->
8eqq0qbecn3nh077dy565zl7eddwidu
User:ToprakM/common.css
2
162878
739984
635788
2026-05-01T13:13:05Z
ToprakM
35316
739984
css
text/css
@import url("/index.php?title=User:ToprakM/test.css&action=raw&ctype=text/css");
hcs4gkr48gfyxlxhd7tzkkbd72xl4w9
739986
739984
2026-05-01T13:13:56Z
ToprakM
35316
739986
css
text/css
@import url("/index.php?title=User:ToprakM/test.css&action=raw&ctype=text/css");
background-color: red;
ncvej4djqksahqm0v31gshaguolb3ek
739988
739986
2026-05-01T13:15:34Z
ToprakM
35316
739988
css
text/css
@import url("/index.php?title=User:ToprakM/test.css&action=raw&ctype=text/css");
body, #mw-page-base, #content {
background-color: purple !important;
}
ikwq82wjiniofujilmk3212mxauphxu
739989
739988
2026-05-01T13:15:49Z
ToprakM
35316
739989
css
text/css
@import url("/index.php?title=User:ToprakM/test.css&action=raw&ctype=text/css");
hcs4gkr48gfyxlxhd7tzkkbd72xl4w9
739990
739989
2026-05-01T13:18:14Z
ToprakM
35316
739990
css
text/css
@import url("/index.php?action=raw&ctype=text/css&title=User:ToprakM/test.css");
pyjgosbsb4eve1t5ubs0hzjvgn5lnut
739991
739990
2026-05-01T13:19:41Z
ToprakM
35316
739991
css
text/css
@import url("User:ToprakM/test.css");
9h6vxwhol955rt0yiejhfkqo949np94
739992
739991
2026-05-01T13:21:44Z
ToprakM
35316
739992
css
text/css
@import url("/index.php?action=raw&ctype=text/css&title=User:ToprakM/test.css");
pyjgosbsb4eve1t5ubs0hzjvgn5lnut
739993
739992
2026-05-01T13:23:50Z
ToprakM
35316
739993
css
text/css
@import url("https://test.wikipedia.org/w/index.php?title=User:ToprakM/test.css&action=raw&ctype=text/css");
3vizexvnyyuvsrhzuvpawdjqmjfgar5
739994
739993
2026-05-01T13:24:02Z
ToprakM
35316
739994
css
text/css
@import url("/w/index.php?title=User:ToprakM/test.css&action=raw&ctype=text/css");
di2oflz8nyxe74621d8jafp2k8p9qbh
Template:AfC submission/styles.css
10
166304
740092
658468
2026-03-31T05:10:41Z
en>Oshwah
0
Increase title size to make easier for new users to see and read.
740092
sanitized-css
text/css
/* {{pp-template|small=yes}} */
.afc-submission-header {
font-weight: bold;
font-size: 135%;
text-align: center;
}
.afc-submission-g13 {
text-align: center;
background-color: #fcc;
border: 1px solid #faa;
margin: 0.5em;
padding: 0.5em;
font-weight: bold;
color: #202122;
}
.afc-submission-advice {
border-top: 1px #AAA solid;
margin-top: 5px;
padding-top: 5px
}
.afc-submission-pending {
background-color: #f3eba3;
}
.afc-submission-declined,
.afc-submission-rejected {
background-color: #fee;
}
.afc-submission-reviewing {
background-color: #b1dae8;
}
.afc-submission-declined-comment {
background-color: var(--background-color-neutral-subtle, #f8f9fa);
border: 1px solid #999;
margin: 0.5em;
padding: 1em;
}
@media screen {
html.skin-theme-clientpref-night .afc-submission-pending {
background-color: #381C03;
}
html.skin-theme-clientpref-night .afc-submission-declined,
html.skin-theme-clientpref-night .afc-submission-rejected {
background-color: #300;
}
html.skin-theme-clientpref-night .afc-submission-reviewing {
background-color: #0d1a27;
}
}
@media screen and ( prefers-color-scheme: dark) {
html.skin-theme-clientpref-os .afc-submission-pending {
background-color: #381C03;
}
html.skin-theme-clientpref-os .afc-submission-declined,
html.skin-theme-clientpref-os .afc-submission-rejected {
background-color: #300;
}
html.skin-theme-clientpref-os .afc-submission-reviewing {
background-color: #0d1a27;
}
}
4976kyexml6iq95bq9fow1d1x7ni819
740093
740092
2026-05-01T19:28:21Z
Novem Linguae
49714
1 revision imported from [[:en:Template:AfC_submission/styles.css]]: Copied content from another wiki; see the linked page's history for attribution ([[w:User:Novem Linguae/Scripts/CWWEditSummary.js|CWWEditSummary]])
740092
sanitized-css
text/css
/* {{pp-template|small=yes}} */
.afc-submission-header {
font-weight: bold;
font-size: 135%;
text-align: center;
}
.afc-submission-g13 {
text-align: center;
background-color: #fcc;
border: 1px solid #faa;
margin: 0.5em;
padding: 0.5em;
font-weight: bold;
color: #202122;
}
.afc-submission-advice {
border-top: 1px #AAA solid;
margin-top: 5px;
padding-top: 5px
}
.afc-submission-pending {
background-color: #f3eba3;
}
.afc-submission-declined,
.afc-submission-rejected {
background-color: #fee;
}
.afc-submission-reviewing {
background-color: #b1dae8;
}
.afc-submission-declined-comment {
background-color: var(--background-color-neutral-subtle, #f8f9fa);
border: 1px solid #999;
margin: 0.5em;
padding: 1em;
}
@media screen {
html.skin-theme-clientpref-night .afc-submission-pending {
background-color: #381C03;
}
html.skin-theme-clientpref-night .afc-submission-declined,
html.skin-theme-clientpref-night .afc-submission-rejected {
background-color: #300;
}
html.skin-theme-clientpref-night .afc-submission-reviewing {
background-color: #0d1a27;
}
}
@media screen and ( prefers-color-scheme: dark) {
html.skin-theme-clientpref-os .afc-submission-pending {
background-color: #381C03;
}
html.skin-theme-clientpref-os .afc-submission-declined,
html.skin-theme-clientpref-os .afc-submission-rejected {
background-color: #300;
}
html.skin-theme-clientpref-os .afc-submission-reviewing {
background-color: #0d1a27;
}
}
4976kyexml6iq95bq9fow1d1x7ni819
User:Supertian8
2
166966
740040
739397
2026-05-01T17:11:06Z
Supertian8
67751
gooden linkboxes
740040
wikitext
text/x-wiki
= win =
NaNNaN{{subst:<noinclude/>#ifeq:{{{nxtchap|none}}}|none
|
|{{subst:<noinclude/>!}}nxtchap={{{nxtchap}}}}}
<div style="display: flex; justify-content: flex-end;">
<div>
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -326px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Powered_by_Miraheze_(no_box).svg|link=meta:Special:MyLanguage/Miraheze Meta]]</div>
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -215px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Cc-by-sa.png|link=https://creativecommons.org/licenses/by-sa/4.0/|87px]]</div><!--not the exact same image; this one's a bit more blurry, but I couldn't be bothered finding the exact one-->
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -104px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Poweredby MediaWiki current.svg|link=mediawikiwiki:|87px]]</div></div></div>
<div style="position: relative;position:absolute;bottom:-1036px"> </div><!--adds padding between link boxes and the bottom of the page. kind of a jank way to do it, but it works.-->
<!--above is linked boxes
above is linked boxes
-->
<div style="
position: relative;
position:absolute;
bottom: -900px;
border-left-style: solid; border-left-color: #a7d6f7; border-left-width: 1px;
border-top-style: solid; border-top-color: #a7d6f7; border-top-width: 1px;
border-bottom-style: solid; border-bottom-color: #a7d6f7; border-bottom-width: 1px;
margin-left: -1.76em;
margin-right: -1.7em;
background-color: rgba(255, 255, 255, 0.8);
><!--
--><div style="margin-left: 1.75em;margin-right: 1.7em;opacity: 1;">
=User:Supertian9=
[[File:Example.jpg|thumb|right|Win!]]
According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off of the ground, but the bee of course, flies anyway, because bees don't care what humans think is impossible.<!---
---><div class="catlinks" style="margin-bottom: 1.5em;">[[Special:Categories|Categories]]: <ul style="margin: 0"><li>foo</li><li>bar</li></ul></div><!--
--></div></div><div style="
position: relative;
position:absolute;
bottom: -1012px;
font-size: 12px;
line-height: 2em;
margin-left: -0.3em; <!--Interestingly, on Vector 2010, the notice below the page is not aligned with the text in the page-->
">
This page was last edited on 03 January 2000, at 09:05.
Content is available under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) unless otherwise noted.
[[meta:Special:MyLanguage/Privacy Policy|Privacy Policy]] [[MediaWiki:Noarticletext|About You Shou Yan]] [[MediaWiki:Noarticletext|Disclaimers]] [[meta:Special:MyLanguage/Terms of Use|Terms of Use]] [[meta:Special:MyLanguage/Donate|Donate to Miraheze]] [[:File:ImageShare1.jpg|Mobile view]]
</div>
<div style="width:100px; height:15; background-color: white;border-radius: 2px; border:0.5px solid black;">[[File:Powered_by_Miraheze_(no_box).svg]]</div>
<!--<div style="width:200px; height:50px; fill: white">[[File:Cc-by-sa.png]]</div>
<div style="width:200px; height:50px; fill: white">[[File:Poweredby_MediaWiki_2025.svg]]</div>-->
<div style="width:100px; height:15; background-color: white;">foo</div>
3tspsfovvpjajrqpr4iyblfx11rqh6y
740041
740040
2026-05-01T17:13:16Z
Supertian8
67751
testing second article title
740041
wikitext
text/x-wiki
<h1><big>User:Supertian8</big></h1>
NaNNaN{{subst:<noinclude/>#ifeq:{{{nxtchap|none}}}|none
|
|{{subst:<noinclude/>!}}nxtchap={{{nxtchap}}}}}
<div style="display: flex; justify-content: flex-end;">
<div>
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -326px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Powered_by_Miraheze_(no_box).svg|link=meta:Special:MyLanguage/Miraheze Meta]]</div>
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -215px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Cc-by-sa.png|link=https://creativecommons.org/licenses/by-sa/4.0/|87px]]</div><!--not the exact same image; this one's a bit more blurry, but I couldn't be bothered finding the exact one-->
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -104px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Poweredby MediaWiki current.svg|link=mediawikiwiki:|87px]]</div></div></div>
<div style="position: relative;position:absolute;bottom:-1036px"> </div><!--adds padding between link boxes and the bottom of the page. kind of a jank way to do it, but it works.-->
<!--above is linked boxes
above is linked boxes
-->
<div style="
position: relative;
position:absolute;
bottom: -900px;
border-left-style: solid; border-left-color: #a7d6f7; border-left-width: 1px;
border-top-style: solid; border-top-color: #a7d6f7; border-top-width: 1px;
border-bottom-style: solid; border-bottom-color: #a7d6f7; border-bottom-width: 1px;
margin-left: -1.76em;
margin-right: -1.7em;
background-color: rgba(255, 255, 255, 0.8);
><!--
--><div style="margin-left: 1.75em;margin-right: 1.7em;opacity: 1;">
<h1><big>User:Supertian9</big></h1>
[[File:Example.jpg|thumb|right|Win!]]
According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off of the ground, but the bee of course, flies anyway, because bees don't care what humans think is impossible.<!---
---><div class="catlinks" style="margin-bottom: 1.5em;">[[Special:Categories|Categories]]: <ul style="margin: 0"><li>foo</li><li>bar</li></ul></div><!--
--></div></div><div style="
position: relative;
position:absolute;
bottom: -1012px;
font-size: 12px;
line-height: 2em;
margin-left: -0.3em; <!--Interestingly, on Vector 2010, the notice below the page is not aligned with the text in the page-->
">
This page was last edited on 03 January 2000, at 09:05.
Content is available under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) unless otherwise noted.
[[meta:Special:MyLanguage/Privacy Policy|Privacy Policy]] [[MediaWiki:Noarticletext|About You Shou Yan]] [[MediaWiki:Noarticletext|Disclaimers]] [[meta:Special:MyLanguage/Terms of Use|Terms of Use]] [[meta:Special:MyLanguage/Donate|Donate to Miraheze]] [[:File:ImageShare1.jpg|Mobile view]]
</div>
<div style="width:100px; height:15; background-color: white;border-radius: 2px; border:0.5px solid black;">[[File:Powered_by_Miraheze_(no_box).svg]]</div>
<!--<div style="width:200px; height:50px; fill: white">[[File:Cc-by-sa.png]]</div>
<div style="width:200px; height:50px; fill: white">[[File:Poweredby_MediaWiki_2025.svg]]</div>-->
<div style="width:100px; height:15; background-color: white;">foo</div>
123snybdm2v5oknyf4pve186ze1jk13
740044
740041
2026-05-01T17:17:07Z
Supertian8
67751
more accurate maybe
740044
wikitext
text/x-wiki
<h1><span style="font-size: 30px;">User:Supertian8</span></h1>
NaNNaN{{subst:<noinclude/>#ifeq:{{{nxtchap|none}}}|none
|
|{{subst:<noinclude/>!}}nxtchap={{{nxtchap}}}}}
<div style="display: flex; justify-content: flex-end;">
<div>
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -326px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Powered_by_Miraheze_(no_box).svg|link=meta:Special:MyLanguage/Miraheze Meta]]</div>
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -215px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Cc-by-sa.png|link=https://creativecommons.org/licenses/by-sa/4.0/|87px]]</div><!--not the exact same image; this one's a bit more blurry, but I couldn't be bothered finding the exact one-->
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -104px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Poweredby MediaWiki current.svg|link=mediawikiwiki:|87px]]</div></div></div>
<div style="position: relative;position:absolute;bottom:-1036px"> </div><!--adds padding between link boxes and the bottom of the page. kind of a jank way to do it, but it works.-->
<!--above is linked boxes
above is linked boxes
-->
<div style="
position: relative;
position:absolute;
bottom: -900px;
border-left-style: solid; border-left-color: #a7d6f7; border-left-width: 1px;
border-top-style: solid; border-top-color: #a7d6f7; border-top-width: 1px;
border-bottom-style: solid; border-bottom-color: #a7d6f7; border-bottom-width: 1px;
margin-left: -1.76em;
margin-right: -1.7em;
background-color: rgba(255, 255, 255, 0.8);
><!--
--><div style="margin-left: 1.75em;margin-right: 1.7em;opacity: 1;">
<h1><span style="font-size: 30px;">User:Supertian8</span>/h1>
[[File:Example.jpg|thumb|right|Win!]]
According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off of the ground, but the bee of course, flies anyway, because bees don't care what humans think is impossible.<!---
---><div class="catlinks" style="margin-bottom: 1.5em;">[[Special:Categories|Categories]]: <ul style="margin: 0"><li>foo</li><li>bar</li></ul></div><!--
--></div></div><div style="
position: relative;
position:absolute;
bottom: -1012px;
font-size: 12px;
line-height: 2em;
margin-left: -0.3em; <!--Interestingly, on Vector 2010, the notice below the page is not aligned with the text in the page-->
">
This page was last edited on 03 January 2000, at 09:05.
Content is available under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) unless otherwise noted.
[[meta:Special:MyLanguage/Privacy Policy|Privacy Policy]] [[MediaWiki:Noarticletext|About You Shou Yan]] [[MediaWiki:Noarticletext|Disclaimers]] [[meta:Special:MyLanguage/Terms of Use|Terms of Use]] [[meta:Special:MyLanguage/Donate|Donate to Miraheze]] [[:File:ImageShare1.jpg|Mobile view]]
</div>
<div style="width:100px; height:15; background-color: white;border-radius: 2px; border:0.5px solid black;">[[File:Powered_by_Miraheze_(no_box).svg]]</div>
<!--<div style="width:200px; height:50px; fill: white">[[File:Cc-by-sa.png]]</div>
<div style="width:200px; height:50px; fill: white">[[File:Poweredby_MediaWiki_2025.svg]]</div>-->
<div style="width:100px; height:15; background-color: white;">foo</div>
gy5j2kzlv3tyhx8o41f01ks7cm7hgra
740047
740044
2026-05-01T17:19:25Z
Supertian8
67751
precise and fix h1
740047
wikitext
text/x-wiki
<h1><span style="font-size: 29px;">User:Supertian8</span></h1>
NaNNaN{{subst:<noinclude/>#ifeq:{{{nxtchap|none}}}|none
|
|{{subst:<noinclude/>!}}nxtchap={{{nxtchap}}}}}
<div style="display: flex; justify-content: flex-end;">
<div>
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -326px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Powered_by_Miraheze_(no_box).svg|link=meta:Special:MyLanguage/Miraheze Meta]]</div>
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -215px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Cc-by-sa.png|link=https://creativecommons.org/licenses/by-sa/4.0/|87px]]</div><!--not the exact same image; this one's a bit more blurry, but I couldn't be bothered finding the exact one-->
<div style="
position: relative;
position:absolute;
bottom: -1028px;
margin-left: -104px;
width:104px;
height:42px;
background-color: white;
border-radius: 2px;
border:0.5px solid #72777d;
display: flex;
justify-content: center;
align-items: center;
">[[File:Poweredby MediaWiki current.svg|link=mediawikiwiki:|87px]]</div></div></div>
<div style="position: relative;position:absolute;bottom:-1036px"> </div><!--adds padding between link boxes and the bottom of the page. kind of a jank way to do it, but it works.-->
<!--above is linked boxes
above is linked boxes
-->
<div style="
position: relative;
position:absolute;
bottom: -900px;
border-left-style: solid; border-left-color: #a7d6f7; border-left-width: 1px;
border-top-style: solid; border-top-color: #a7d6f7; border-top-width: 1px;
border-bottom-style: solid; border-bottom-color: #a7d6f7; border-bottom-width: 1px;
margin-left: -1.76em;
margin-right: -1.7em;
background-color: rgba(255, 255, 255, 0.8);
><!--
--><div style="margin-left: 1.75em;margin-right: 1.7em;opacity: 1;">
<h1><span style="font-size: 29px;">User:Supertian8</span></h1>
[[File:Example.jpg|thumb|right|Win!]]
According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off of the ground, but the bee of course, flies anyway, because bees don't care what humans think is impossible.<!---
---><div class="catlinks" style="margin-bottom: 1.5em;">[[Special:Categories|Categories]]: <ul style="margin: 0"><li>foo</li><li>bar</li></ul></div><!--
--></div></div><div style="
position: relative;
position:absolute;
bottom: -1012px;
font-size: 12px;
line-height: 2em;
margin-left: -0.3em; <!--Interestingly, on Vector 2010, the notice below the page is not aligned with the text in the page-->
">
This page was last edited on 03 January 2000, at 09:05.
Content is available under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) unless otherwise noted.
[[meta:Special:MyLanguage/Privacy Policy|Privacy Policy]] [[MediaWiki:Noarticletext|About You Shou Yan]] [[MediaWiki:Noarticletext|Disclaimers]] [[meta:Special:MyLanguage/Terms of Use|Terms of Use]] [[meta:Special:MyLanguage/Donate|Donate to Miraheze]] [[:File:ImageShare1.jpg|Mobile view]]
</div>
<div style="width:100px; height:15; background-color: white;border-radius: 2px; border:0.5px solid black;">[[File:Powered_by_Miraheze_(no_box).svg]]</div>
<!--<div style="width:200px; height:50px; fill: white">[[File:Cc-by-sa.png]]</div>
<div style="width:200px; height:50px; fill: white">[[File:Poweredby_MediaWiki_2025.svg]]</div>-->
<div style="width:100px; height:15; background-color: white;">foo</div>
k0b1t2q6u0j8txh8gdyuo4lnbc42hiw
User:Namoroka/common.js
2
166970
740128
665307
2026-05-02T02:37:19Z
Namoroka
19627
740128
javascript
text/javascript
mw.loader.using(['mediawiki.util'], function () {
mw.loader.load('User:Namoroka/keyboard-layouts.js');
mw.loader.load('User:Namoroka/additional-layouts.js');
mw.loader.load('User:Namoroka/ohi.js');
});
lj3jpwa82nmjctgfgstadt1zchgvvko
740130
740128
2026-05-02T03:18:26Z
Namoroka
19627
Blanked the page
740130
javascript
text/javascript
phoiac9h4m842xq45sp7s6u21eteeq1
User:Namoroka/ohi.js
2
166972
740126
665299
2026-05-02T02:27:17Z
Namoroka
19627
740126
javascript
text/javascript
/** Modified Version (http://ohi.pat.im)
* Modifier : Pat-Al <pat@pat.im> (https://pat.im/910)
* Last Update : 2024/08/07
* Added support for more keyboard layouts by custom keyboard layout tables.
* Added support for Dvorak and Colemak and Workman keyboard layouts.
* Added support for Firefox 12 and higher version.
* Added the on-screen keyboard function.
* Added support for old Hangeul combination by Syllable-Initial-Peak-Final Encoding Approach.
* Added support for multi-key input (moachigi).
**/
/** Original Version (copy - http://ohi.pat.im/org)
* Author : Ho-Seok Ee <hsee@korea.ac.kr>
* Release: 2006/07/18
* Update : 2011/01/22
Copyright (C) Ho-Seok Ee <hsee@korea.ac.kr>. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
The license can be found at http://www.gnu.org/licenses/gpl.txt.
**/
var default_En_type = 'QWERTY';
var default_Ko_type = 'Sin3-P2';
var default_ohi_KBD_type = 'QWERTY';
var default_ohi_KE = 'Ko';
var En_type; // 영문 자판 종류 (ohiChange 함수로 바꿈)
var Ko_type; // 한글 자판 종류 (ohiChange 함수로 바꿈)
var ohi_KBD_type; // 기준 자판 종류 (QWERTY/QWERTZ/AZERTY, ohiChange_KBD_type 함수로 바꿈)
var ohi_KE; // 한글·영문 상태 (Ko: 한글, En: 영문) (ohiChange_KE 함수로 바꿈)
function option() {
var enable_double_final_ext; // 겹받침 확장 배열 쓰기 --> ohiChange_enable_double_final_ext() 함수로 값을 바꿈
var enable_sign_ext; // 세벌식 자판의 기호 확장 배열 쓰기 --> ohiChange_enable_sign_ext() 함수로 값을 바꿈
var force_normal_typing; // 모아치기 자판을 이어치기(일반 타자법)로 치게 하기
var only_NFD_hangeul_encoding; // 한글을 첫가끝 조합형으로만 넣기 (완성형으로 넣지 않기)
var enable_old_hangeul_input; // 옛한글 조합하기
var use_hangeul_compatibility_jamo_when_entering_old_hangeul; // 옛한글을 조합할 때 낱자를 호환 자모로 나타냄
var enable_Sin3_diphthong_key; // 0이면 신세벌식 자판에서 오른쪽 글쇠로 홀소리를 넣을 수 없음
var phonemic_writing; // 풀어쓰기
var phonemic_writing_in_single_phoneme; // 풀어쓰기: 겹낱자를 홑낱자로 풀기
var phonemic_writing_in_halfwidth_letter; // 풀어쓰기: 한글 낱자를 반각 문자로 넣기
var phonemic_writing_initial_ieung_ellipsis; // 풀어쓰기: 첫소리 ㅇ 넣지 않기
var phonemic_writing_adding_space_every_syllable_end; // 풀어쓰기: 낱내자(음절자)마다 빈칸 넣기
var phonemic_writing_directly; // 풀어쓰기: 조합하지 않고 낱자를 바로 넣기
var abbreviation; // 이어치기 자판에서 줄임말 기능 쓰기
var convenience_combination; // 입력 편의를 높이는 추가 낱자 조합 쓰기
var sunalae; // 두벌식 자판 순아래 조합
var show_layout; // 1: 자판 배열표 보이기 / 0: 자판 배열표 감추기 --> show_keyboard_layout() 함수로 값을 바꿈
var turn_off_OHI; // OHI 입력 기능 끄기 (끄더라도 화상 자판은 그대로 쓸 수 있음)
var square_layout; // 화상 배열표를 네모지게 나타내기
}
function converting_option() {
var NCR_text; // HTML 문자 참조 보기
var convert_only_NFD_hangeul_encoding_in_NCR_text; // 첫가끝 조합형으로 들어간 한글만 바꾸기
var direct_typing_text; // 쿼티 배열 기준으로 글쇠값 바꾸기
var extended_hangeul_layout_reflection; // 한글 확장 배열 반영하기
var combination_table_reflection; // 낱자 조합 규칙 반영하기
var combination_table_reflection_priority; // 겹낱자까지 낱자 조합 규칙을 우선 반영하기
var combination_table_reflection_ggeut_ss_exception; // 낱자 조합 규칙을 적용하더라도 받침 ㅆ은 조합해서 넣지 않는 것으로 셈하기
}
function initialize_options() {
var default_enable_double_final_ext = 0;
var default_enable_sign_ext = 1;
var default_force_normal_typing = 0;
var default_only_NFD_hangeul_encoding = 0;
var default_enable_old_hangeul_input = 0;
var default_use_hangeul_compatibility_jamo_when_entering_old_hangeul = 1;
var default_enable_Sin3_diphthong_key = 1;
var default_enable_Sin3_adding_cheos_with_shift_key = 0;
var default_phonemic_writing = 0;
var default_phonemic_writing_in_single_phoneme = 1;
var default_phonemic_writing_in_halfwidth_letter = 0;
var default_phonemic_writing_initial_ieung_ellipsis = 1;
var default_phonemic_writing_adding_space_every_syllable_end = 0;
var default_phonemic_writing_directly = 0;
var default_phonemic_writing_NFD_ggeut_to_cheos = 1;
var default_abbreviation = 0;
var default_convenience_combination = 1;
var default_sunalae = 0;
var default_square_layout = 0;
// En_type, Ko_type 등이 미리 지정되어 있으면 지정된 것으로 초기값을 바꿈
if(typeof En_type != 'undefined') default_En_type = En_type; else En_type = default_En_type;
if(typeof Ko_type != 'undefined') default_Ko_type = Ko_type; else Ko_type = default_Ko_type;
if(typeof ohi_KBD_type != 'undefined') default_ohi_KBD_type = ohi_KBD_type; else ohi_KBD_type = default_ohi_KBD_type;
if(typeof ohi_KE != 'undefined') default_ohi_KE = ohi_KE; else ohi_KE = default_ohi_KE;
if(typeof enable_sign_ext != 'undefined') default_enable_sign_ext = enable_sign_ext;
if(typeof force_normal_typing != 'undefined') default_force_normal_typing = force_normal_typing;
if(typeof phonemic_writing != 'undefined') default_phonemic_writing = phonemic_writing;
if(typeof phonemic_writing_in_single_phoneme != 'undefined') default_phonemic_writing_in_single_phoneme = phonemic_writing_in_single_phoneme;
if(typeof phonemic_writing_in_halfwidth_letter != 'undefined') default_phonemic_writing_in_halfwidth_letter = phonemic_writing_in_halfwidth_letter;
if(typeof phonemic_writing_initial_ieung_ellipsis != 'undefined') default_phonemic_writing_initial_ieung_ellipsis = phonemic_writing_initial_ieung_ellipsis;
if(typeof phonemic_writing_adding_space_every_syllable_end != 'undefined') default_phonemic_writing_adding_space_every_syllable_end = phonemic_writing_adding_space_every_syllable_end;
if(typeof phonemic_writing_directly != 'undefined') default_phonemic_writing_directly = phonemic_writing_directly;
if(typeof square_layout != 'undefined') default_square_layout = square_layout;
option = new option();
option.enable_double_final_ext = default_enable_double_final_ext;
option.enable_sign_ext = default_enable_sign_ext;
option.force_normal_typing = default_force_normal_typing;
option.only_NFD_hangeul_encoding = default_only_NFD_hangeul_encoding;
option.enable_old_hangeul_input = default_enable_old_hangeul_input;
option.use_hangeul_compatibility_jamo_when_entering_old_hangeul = default_use_hangeul_compatibility_jamo_when_entering_old_hangeul;
option.enable_Sin3_diphthong_key = default_enable_Sin3_diphthong_key;
option.enable_Sin3_adding_cheos_with_shift_key = default_enable_Sin3_adding_cheos_with_shift_key;
option.phonemic_writing = default_phonemic_writing;
option.phonemic_writing_in_single_phoneme = default_phonemic_writing_in_single_phoneme;
option.phonemic_writing_in_halfwidth_letter = default_phonemic_writing_in_halfwidth_letter;
option.phonemic_writing_initial_ieung_ellipsis = default_phonemic_writing_initial_ieung_ellipsis;
option.phonemic_writing_adding_space_every_syllable_end = default_phonemic_writing_adding_space_every_syllable_end;
option.phonemic_writing_directly = default_phonemic_writing_directly;
option.phonemic_writing_NFD_ggeut_to_cheos = default_phonemic_writing_NFD_ggeut_to_cheos;
option.abbreviation = default_abbreviation;
option.convenience_combination = default_convenience_combination;
option.sunalae = default_sunalae;
option.turn_off_OHI = 0;
option.show_layout = 1;
option.square_layout = default_square_layout;
converting_option = new converting_option();
converting_option.NCR_text = 0;
converting_option.convert_only_NFD_hangeul_encoding_in_NCR_text = 0;
converting_option.direct_typing_text = 0;
converting_option.extended_hangeul_layout_reflection = 0;
converting_option.combination_table_reflection = 1;
converting_option.combination_table_reflection_priority = 0;
converting_option.combination_table_reflection_ggeut_ss_exception = 1;
}
initialize_options();
var ohiQ = [0,0,0,0,0,0,0,0,0]; // 조합하고 있는 완성형 한글 낱내자의 낱자들을 담는 배열 [첫,첫,첫,가,가,가,끝,끝,끝]
var ohiRQ = [0,0,0,0,0,0,0,0,0]; // 조합하고 있는 완성형 한글 낱내자의 낱자들의 추가 정보를 담는 배열 (보기: 겹홀소리 조합용 홀소리인지, 받침 붙는 홀소리인지)
var prev_ohiQ = [];
var prev_ohiRQ = [];
var backup_ohiQ = []; // 완성형 한글 낱내자를 옛한글 상태로 바꿀 때에 복사해 두는 배열
var backup_ohiRQ = [];
var backspacing_state = 0; // 뒷걸음쇠 처리를 하고 있는지를 알리는 상태 변수 (ohiInsert 함수에 알림)
var prev_cursor_position = -1; // 앞선 상태의 가리키개 자리 (모아치기 자판이나 줄임말 기능으로 넣은 글을 한꺼번에 지울 때 쓰임)
var backup_prev_cursor_position = -1; // 앞선 상태의 가리키개 자리 (모아치기 자판이나 줄임말 기능으로 넣은 글을 한꺼번에 지울 때 쓰임)
var abbreviation_processing_state = 0; // 줄임말 처리를 하고 있는지를 나타내는 변수
var ohiStatus = document.createElement('div');
var ohiTimeout = 0;
var sign_ext_state = 0; // 기호 확장 배열을 쓰고 있는지를 나타냄
var phoneme_input_state = 0 // 풀어쓰기로 넣고 있는지를 나타냄 (ohiInsert 함수에 알림)
var onkeypress_skip = 0; // ohiKeypress() 처리를 건너뛰기 (보기: 오른쪽 숫자판을 눌렀을 때)
var onkeyup_skip = 0; // ohiKeyup() 처리를 건너뛰기
var ohiHangeul3_HanExtKey = 0; // 한글 확장 글쇠가 눌린 상태
var shift_lock = 0; // 한글 타자기 받침 글쇠 눌린 상태
var shift_click = 0; // 배열표에서 윗글쇠 누른 상태
var shiftlock_click = 0; // 배열표에서 Shift Lock을 누른 상태
var browser = '', browser_ver = 0, nu = navigator.userAgent;
var dkey, ukey;
var pressed_keys = []; // 모아친 글쇠들의 값
var prev_pressed_keys = []; // 바로 앞에 모아친 글쇠들의 값
var prev_class = []; // 바로 앞에 모아친 줄임말의 종류(품사 등)
var pressing_keys = 0; // 눌려 있는 글쇠 수
var double_multikey_abbreviated_state = 0; // 줄여넣기를 두 차례 잇달아 했는지를 나타냄
function NFD_stack() { // 먼저 넣은 낱자 정보가 배열의 뒤에 들어가고, 마지막으로 들어온 낱자가 배열의 맨 앞에 들어감
var phoneme = []; // 글쇠로 친 첫가끝 낱자들을 겹낱자로 조합하지 않은 채로 담음
var phoneme_R = []; // 조합하는 첫가끝 낱자들의 추가 정보를 담음 (보기: 겹홀소리 조합용 홀소리인지, 받침 붙는 홀소리인지)
var virtual_phoneme = []; // 가상 낱자
var combined_phoneme = []; // 조합한 첫가끝 낱자들을 담음 (첫+가 또는 첫+가+끝)
}
function initialize_NFD_stack() {
NFD_stack.phoneme = [];
NFD_stack.phoneme_R = [];
NFD_stack.combined_phoneme = [];
}
initialize_NFD_stack();
var ohi_cheos, ohi_ga, ohi_ggeut, ohi_hotbadchim; // OHI에서 쓰는 요즘한글 첫·가·끝 낱자
var unicode_NFD_hangeul_phoneme = [], unicode_cheos = [], unicode_ga = [], unicode_ggeut=[]; // 유니코드 한글 낱자, 유니코드 한글 첫·가·끝 낱자
var unicode_modern_hangeul_phoneme= [], unicode_modern_cheos = [], unicode_modern_ga = [], unicode_modern_ggeut = []; // 유니코드 조합형 한글 낱자, 유니코드 조합형 요즘한글 첫·가·끝 낱자
var compatibility_hangeul_phoneme = [], compatibility_modern_cheos = [], compatibility_ga = [], compatibility_ggeut = []; // 유니코드 한글 호환 자모
var halfwidth_cheos = [], halfwidth_ga = [], halfwidth_ggeut= [];
var current_layout_info=[];
function browser_detect() {
var trident=navigator.userAgent.match(/Trident\/(\d\.\d)/i);
var trident_ver = trident===undefined || !trident ? 0 : parseFloat(trident[1]);
if(nu.indexOf('MSIE')>=0 || trident_ver>=7) {
browser = "MSIE";
if(trident_ver<7) browser_ver = parseFloat(nu.substring(nu.indexOf("MSIE ")+5));
else if(trident_ver==7) browser_ver=11;
}
else if(nu.indexOf('Firefox')>=0) {
browser = "Firefox";
browser_ver = parseFloat(nu.substring(nu.indexOf('Firefox/')+8));
}
else if(nu.indexOf('Chrome')>=0) {
browser = "Chrome";
browser_ver = parseFloat(nu.substring(nu.indexOf('Chrome/')+7));
}
}
function ohiBackspace(f) { // backspace 동작
if(document.selection && browser=='MSIE' && browser_ver<9) {
var s=document.selection.createRange();
s.moveStart('character', -f.value.length);
var pos = s.text.length;
if(f.setSelectionRange) {
f.setSelectionRange(pos-1,pos);
f.text='';
}
else if(f.createTextRange) {
var range = f.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos-length);
range.select();
range.text = '';
var scrollTop = f.scrollTop, scrollLeft = f.scrollLeft, selectionStart = f.selectionStart;
var endText = f.value.substr(f.selectionEnd,f.value.length);
f.value = f.value.substr(0,selectionStart)+String.fromCharCode(c);
var scrollHeight = f.scrollHeight, scrollWidth = f.scrollWidth;
f.value += endText;
if(c==13 && browser=='MSIE' && browser_ver==11 && !endText.length) {
// IE 11에서 뒤에 아무 문자 없을 때 줄을 바꾸면 한글 조합이 안 됨
f.value += String.fromCharCode(32);
}
f.scrollTop = (scrollTop > scrollHeight-f.clientHeight) ? scrollTop : scrollHeight-f.clientHeight;
f.scrollLeft = (scrollLeft > scrollWidth-f.clientWidth) ? scrollLeft : scrollWidth-f.clientWidth;
f.setSelectionRange(m || c<32 ? selectionStart:selectionStart+1, selectionStart+1);
}
}
else {
var bs_start = f.selectionStart;
var bs_end = f.selectionEnd;
if(!bs_end) return;
if(bs_start == bs_end) {
if(!NFD_stack.phoneme.length && prev_cursor_position<0) { // 첫가끝 조합 상태가 아닐 때
// 첫가끝 조합형으로 넣은 한글을 낱내자 단위로 지울 수 있게 낱내자의 낱자, 채움 문자, 방점 수를 셈
var i=0, ggeut=0;
do {
var code = f.value.substr(bs_start-i-1,1).charCodeAt(0);
if(!i && unicode_ggeut.indexOf(code)>=0) {ggeut=1; continue;}
if(i-ggeut==0 && (code==0x1160 || unicode_ga.indexOf(code)>=0)) continue;
if(i-ggeut==0 && (code==0x1160 || unicode_ga.indexOf(code)>=0)) continue;
if(i-ggeut==1 && (code==0x115F || unicode_cheos.indexOf(code)>=0)) continue;
break;
} while(bs_start-(++i));
}
bs_start -= i?i:1;
}
f.value = f.value.substr(0,bs_start)+f.value.substr(bs_end);
f.selectionStart = f.selectionEnd = bs_start;
}
ohiInsert(f,0,0);
}
function ohiHangeul_moa_backspace(f,e) {
if(f.selectionEnd) {
if(prev_cursor_position>=0 && f.selectionEnd > prev_cursor_position) {
initialize_NFD_stack();
while(f.selectionEnd && f.selectionEnd > prev_cursor_position) {if(!ohiHangeul_backspace(f,e)) ohiBackspace(f);}
}
else if(!ohiHangeul_backspace(f,e)) ohiBackspace(f);
}
prev_cursor_position = -1;
prev_class = [];
esc_ext_state();
return true;
}
function ohiCombinedCharacter_backspace(f,e) {
if(e.preventDefault) e.preventDefault();
if(character_combination_queue.length) {
ohiBackspace(f);
character_combination_queue.pop();
if(character_combination_queue.length) {
ohiInput(f,0,character_combination_queue[character_combination_queue.length-1]);
ohiSelection(f,1);
}
return true;
}
return false;
}
function ohiHangeul_backspace(f,e) {
var i,j;
var KE=ohi_KE;
if(e.preventDefault) e.preventDefault();
// Backspace (기호 확장 배열 상태일 때)
if(option.enable_sign_ext && sign_ext_state) {
if(Ko_type.substr(0,4)=='Sin3') ohiBackspace(f);
esc_ext_state();
return true;
}
if(ohiQ[1] || ohiQ[4] || ohiQ[0]&&ohiQ[3]) { // Backspace (요즘한글 조합 상태)
for(i=8; !ohiQ[i];) i--;
backspacing_state=1;
ohiInsert(f,ohiQ[i]=0,ohiQ);
backspacing_state=0;
ohiRQ[i]=0;
esc_ext_state();
return true;
}
if(KE=='Ko' && NFD_stack.phoneme.length) { // 첫가끝 조합 상태
if(!ohiHangeul3_HanExtKey) {
ohiBackspace(f);
if(browser=="MSIE" && browser_ver<9 ) { // IE ~8
i=NFD_stack.combined_phoneme.length-1; while(i--) ohiBackspace(f);
}
var temp_NFD_stack_phoneme = NFD_stack.phoneme.slice();
var temp_NFD_stack_phoneme_R = NFD_stack.phoneme_R.slice();
initialize_NFD_stack();
for(j=0, i=temp_NFD_stack_phoneme.length-1; i>=1; --i)
if(unicode_NFD_hangeul_filler.indexOf(temp_NFD_stack_phoneme[i])>=0) ++j;
if(j!=temp_NFD_stack_phoneme.length-1) { // 채움 문자만 남지 않았으면
for(i=temp_NFD_stack_phoneme.length-1; i>=1; --i) {
NFD_hangeul_input(f,0,(temp_NFD_stack_phoneme_R[i] ? -1:1)*temp_NFD_stack_phoneme[i]);
NFD_stack.phoneme_R[i-1] = temp_NFD_stack_phoneme_R[i];
}
}
}
if(!is_old_hangeul_input() && !option.only_NFD_hangeul_encoding) {
// 아래아 등이 지워졌을 때 첫가끝 코드 조합 상태에서 요즘한글(완성형) 코드로 바꾸기
for(i=0;i<NFD_stack.combined_phoneme.length;++i) {
if(unicode_modern_hangeul_phoneme.indexOf(NFD_stack.combined_phoneme[i])<0 && unicode_NFD_hangeul_filler.indexOf(NFD_stack.combined_phoneme[i])<0) break;
}
if(i==NFD_stack.combined_phoneme.length) { // 첫가끝 방식으로 조합하던 낱자들을 지우고 요즘한글 완성형으로 넣기
ohiBackspace(f);
if(browser=="MSIE" && browser_ver<9 ) {
i=NFD_stack.combined_phoneme.length-1;
while(i--) ohiBackspace(f);
}
initialize_NFD_stack();
ohiQ = backup_ohiQ.slice();
ohiRQ = backup_ohiRQ.slice();
for(i=9;i>=0;--i) {
if(ohiQ[i]) {
ohiQ[i]=0;
ohiRQ[i]=0;
break;
}
}
ohiInsert(f,0,ohiQ);
}
}
esc_ext_state();
return true;
}
return false;
}
function ohiDoubleJamo(a,c,d) {
var i,j;
if(typeof current_layout_info.hangeul_combination_table != 'undefined') { // 낱자 조합 규칙이 따로 있으면
var c1, c2, c3, combination_table = current_layout_info.hangeul_combination_table;
for(i=0; i<combination_table.length; ++i) {
c1 = convert_into_ohi_hangeul_phoneme(parseInt(combination_table[i][0]/0x10000));
c2 = convert_into_ohi_hangeul_phoneme(combination_table[i][0]%0x10000);
c3 = convert_into_ohi_hangeul_phoneme(combination_table[i][1]);
if(!c1 || !c2 || !c3) continue;
if(a==0 && ohi_cheos.indexOf(c3)>=0) j=127;
else if(a==1 && ohi_ga.indexOf(c3)>=0) j=35;
else if(a==2 && ohi_ggeut.indexOf(c3)>=0) j=0;
else continue;
if(c1==c+j && c2==d+j) return c3-c1;
}
return 0;
}
var j=[ // Double Jamos
[ [1,7,18,21,24],1,7,18,21,24 ], // Cho
[ [39,44,49],[31,32,51],[35,36,51],51 ], // Jung
[ [1,4,9,18,21],[1,21],[24,30],[1,17,18,21,28,29,30],[0,21],21 ] // Jong
];
a=j[a];
for(i=a[0].length; c!=a[0][i-1]; i--) if(!i) return i;
for(a=a[i], i=a.length||1; 1; i--) if(!i || d==a || d==a[i-1]) return i;
}
function ohiInsert(f,m,q) { // Insert
// q가 숫자이면 그 부호값에 맞는 유니코드 부호를 넣음
// q가 배열(ohiQ)이면 유니코드 완성형 한글로 넣음
var a,b,c=q,d=m?1:0,g=0,h=0,i=0,j=0,k=0,u=0;
var character_combination_table = find_character_combination_table();
if(!sign_ext_state && c && typeof c == 'number' && unicode_NFD_hangeul_phoneme.indexOf(c)<0 && character_combination_table.length) { // 한글이 아닌 문자를 조합하여 넣기
complete_hangeul_syllable(f);
a = character_combination_table.filter(function(e) {return e[0]==c});
b = character_combination_queue.length ? character_combination_table.filter(function(e) {return e[1]==c && e[0]==character_combination_queue[character_combination_queue.length-1]}) : [];
if(character_combination_queue.length) {
if(b.length && c==b[0][1] && b[0][0]==character_combination_queue[character_combination_queue.length-1]) {
ohiSelection(f,0);
ohiBackspace(f);
ohiInput(f,0,b[0][2]);
character_combination_queue.push(b[0][2]);
ohiSelection(f,1);
return;
}
else { // 조합이 더 이어지지 않을 때
character_combination_queue = [];
ohiSelection(f,0);
}
}
if(!character_combination_queue.length && a.length && c==a[0][0]) {
ohiInput(f,0,c);
ohiSelection(f,1);
character_combination_queue.push(c);
return;
}
}
if(!q) {
ohiQ = ohiRQ = prev_ohiQ = prev_ohiRQ = [0,0,0,0,0,0,0,0,0];
return true;
}
if(q.length!=9) ohiQ = ohiRQ = [0,0,0,0,0,0,0,0,0];
else {
for(a=0;a<9;++a) {
if(q[a]>0) ++h;
if(unicode_NFD_hangeul_phoneme.indexOf(ohiQ[a])>=0) ++u;
}
if(!u) { // 요즘한글 자판으로 요즘낱자 처리 (완성형)
var m=m||'0,0,0,0,0,0,0,0,0', i=q[0]+q[1]+q[2], j=q[3]+q[4]+q[5], k=q[6]+q[7]+q[8];
c=i&&j?0xac00+(i-(i<3?1:i<5?2:i<10?4:i<20?11:12))*588+(j-31)*28+k-(k<8?0:k<19?1:k<25?2:3):0x3130+(i||j||k);
}
else if(!NFD_stack.phoneme.length && !is_old_hangeul_input()) {
// 요즘한글 자판을 쓰는데 옛낱자가 들어오면 첫가끝 조합형으로 바꿈
backup_ohiQ = ohiQ.slice();
backup_ohiRQ = ohiRQ.slice();
if(h>1) ohiBackspace(f);
for(a=0;a<3;++a) {
c=backup_ohiQ[a*3]+backup_ohiQ[a*3+1]+backup_ohiQ[a*3+2];
if(!c) continue;
if(c<158) c+=!a?127:a==1?35:0;
c=convert_into_unicode_hangeul_phoneme(c);
if(c) NFD_hangeul_input(f,0,c);
}
return;
}
}
if((is_phonemic_writing_input() || option.only_NFD_hangeul_encoding && !is_old_hangeul_input()) && !phoneme_input_state && !backspacing_state) {
// 풀어쓰기를 하거나 요즘한글 자판으로 첫가끝 낱자로만 조합할 때
if(is_phonemic_writing_input() && option.phonemic_writing_directly && !option.only_NFD_hangeul_encoding && i+j+k) {
// 낱자 바로 풀기
if(option.phonemic_writing_in_halfwidth_letter && !is_old_hangeul_input()) c=convert_into_halfwidth_hangeul_letter(c);
else c=convert_into_compatibility_hangeul_letter(c);
ohiInsert(f,0,c);
return;
}
else {
for(a=0;a<9;++a) if(prev_ohiQ[a]>0) ++g;
if(g>0 && h<2 || d) {
phoneme_input_state=1;
ohiQ = prev_ohiQ.slice();
if(ohiQ[0]+ohiQ[3]+ohiQ[6]) {
// 두벌식 자판의 도깨비불 상태에서 홀소리가 들어와 앞 낱내의 끝이 가려짐 → 앞 낱내의 받침에 들어간 닿소리를 뒤 낱내의 첫소리로 넘기고 앞 낱내의 조합을 끊음
if(Ko_type.substr(0,2)=='2-' && h&&i&&j)
for(a=8;a>=0;--a) if(ohiQ[a]) { ohiQ[a]=0; break; }
complete_hangeul_syllable(f);
// 낱내자 뒤에 빈칸 넣기 (한글 조합이 새로 이어질 때)
if(is_phonemic_writing_input() && option.phonemic_writing_adding_space_every_syllable_end && h && i+j+k) ohiInsert(f,0,32);
}
ohiQ=[h&&i?i:0,0,0,h&&j?j:0,0,0,h&&k?k:0,0,0];
phoneme_input_state=0;
}
}
}
if(is_moachigi_input() && NFD_stack.phoneme.length && unicode_NFD_hangeul_code.indexOf(c)<0) complete_hangeul_syllable(f);
ohiInput(f,m,c);
prev_ohiQ = ohiQ.slice();
prev_ohiRQ = ohiRQ.slice();
}
function ohiInput(f,m,c) {
if(document.selection && browser=="MSIE" && browser_ver<10 ) { // IE ~9
var s=document.selection.createRange(), t=s.text;
if(t && document.selection.clear) document.selection.clear();
s.text=(m=='0,0,0,0,0,0,0,0,0'||c&&t.length>1?'':t.substr(0,t.length))+String.fromCharCode(c);
if(!c || !m || s.moveStart('character',-1)) s.select();
}
else if(f.selectionEnd+1) {
if(m!='0,0,0,0,0,0,0,0,0' && f.selectionEnd-f.selectionStart==1) f.selectionStart++;
var e=document.createEvent('KeyboardEvent');
if(e.initKeyEvent && !(browser=="Firefox" && browser_ver>=12 ) && browser!="Chrome") { // Gecko
e.initKeyEvent('keypress',0,0,null,0,0,0,0,127,c);
if(c && f.dispatchEvent(e) && m) f.selectionStart--;
} else { // Firefox 12~, Chrome
var scrollTop = f.scrollTop, scrollLeft = f.scrollLeft, selectionStart = f.selectionStart;
var endText = f.value.substr(f.selectionEnd,f.value.length);
f.value = f.value.substr(0,selectionStart)+String.fromCharCode(c);
var scrollHeight = f.scrollHeight, scrollWidth = f.scrollWidth;
f.value += endText;
if(c==13 && browser=='MSIE' && browser_ver==11 && !endText.length) f.value += String.fromCharCode(32); // IE 11에서 뒤에 아무 문자 없을 때 줄을 바꾸면 한글 조합이 안 됨
f.scrollTop = (scrollTop > scrollHeight-f.clientHeight) ? scrollTop : scrollHeight-f.clientHeight;
f.scrollLeft = (scrollLeft > scrollWidth-f.clientWidth) ? scrollLeft : scrollWidth-f.clientWidth;
f.setSelectionRange(m || c<32 ? selectionStart:selectionStart+1, selectionStart+1);
}
}
}
function ohiSelection(f,length) {
if(document.selection && browser=="MSIE" && browser_ver<9) { // IE ~8
}
else if(f.selectionEnd+1) {
var e=document.createEvent('KeyboardEvent');
if(e.initKeyEvent && !(browser=="Firefox" && browser_ver>=12 ) && browser!="Chrome") f.selectionStart-=length; // Gecko
else f.selectionStart=f.selectionEnd-length; // Firefox 12~, Chrome
}
}
function esc_ext_state() { // 기호 확장 배열 또는 한글 확장 배열을 상태에서 기본 배열을 쓰는 상태로 바꿈
var KE = ohi_KE;
if(ohiHangeul3_HanExtKey || sign_ext_state) {
if(KE=='Ko') {
sign_ext_state=0;
ohiHangeul3_HanExtKey=0;
show_keyboard_layout(Ko_type);
}
}
sign_ext_state=0;
ohiHangeul3_HanExtKey=0;
character_combination_queue = [];
}
function change_syllable_from_NFC_to_NFD(f) { // 완성형(NFC) → 첫가끝(NFD) (조합을 막 끝낸 낱내자를 바꿈)
var _ohiQ = ohiQ.slice(), _ohiRQ = ohiRQ.slice();
ohiBackspace(f);
i=ohi_cheos.indexOf(_ohiQ[0]+_ohiQ[1]+_ohiQ[2]+127);
if(i>=0 && _ohiQ[0]+_ohiQ[1]+_ohiQ[2]) ohiInsert(f,0,unicode_cheos[i]);
else ohiInput(f,0,0x115F);
i=ohi_ga.indexOf(_ohiQ[3]+_ohiQ[4]+_ohiQ[5]+35);
if(i>=0 && _ohiQ[3]+_ohiQ[4]+_ohiQ[5]) ohiInsert(f,0,unicode_ga[i]);
else ohiInput(f,0,0x1160);
i=ohi_ggeut.indexOf(_ohiQ[6]+_ohiQ[7]+_ohiQ[8]);
if(i>=0 && _ohiQ[6]+_ohiQ[7]+_ohiQ[8]) ohiInsert(f,0,unicode_ggeut[i]);
}
function change_syllable_from_NFD_to_NFC(f) { // 첫가끝(NFD) → 완성형(NFC) (조합을 막 끝낸 낱내자를 바꿈)
var i,j,k;
if(unicode_modern_cheos.indexOf(NFD_stack.combined_phoneme[1])>=0 && unicode_modern_ga.indexOf(NFD_stack.combined_phoneme[0])>=0
|| unicode_modern_cheos.indexOf(NFD_stack.combined_phoneme[2])>=0 && unicode_modern_ga.indexOf(NFD_stack.combined_phoneme[1])>=0 && unicode_modern_ggeut.indexOf(NFD_stack.combined_phoneme[0])>=0) {
// 요즘낱자 첫+가 또는 첫+가+끝
i=NFD_stack.combined_phoneme.length;
for(j=0;j<i;++j) ohiBackspace(f);
ohiQ = [NFD_stack.combined_phoneme[i-1]-0x1100+11+(NFD_stack.combined_phoneme[i-1]>0x1108 ? 1:0),0,0,
NFD_stack.combined_phoneme[i-2]-0x1161+31,0,0,
i==3 ? (NFD_stack.combined_phoneme[0]-0x11A8+1+(NFD_stack.combined_phoneme[0]>0x11AE ? 1:0)+(NFD_stack.combined_phoneme[0]>0x11B8 ? 1:0)+(NFD_stack.combined_phoneme[0]>0x11BD ? 1:0)):0,0,0
];
ohiInsert(f,0,ohiQ);
if(typeof f.selectionEnd != 'undefined') f.selectionStart = f.selectionEnd;
return;
}
if(!is_old_hangeul_input() && NFD_stack.combined_phoneme.length==2 && (unicode_NFD_hangeul_filler.indexOf(NFD_stack.combined_phoneme[1])>=0 ^ unicode_NFD_hangeul_filler.indexOf(NFD_stack.combined_phoneme[0])>=0)) {
// 옛한글 자판이 아니면서 아래아를 넣는 요즘한글 자판을 쓸 때에 아래아와 아래애를 호환 자모로 바꿈
var NFD_to_compatibility_phoneme_list = [
0x119E, 0x318D, // 아래아(ㆍ)
0x11A1, 0x318E // 아래애(ㆎ)
];
i = NFD_to_compatibility_phoneme_list.indexOf(unicode_NFD_hangeul_filler.indexOf(NFD_stack.combined_phoneme[1])>=0 ? NFD_stack.combined_phoneme[0] : NFD_stack.combined_phoneme[1]);
if(i>=0) {
// 호환 자모로 바꿈
for(j=0;j<NFD_stack.combined_phoneme.length;++j) ohiBackspace(f);
ohiInsert(f,0,NFD_to_compatibility_phoneme_list[i+1]);
}
}
else if(option.use_hangeul_compatibility_jamo_when_entering_old_hangeul && is_old_hangeul_input() && !option.only_NFD_hangeul_encoding
&& ( unicode_NFD_hangeul_filler.indexOf(NFD_stack.combined_phoneme[2])>=0 && unicode_NFD_hangeul_filler.indexOf(NFD_stack.combined_phoneme[1])>=0 && unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0
|| unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0
|| unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 && unicode_NFD_hangeul_filler.indexOf(NFD_stack.combined_phoneme[1])>=0)
) {
// 옛한글을 조합하는 때에 따로 들어간 낱자를 호환 자모로 바꿈
compatibility_jamo = compatibility_cheos.concat(compatibility_hol, compatibility_dah);
compatibility_jamo_to_NFD_phonemes = compatibility_cheos_to_NFD_hotdah.concat(compatibility_hol_to_NFD_hothol, compatibility_dah_to_NFD_hotbadchim);
for(j=0;j<compatibility_jamo_to_NFD_phonemes.length;++j) {
i = compatibility_jamo_to_NFD_phonemes[j][0];
for(k=1;k<compatibility_jamo_to_NFD_phonemes[j].length;++k) {
i = combine_unicode_NFD_hangeul_phoneme(i,compatibility_jamo_to_NFD_phonemes[j][k]);
}
if(i==NFD_stack.combined_phoneme[0] || unicode_NFD_hangeul_filler.indexOf(NFD_stack.combined_phoneme[0])>=0 && i==NFD_stack.combined_phoneme[1]) break;
}
if(j!=compatibility_jamo_to_NFD_phonemes.length) {
for(k=0;k<NFD_stack.combined_phoneme.length;++k) ohiBackspace(f);
ohiInput(f,0,compatibility_jamo[j]);
}
}
}
function combine_unicode_NFD_hangeul_phoneme(c1,c2) { // 유니코드 한글 낱자 조합하기
var i;
var combination_table;
var combined_phoneme;
var layout_info = find_current_layout_info();
if(typeof current_layout_info.moachigi_hangeul_combination_table != 'undefined' && typeof current_layout_info.hangeul_combination_table == 'undefined') {
// 모아치기 자판을 이어치기 방식으로 쓸 때
combination_table = current_layout_info.moachigi_hangeul_combination_table;
for(i=0; i<combination_table.length; ++i) {
if(combination_table[i].phonemes.length!=2) continue;
if(combination_table[i].phonemes.indexOf(c1)<0 || combination_table[i].phonemes.indexOf(c2)<0) continue;
if(c1==c2 && combination_table[i].phonemes[0]!=combination_table[i].phonemes[1]) continue;
combined_phoneme=combination_table[i].char;
break;
}
if(i==combination_table.length) return 0;
return combined_phoneme;
}
else {
// 이어치기 자판
combination_table = is_old_hangeul_input() ? hangeul_combination_table_full : hangeul_combination_table_default;
// 자판 배열 정보에서 지정한 낱자 조합 규칙
if(typeof layout_info.hangeul_combination_table != 'undefined') {
combination_table = layout_info.hangeul_combination_table;
}
if(is_old_hangeul_input() && option.enable_old_hangeul_input) {
if(typeof current_layout_info.old_hangeul_layout_type_name != 'undefined' && typeof find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).hangeul_combination_table != 'undefined')
combination_table = find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).hangeul_combination_table;
}
// 편의를 높이기 위한 낱자 조합을 더함 (옛한글 자판이 아닌 한글 자판)
if(!is_old_hangeul_input() && option.convenience_combination && typeof layout_info.hangeul_convenience_combination_table != 'undefined')
combination_table = layout_info.hangeul_convenience_combination_table.concat(combination_table);
var combined_phoneme=0x10000*c1+c2;
for(i=0; i<combination_table.length; ++i) {
if(combined_phoneme==combination_table[i][0]) {
combined_phoneme=combination_table[i][1];
break;
}
}
if(i==combination_table.length) return 0;
return combined_phoneme;
}
}
function complete_hangeul_syllable(f) {
// 한글 낱내자 조합을 끊고 설정에 따라 한글 부호값을 바꿈
// option.only_NFD_hangeul_encoding==0 : 첫가끝 조합형 낱내자(NFD)를 완성형 낱내자(NFC)로 바꿈
// option.only_NFD_hangeul_encoding==1 : 완성형 낱내자(NFC)를 첫가끝 조합형 낱내자(NFD)로 바꿈
// 수정됨: inputText -> wpTextbox1
if(typeof f == 'undefined' || !f) f = document.getElementById('wpTextbox1');
var c,i,j,k;
if(NFD_stack.phoneme.length) {
// 첫가끝 조합형으로 조합하다가 채움 문자만 남았으면 채움 문자를 지움
for(j=0, i=NFD_stack.phoneme.length-1; i>=0; --i)
if(unicode_NFD_hangeul_filler.indexOf(NFD_stack.phoneme[i])>=0) ++j;
if(j==NFD_stack.phoneme.length)
for(i=0;i<NFD_stack.combined_phoneme.length;++i) ohiBackspace(f);
}
if(ohiQ[0]+ohiQ[3]+ohiQ[6] || NFD_stack.phoneme.length) ohiSelection(f,0);
// 풀어쓰기 처리
if(is_phonemic_writing_input()) convert_syllable_into_phonemes(f);
// 첫가끝(NFD) → 완성형(NFC)
else if(!option.only_NFD_hangeul_encoding && NFD_stack.phoneme.length) change_syllable_from_NFD_to_NFC(f);
// 완성형(NFC) → 첫가끝(NFD)
else if(option.only_NFD_hangeul_encoding && !is_old_hangeul_input() && ohiQ[0]+ohiQ[3]+ohiQ[6]) change_syllable_from_NFC_to_NFD(f);
ohiInsert(f,0,0);
initialize_NFD_stack();
if(!abbreviation_processing_state) prev_cursor_position = -1;
}
function convert_into_ohi_hangeul_phoneme(c) {
// 유니코드의 요즘한글 낱자 코드와 홀소리 호환 낱자 코드를 ohi에서 쓰는 코드로 바꾸기 (옛한글 낱자는 바꾸지 않음)
if(unicode_modern_cheos.indexOf(c)>=0) c=ohi_cheos[unicode_modern_cheos.indexOf(c)];
else if(unicode_modern_ga.indexOf(c)>=0) c=ohi_ga[unicode_modern_ga.indexOf(c)];
else if(unicode_modern_ggeut.indexOf(c)>=0) c=ohi_ggeut[unicode_modern_ggeut.indexOf(c)];
else if(compatibility_ga.indexOf(c)>=0) c=ohi_ga[compatibility_ga.indexOf(c)];
return c;
}
function convert_into_unicode_hangeul_phoneme(c) {
// ohi에서 쓰는 한글 낱자값을 유니코드 조합형 낱자 부호값으로 바꾸기
if(ohi_cheos.indexOf(c)>=0) c=unicode_cheos[ohi_cheos.indexOf(c)];
else if(ohi_ga.indexOf(c)>=0) c=unicode_ga[ohi_ga.indexOf(c)];
else if(ohi_ggeut.indexOf(c)>=0) c=unicode_ggeut[ohi_ggeut.indexOf(c)];
return c;
}
function convert_into_compatibility_hangeul_letter(c) {
// 호환 한글 자모로 바꾸기
c=convert_into_unicode_hangeul_phoneme(c);
old_hangeul_cheos = [0x1140,0x114C,0x1159];
old_hangeul_ga = [0x119E];
old_hangeul_ggeut = [0x11EB,0x11F0,0x11F9];
compatibility_yeshangeul_dah = [0x317F,0x3181,0x3186]; // ㅿ,ㆁ,ㆆ
compatibility_yeshangeul_hol = [0x318D]; // ㆍ
if(unicode_modern_cheos.indexOf(c)>=0) c=compatibility_modern_cheos[unicode_modern_cheos.indexOf(c)];
else if(unicode_modern_ga.indexOf(c)>=0) c=compatibility_ga[unicode_modern_ga.indexOf(c)];
else if(unicode_modern_ggeut.indexOf(c)>=0) c=compatibility_ggeut[unicode_modern_ggeut.indexOf(c)];
else if(old_hangeul_cheos.indexOf(c)>=0) c=compatibility_yeshangeul_dah[old_hangeul_cheos.indexOf(c)];
else if(old_hangeul_ga.indexOf(c)>=0) c=compatibility_yeshangeul_hol[old_hangeul_ga.indexOf(c)];
else if(old_hangeul_ggeut.indexOf(c)>=0) c=compatibility_yeshangeul_dah[old_hangeul_ggeut.indexOf(c)];
return c;
}
function convert_into_halfwidth_hangeul_letter(c) {
c=convert_into_unicode_hangeul_phoneme(c);
if(unicode_modern_cheos.indexOf(c)>=0) c=halfwidth_cheos[unicode_modern_cheos.indexOf(c)];
else if(unicode_modern_ga.indexOf(c)>=0) c=halfwidth_ga[unicode_modern_ga.indexOf(c)];
else if(unicode_modern_ggeut.indexOf(c)>=0) c=halfwidth_ggeut[unicode_modern_ggeut.indexOf(c)];
else if(compatibility_modern_cheos.indexOf(c)>=0) c=halfwidth_cheos[compatibility_modern_cheos.indexOf(c)];
else if(compatibility_ga.indexOf(c)>=0) c=halfwidth_ga[compatibility_ga.indexOf(c)];
else if(compatibility_ggeut.indexOf(c)>=0) c=halfwidth_ggeut[compatibility_ggeut.indexOf(c)];
return c;
}
function convert_into_single_phonemes(combined_phoneme) {
// 겹낱자를 홀낱자들로 바꿈
combined_phoneme = convert_into_unicode_hangeul_phoneme(combined_phoneme);
var i, single_phonemes=[combined_phoneme], test_phonemes;
var combination_table = hangeul_combination_table_default;
if(is_old_hangeul_input()) combination_table = hangeul_combination_table_full;
if(unicode_non_combined_phoneme.indexOf(combined_phoneme)>=0) return single_phonemes;
for(i=0; i<combination_table.length; ++i) {
if(combined_phoneme==combination_table[i][1]) {
single_phonemes.splice(0,1,parseInt(combination_table[i][0]/0x10000),combination_table[i][0]%0x10000);
break;
}
}
if(i!=combination_table.length && is_old_hangeul_input()) {
// 옛한글에 쓰이는 겹낱자인지 살핌
for(i=1;i>=0;--i) {
test_phonemes = convert_into_single_phonemes(single_phonemes[i]);
if(test_phonemes.length>1) single_phonemes.splice(i,1,test_phonemes[0], test_phonemes[1]);
}
}
return single_phonemes;
}
function convert_NFC_into_NFD(NFC_c) {
// 완성형 낱내자 부호값(NFC)을 받아서 첫가끝 조합형 낱자 단위 낱내자 부호값(NFD)으로 바꾸어 돌려줌
if(NFC_c<0xAC00 || NFC_c>0xD7A3) return false;
var i,j,k;
i=parseInt((NFC_c-0xAC00)/588)+0x1100;
j=parseInt((NFC_c-0xAC00)%588/28)+0x1161;
k=(NFC_c-0xAC00)%588%28+0x11A7;
k = k==0x11A7 ? 0 : k;
return [i,j,k];
}
function convert_NFD_into_NFC(NFD_phonemes) {
// 첫가끝 조합형 낱자 단위 낱내자 부호값(NFD)을 받아 완성형 낱내자 단위 부호값(NFC)으로 돌려줌
// 요즘한글에 쓰이지 않는 낱자가 있으면 거짓(false)값을 돌려줌
var p=[], h,i,j,k;
if(NFD_phonemes.length>3 || NFD_phonemes.length<2) return false;
h = unicode_modern_cheos.indexOf(NFD_phonemes[0])<0 ? 0 : 1;
for(i=0;i<NFD_phonemes.length;++i) {
// p의 낱자 차례를 '첫+가' 또는 '첫+가+끝'으로 맞춤
j = h ? i : NFD_phonemes.length-i-1;
p[i]=NFD_phonemes[j];
if(unicode_modern_hangeul_phoneme.indexOf(p[i])<0) return false;
}
if(unicode_modern_cheos.indexOf(p[0])<0 || unicode_modern_ga.indexOf(p[1])<0) return false;
if(p.length==3 && unicode_modern_ggeut.indexOf(p[2])<0) return false;
return 0xAC00+(p[0]-0x1100)*588+(p[1]-0x1161)*28+(p[2]-0x11A8);
}
function ohiRoman(f,e,key) { // Roman keyboard basic_layouts (Dvorak, Colemak)
var c=key;
var layout = find_current_layout();
if(En_type!='QWERTY') c=layout[key-33];
ohiInsert(f,0,c);
}
function ohiSpecialKey(f,e,c) {
if(c==0x1B) { // 글쇠값이 0 또는 escape이면 조합 끊기
complete_hangeul_syllable(f);
return true;
}
if(c==0x0D) { // Enter
complete_hangeul_syllable(f);
ohiInsert(f,0,c);
esc_ext_state();
ohiSelection(f,0);
return true;
}
if(c==0x08) { // Backspace (되걸음쇠)
if(ohiHangeul_backspace(f,e)) return true;
ohiBackspace(f);
return true;
}
return false;
}
function Hangeul2_galmadeuli_selection(a) {
if(typeof a.length == 'undefined') return 0;
if(a.length==1) return a[0];
var i, dah=0, hol=0;
for(i=a.length-1;i>=0;--i) {
c = convert_into_unicode_hangeul_phoneme(a[i]);
if(unicode_cheos.indexOf(c)>=0) dah=c;
else if(unicode_ga.indexOf(c)>=0) hol=c;
}
if(!dah || !hol) return dah+hol;
if(ohiQ[0]&&!ohiQ[3]&&!ohiQ[6] || unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0) return hol;
return dah;
}
function ohiHangeul2(f,e,key) { // 2-Beolsik
var i;
if((Ko_type.indexOf('KSX5002')>=0 || Ko_type=='2-KPS9256') && (key<65 || (key-1)%32>25)) {
complete_hangeul_syllable(f);
ohiInsert(f,0,key);
return;
}
var layout_info = find_current_layout_info();
var layout = find_current_layout();
var c = layout[key-33];
if(typeof c == 'object') c = Hangeul2_galmadeuli_selection(c);
c = convert_into_ohi_hangeul_phoneme(c);
if(special_chars.indexOf(c)>=0)
if(ohiSpecialKey(f,e,c)) return;
if(is_old_hangeul_input() || option.only_NFD_hangeul_encoding || NFD_stack.phoneme.length) {
c = NFD_hangeul2_preprocess(f,e,key);
if(unicode_NFD_hangeul_code.indexOf(c)>=0) {
NFD_hangeul_input(f,key,c);
return;
}
}
if(c==layout[key-33]) {
ohiInsert(f,0,c);
return;
}
if(ohi_cheos.indexOf(c)>=0) c-=127;
else if(ohi_ga.indexOf(c)>=0) c-=35;
else if(ohi_ggeut.indexOf(c)>=0) c-=127;
if(c<31) { // Jaum
if((!ohiQ[7] || !(ohiQ[0]=-1)) && ohiQ[3]) ohiQ[7]=ohiDoubleJamo(2,ohiQ[6],c);
if(!ohiQ[3] || ohiQ[0]<0 || ohiQ[0] && (!ohiQ[6] || !ohiQ[7]) && (ohiQ[6] || c==8 || c==19 || c==25)) {
i = ohiDoubleJamo(0,ohiQ[0],c);
ohiInsert(f,(ohiQ=ohiQ[1]||ohiQ[3]||!i?ohiQ:0),ohiQ=[c,ohiQ?0:i,0,0,0,0,0,0,0]);
}
else if(!ohiQ[0] && ohiQ[3]) {
// 닿소리 없이 홀소리가 들어왔고 닿소리가 눌렸을 때 새 낱내자로 조합하기
complete_hangeul_syllable(f);
ohiInsert(f,0,ohiQ=[c,0,0,0,0,0,0,0,0]);
}
else if(!ohiQ[0] && (ohiQ[0]=c) || (ohiQ[6]=ohiQ[6]||c)) ohiInsert(f,0,ohiQ);
if(ohiQ[7]) ohiQ[7]=c;
}
else { // Moum
if(option.sunalae || Ko_type.substr(0,5)=='2-sun') {
if(ohiQ[3]==c && !ohiQ[1] && !ohiQ[6] && (ohiQ[0]==1 || ohiQ[0]==7 || ohiQ[0]==18 || ohiQ[0]==21 || ohiQ[0]==24)) {
// 홀소리 글쇠를 거듭 눌러 된소리 만들기
ohiQ[1]=1;
ohiInsert(f,0,ohiQ);
return;
}
}
if(option.sunalae || Ko_type=='2-KPS9256' || Ko_type.substr(0,5)=='2-sun' || Ko_type=='2-Gaon26KM') {
if((ohiQ[3]==37 || ohiQ[3]==33) && c==51 && !ohiQ[6]) {
// ㅕ+ㅣ→ㅖ, ㅑ+ㅣ→ㅒ
ohiQ[4]=1;
ohiInsert(f,0,ohiQ);
return;
}
}
if(!option.sunalae && c==66-35 && ohiQ[3]==c && !ohiQ[4] && !ohiQ[6]) { // ㅏ+ㅏ→ ㆍ(아래아)
ohiQ[4]=0x119E-ohiQ[3];
ohiInsert(f,0,ohiQ);
return;
}
if(!option.sunalae && c==86-35 && ohiQ[3]==c && !ohiQ[4] && !ohiQ[6]) { // ㅣ+ㅣ→ ᅟᆢ(쌍아래아)
ohiQ[4]=0x11A2-ohiQ[3];
ohiInsert(f,0,ohiQ);
return;
}
if((!ohiQ[4] || ohiQ[6] || !(ohiQ[3]=-1)) && !ohiQ[6]) ohiQ[4]=ohiDoubleJamo(1,ohiQ[3],c);
if((ohiQ[0] && ohiQ[3]>0 && ohiQ[6]) && (ohiQ[7] || !(ohiQ[7]=ohiQ[6]) || !(ohiQ[6]=0))) {
ohiInsert(f,0,[ohiQ[0],ohiQ[1],0,ohiQ[3],ohiQ[4],0,ohiQ[6],0,0]);
ohiInsert(f,ohiQ,ohiQ=[ohiQ[7],0,0,c,0,0,0,0,0]);
}
else if((!ohiQ[0] || ohiQ[3]) && (!ohiQ[4] || ohiQ[6]) || ohiQ[3]<0) ohiInsert(f,ohiQ,ohiQ=[0,0,0,c,0,0,0,0,0]);
else if(ohiQ[3]=ohiQ[3]||c) ohiInsert(f,0,ohiQ);
}
}
function NFD_hangeul2_preprocess(f,e,key) {
var layout_info = find_current_layout_info();
var layout = find_current_layout();
var combined_phoneme, backup_phoneme, backup_phoneme_R;
var c = layout[key-33];
if(typeof c == 'object') c = Hangeul2_galmadeuli_selection(c);
c = convert_into_unicode_hangeul_phoneme(c);
if(unicode_cheos.indexOf(c)>=0) { // 닿소리일 때
if(unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0) { // 바로 앞에 끝소리가 들어왔다면
combined_phoneme=combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0], unicode_cheos_to_ggeut[unicode_cheos.indexOf(c)]);
if(combined_phoneme) {
// 먼저 들어온 끝소리와 조합되는 닿소리이면 끝소리로 넣음
return unicode_cheos_to_ggeut[unicode_cheos.indexOf(c)];
}
else {
// 먼저 들어온 끝소리와 조합되지 않은 닿소리이면 조합을 끊고 첫소리로 넣음
complete_hangeul_syllable(f);
if(is_phonemic_writing_input() && option.phonemic_writing_adding_space_every_syllable_end) ohiInsert(f,0,32); // 풀어쓰기할 때 낱내자 뒤에 빈칸 넣기 (한글 조합이 새로 이어질 때)
if(!is_old_hangeul_input() && !option.only_NFD_hangeul_encoding) {
c = convert_into_ohi_hangeul_phoneme(c); // 완성형으로 조합하는 요즘한글 자판일 때 호환 자모로 바꿈
}
return c;
}
}
else if(unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 || NFD_stack.phoneme[0]==0x1160) { // 바로 앞에 홀소리 또는 홀소리 채움 문자가 들어왔다면
if(is_old_hangeul_input() || unicode_modern_hangeul_phoneme.indexOf(unicode_cheos_to_ggeut[unicode_cheos.indexOf(c)])>=0) { // 옛한글 자판이거나 요즘낱자에 들어가는 받침이면
c=unicode_cheos_to_ggeut[unicode_cheos.indexOf(c)]; // 끝소리로 넣음
return c;
}
}
else return c;
}
else if(unicode_ga.indexOf(c)>=0 || c==0x1160) { // 홀소리 또는 홀소리 채움 문자일 때
if(unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0) { // 바로 앞에 끝소리가 들어왔다면
backup_phoneme = unicode_ggeut_to_cheos[unicode_ggeut.indexOf(NFD_stack.phoneme[0])];
backup_phoneme_R = NFD_stack.phoneme_R[0];
ohiHangeul_backspace(f,e); // 앞에 넣은 끝소리를 지움
complete_hangeul_syllable(f);
if(is_phonemic_writing_input() && option.phonemic_writing_adding_space_every_syllable_end) ohiInsert(f,0,32); // 풀어쓰기할 때 낱내자 뒤에 빈칸 넣기 (한글 조합이 새로 이어질 때)
if(!is_old_hangeul_input() && !option.only_NFD_hangeul_encoding && unicode_modern_hangeul_phoneme.indexOf(backup_phoneme)>=0 && unicode_modern_hangeul_phoneme.indexOf(c)>=0) {
// 요즘한글 자판이고 앞에 들어온 닿소리와 막 들어온 홀소리가 요즘낱자이면 조합을 끊고 완성형으로 바꿈
ohiInsert(f,0,ohiQ=[convert_into_ohi_hangeul_phoneme(backup_phoneme)-127,0,0,0,0,0,0,0,0]);
c = convert_into_ohi_hangeul_phoneme(c);
}
else {
NFD_hangeul_input(f,key,backup_phoneme); // 앞에 넣은 끝소리를 첫소리로 바꾸어 넣음
NFD_stack.phoneme_R[0]=backup_phoneme_R;
}
}
if(!is_old_hangeul_input() && !option.only_NFD_hangeul_encoding) { // 완성형으로 조합하는 요즘한글 자판일 때
if(unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 && !combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],c)) {
// 앞에 들어온 홀소리와 조합되지 않는 홀소리이면 조합을 끊고 호환 자모로 바꿈
complete_hangeul_syllable(f);
c = convert_into_ohi_hangeul_phoneme(c);
}
}
return c;
}
return c;
}
function seek_ieochigi_abbreviation(abbreviation_table, c1, c2) { // 줄임말 조합을 찾기 (이어치기 자판)
var i, chars=null;
for(i=0; i<abbreviation_table.length; ++i)
if(abbreviation_table[i].phonemes[0]==convert_into_unicode_hangeul_phoneme(c1) && abbreviation_table[i].phonemes[1]==convert_into_unicode_hangeul_phoneme(c2))
return abbreviation_table[i].chars;
return null;
}
function seek_moachigi_abbreviation(abbreviation_table) { // 모아치기 자판의 줄임말 조합을 찾기
var i,j,l=[];
for(i=0; i<abbreviation_table.length; ++i) {
if(double_multikey_abbreviated_state) {
// 줄여넣기 조합을 이미 두 차례 이어서 했을 때
// 이전 글쇠 조합이 지정된 줄임말 글쇠 조합은 건너뜀
if(typeof abbreviation_table[i].prev_keys != 'undefined' && abbreviation_table[i].prev_keys.length) continue;
}
if(pressed_keys.length != abbreviation_table[i].keys.length) continue;
for(j=0;j<abbreviation_table[i].keys.length;++j)
if(pressed_keys.indexOf(abbreviation_table[i].keys[j].charCodeAt(0))<0) break;
if(j!=abbreviation_table[i].keys.length) continue;
// 기호 확장
if(abbreviation_table[i].keys.length==1 && abbreviation_table[i].keys[0]<0) return abbreviation_table[i].chars;
// 줄임말 종류(class)
if(typeof abbreviation_table[i].prev_class != 'undefined' && abbreviation_table[i].prev_class.length) {
if(prev_class.length) {
for(j=0;j<abbreviation_table[i].prev_class.length;++j)
if(prev_class.indexOf(abbreviation_table[i].prev_class[j])>=0) break;
if(j!=abbreviation_table[i].prev_class.length) {
prev_class = typeof abbreviation_table[i].class != 'undefined' ? abbreviation_table[i].class.slice() : [];
prev_pressed_keys = [];
backup_prev_cursor_position = prev_cursor_position;
prev_cursor_position = -1;
return abbreviation_table[i].chars;
}
else continue;
}
else continue;
}
if(typeof abbreviation_table[i].prev_keys == 'undefined' || !abbreviation_table[i].prev_keys.length) {
// 줄여넣기 조합을 이미 두 차례 이어서 했을 때
if(double_multikey_abbreviated_state) {
double_multikey_abbreviated_state = 0;
return abbreviation_table[i].chars;
}
// 이전 글쇠값 정보가 없을 때
if(!prev_pressed_keys.length) {
prev_class = typeof abbreviation_table[i].class != 'undefined' ? abbreviation_table[i].class.slice() : [];
return abbreviation_table[i].chars;
}
}
if(!prev_pressed_keys.length && typeof abbreviation_table[i].prev_keys != 'undefined' && abbreviation_table[i].prev_keys.length) continue;
if(prev_pressed_keys.length && typeof abbreviation_table[i].prev_keys != 'undefined') {
if(prev_pressed_keys.length == abbreviation_table[i].prev_keys.length) {
for(j=0;j<prev_pressed_keys.length;++j)
if(prev_pressed_keys.indexOf(abbreviation_table[i].prev_keys[j].charCodeAt(0))<0) break;
if(j==prev_pressed_keys.length) {
// 앞의 글쇠값과 들어맞을 때
prev_class = typeof abbreviation_table[i].class != 'undefined' ? abbreviation_table[i].class.slice() : [];
prev_pressed_keys = [];
double_multikey_abbreviated_state = 1;
return abbreviation_table[i].chars;
}
}
}
// 앞의 글쇠값과 들어맞지 않으면 후보로 넣음
l.push(i);
}
if(l.length) {
prev_class = typeof abbreviation_table[l[0]].class != 'undefined' ? abbreviation_table[l[0]].class.slice() : [];
return abbreviation_table[l[0]].chars;
}
else return [];
}
function ohiHangeul3_abbreviation(f,key) { // 이어치기 세벌식 자판에서 줄임말 처리
if(!option.abbreviation || typeof current_layout_info.ieochigi_hangeul_abbreviation_table == 'undefined') return 0;
var i,j;
var layout = find_current_layout();
var c = layout[key-33];
var ch, chars;
var ieochigi_hangeul_abbreviation_table=null;
abbreviation_table = current_layout_info.ieochigi_hangeul_abbreviation_table;
if(!abbreviation_table || !abbreviation_table.length) return 0;
if(!NFD_stack.phoneme.length) {
if(ohiQ[6]) ch=ohiQ[6]+ohiQ[7];
else if(ohiQ[3]) ch=ohiQ[3]+ohiQ[4]+35;
else if(ohiQ[0]) ch=ohiQ[0]+ohiQ[1]+127;
chars=seek_ieochigi_abbreviation(abbreviation_table, convert_into_unicode_hangeul_phoneme(ch), convert_into_unicode_hangeul_phoneme(c));
if(chars) {
if(!is_phonemic_writing_input()) ohiBackspace(f);
else {
complete_hangeul_syllable(f);
ohiBackspace(f);
}
insert_chars(f,chars);
return 1;
}
}
return 0;
}
function ohiHangeul3(f,e,key) { // 세벌식 자판 - 낱자 단위 처리
var i, j, c=0;
var sublayout = [];
var extended_sign_layout = [];
var layout_info = find_current_layout_info();
var layout = find_current_layout();
if(!abbreviation_processing_state) {
sublayout = find_sublayout();
extended_sign_layout = find_extended_sign_layout();
prev_cursor_position = -1;
}
if(unicode_cheos.indexOf(key)>=0 || unicode_ga.indexOf(key)>=0 || unicode_ggeut.indexOf(key)>=0) {
// key가 유니코드 한글 낱자일 때
c=key;
}
else if(layout) {
if(typeof layout[key-33]=='object') c=layout[key-33][0];
else c=layout[key-33];
if(ohiHangeul3_HanExtKey && typeof layout_info.extended_hangeul_layout[key-33][ohiHangeul3_HanExtKey-1] == 'number' &&
(layout_info.extended_hangeul_layout[key-33][ohiHangeul3_HanExtKey-1]>=0)) {
// 한글 확장 배열에서 문자 넣기
c = layout_info.extended_hangeul_layout[key-33][ohiHangeul3_HanExtKey-1];
NFD_hangeul_input(f,key,c); // 첫가끝 방식으로 한글 조합하기
esc_ext_state();
return c;
}
}
if(typeof layout_info.extended_hangeul_layout != 'undefined' && typeof layout_info.extended_hangeul_layout[key-33][0] == 'number' && (!ohiHangeul3_HanExtKey && c==-1 || ohiHangeul3_HanExtKey && layout_info.extended_hangeul_layout[key-33][ohiHangeul3_HanExtKey-1]==-1)) { // 한글 확장 배열을 쓰는 상태로 들어가기
++ohiHangeul3_HanExtKey;
if(ohiHangeul3_HanExtKey > layout_info.extended_hangeul_layout[key-33].length) esc_ext_state();
else show_keyboard_layout();
if(ohiHangeul3_HanExtKey) return c;
}
if(special_chars.indexOf(c)>=0)
if(ohiSpecialKey(f,e,c)) return;
if((c>64 && c<91 || c>96 && c<123) && !(option.enable_sign_ext && sign_ext_state && extended_sign_layout)) {
// 아스키 영역의 영문자들을 한글 낱자로 처리하지 않고 그대로 넣기 위함 (기호 확장 배열을 쓰지 않을 때)
if(NFD_stack.phoneme.length) complete_hangeul_syllable(f);
ohiInsert(f,0,c);
return c;
}
if(!abbreviation_processing_state || is_moachigi_input()) {
if(Ko_type.substr(0,1)=='3') {
if(sign_layout_input(f,e,key)) return 0; // 기호 확장 배열
if(c<0) return 0;
}
if(option.only_NFD_hangeul_encoding || is_old_hangeul_input()) { // 첫가끝 방식으로 조합할 때
if(is_galmadeuli_input()) { // 갈마들이 세벌식 자판 (신세벌식 자판, 갈마들이 공세벌식 자판)
c = NFD_galmadeuli_preprocess(f,e,key);
if(c==-1) return 0;
}
else if(Ko_type.substr(1,2)=='t-') { // 타자기 자판
c = hangeul_typewriter(f,key);
if(c<=0) return 0;
}
if(c) {
NFD_hangeul_input(f,key,c); // 첫가끝 조합형으로 한글 낱자 처리하기
return 0;
}
}
else { // 첫가끝 방식을 쓰지 않을 때
c = convert_into_ohi_hangeul_phoneme(c);
if(is_galmadeuli_input()) { // 갈마들이 세벌식 자판
c = NFC_galmadeuli_preprocess(f,e,key);
if(c==-1) return 0;
c = convert_into_ohi_hangeul_phoneme(c);
}
else if(Ko_type.substr(1,2)=='t-') { // 타자기 자판
c = hangeul_typewriter(f,key);
if(c<=0) return 0;
}
}
if(!is_old_hangeul_input() && Ko_type.substr(1,2)!='t-' && !is_moachigi_input() &&
(typeof current_layout_info.hangeul_combination_table != 'undefined' || typeof current_layout_info.moachigi_hangeul_combination_table != 'undefined' || typeof current_layout_info.hangeul_convenience_combination_table != 'undefined')
) {
// 옛한글 자판이 아니고 타자기 자판이 아닐 때 낱자 결합 규칙 적용하기
var ch;
if(ohiQ[6]) ch=ohiQ[6]+ohiQ[7];
else if(ohiQ[3]) ch=ohiQ[3]+ohiQ[4]+35;
else if(ohiQ[0]) ch=ohiQ[0]+ohiQ[1]+127;
ch=combine_unicode_NFD_hangeul_phoneme(convert_into_unicode_hangeul_phoneme(ch),convert_into_unicode_hangeul_phoneme(c));
if(ch) {
if(ohiQ[6]) {
if(!ohiQ[7]) ohiQ[7]=convert_into_ohi_hangeul_phoneme(ch)-ohiQ[6];
else { // 3타로 넣는 받침이 들어갔을 때(순아래 2014 자판의 ㄹ+ㅁ+ㅁ→ㄿ, 이어치기 방식으로 쓰는 모아치기 자판)
ohiQ[8]=convert_into_ohi_hangeul_phoneme(ch)-(ohiQ[6]+ohiQ[7]);
}
}
else if(ohiQ[3]) {
if(!ohiQ[4]) ohiQ[4]=convert_into_ohi_hangeul_phoneme(ch)-ohiQ[3]-35;
else { // 3타로 넣는 홀소리가 들어갔을 때 (이어치기 방식으로 쓰는 모아치기 자판 ㅗ+ㅏ+ㅣ→ㅙ)
ohiQ[5]=convert_into_ohi_hangeul_phoneme(ch)-(ohiQ[3]+ohiQ[4])-35;
}
}
else {
if(!ohiQ[1]) ohiQ[1]=convert_into_ohi_hangeul_phoneme(ch)-ohiQ[0]-127;
else { // 3타로 넣는 첫소리가 들어갔을 때
ohiQ[2]=convert_into_ohi_hangeul_phoneme(ch)-(ohiQ[0]+ohiQ[1])-127;
}
}
ohiInsert(f,0,ohiQ);
return ch;
}
}
// 몇몇 공병우 세벌식 자판에서 첫소리만 들어간 채로 [ 자리 글쇠가 눌렸을 때 아래아를 넣음
if(Ko_type.substr(0,2)=='3-' && Ko_type!='3-sun1990' && Ko_type!='3-91_noshift') {
if(key==0x5B && ( (ohiQ[0]&&!ohiQ[3]&&!ohiQ[6] || unicode_cheos.indexOf(NFD_stack.combined_phoneme[0])>=0 ) || NFD_stack.combined_phoneme[0]==0x119E)) {
c=0x119E;
}
}
// 3-91 조합 순아래 자판
if(Ko_type=='3-91_noshift') {
if(key==0x5B && ( (ohiQ[0]&&ohiQ[3]&&!ohiQ[6] || unicode_ga.indexOf(NFD_stack.combined_phoneme[0])>=0) )) {
// 첫소리와 가운뎃소리까지 들어간 채로 [ 자리 글쇠가 눌렸을 때
c=0x11FF;
}
}
}
if(!c) { // 부호값이 0이면 조합 끊기
complete_hangeul_syllable(f);
return 0;
}
// 요즘한글 자판에서 처음 들어온 옛낱자 처리
if(!is_old_hangeul_input() && (unicode_NFD_hangeul_phoneme.indexOf(convert_into_unicode_hangeul_phoneme(c))>=0 || NFD_stack.phoneme.length)) {
if(!NFD_stack.phoneme.length && unicode_NFD_hangeul_phoneme.indexOf(c)>=0) {
// 처음 들어온 옛낱자(c)를 ohiQ에 넣고 ohiInsert 함수로 넘겨 한글 조합 상태를 완성형에서 첫가끝 조합형으로 바꿈
var unicode_phoneme_list = [unicode_cheos, unicode_ga, unicode_ggeut];
for(i=0;i<3;++i) {
if(unicode_phoneme_list[i].indexOf(c)>=0) {
if(!ohiQ[i*3]) ohiQ[i*3]=c;
else if(!combine_unicode_NFD_hangeul_phoneme(convert_into_unicode_hangeul_phoneme(ohiQ[i*3]+ohiQ[i*3+1]),convert_into_unicode_hangeul_phoneme(c))) {
complete_hangeul_syllable(f);
ohiQ[i*3]=c;
}
else if(!ohiQ[i*3+1]) ohiQ[i*3+1]=c-ohiQ[i*3];
else if(!ohiQ[i*3+2]) ohiQ[i*3+2]=c-ohiQ[i*3]-ohiQ[i*3+1];
ohiInsert(f,0,ohiQ);
break;
}
}
return 0;
}
if(!combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],convert_into_unicode_hangeul_phoneme(c))) {
// 앞 낱자와 조합하지 않는 낱자이면 조합을 끊음
if((unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 || unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0)
&& (unicode_cheos.indexOf(convert_into_unicode_hangeul_phoneme(c))>=0 || unicode_ga.indexOf(convert_into_unicode_hangeul_phoneme(c))>=0)) {
// 홀소리나 받침 뒤에 조합되지 않는 첫소리나 홀소리가 오면 조합을 끊기
complete_hangeul_syllable(f);
if(unicode_modern_hangeul_phoneme.indexOf(convert_into_unicode_hangeul_phoneme(c))<0) { // 옛낱자일 때
ohiInsert(f,0,ohiQ=[unicode_cheos.indexOf(c)>=0 ? c:0,0,0,unicode_ga.indexOf(c)>=0 ? c:0,0,0,0,0,0]);
return 1;
}
}
else if(unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0 && unicode_ggeut.indexOf(convert_into_unicode_hangeul_phoneme(c))>=0) {
// 조합되지 않는 받침이면 조합 끊기
complete_hangeul_syllable(f);
}
}
}
// 요즘한글 자판으로 요즘한글 넣기
if(!NFD_stack.phoneme.length && c>127 && c<158 && c!=147) { // Cho
if(NFD_stack.phoneme.length) ohiSelection(f,0);
i=ohiQ[1]||ohiQ[3]||!ohiDoubleJamo(0,ohiQ[0],c-127);
if(!i) ohiQ=0;
ohiInsert(f,i,ohiQ=[c-127,ohiQ?0:1,0,0,0,0,0,0,0]);
return i;
}
else if(!NFD_stack.phoneme.length && c>65 && c<87) { // Jung
if((!ohiQ[4] || !(ohiQ[3]=-1)) && !(Ko_type.substr(1,2)=='t-' && ohiRQ[3]+ohiRQ[4]>1)) {
ohiQ[4]=ohiDoubleJamo(1,ohiQ[3],c-35);
i=1;
}
else i=0;
if((!ohiQ[0] || ohiQ[3]) && (!ohiQ[4] || ohiQ[6]) || ohiQ[3]<0) {
ohiInsert(f,ohiQ,ohiQ=[0,0,0,c-35,0,0,0,0,0]);
i=0;
}
else if(ohiQ[3]=ohiQ[3]||c-35) {
ohiInsert(f,0,ohiQ);
i=1;
}
return i;
}
else if(!NFD_stack.phoneme.length && c<31) { // Jong
i=0;
if(!current_layout_info.hangeul_combination_table && (!ohiQ[7] || !(ohiQ[6]=-1))) {
ohiQ[7]=ohiDoubleJamo(2,ohiQ[6],c);
if(ohiQ[7]) i=1;
}
if(!ohiQ[0] || !ohiQ[3] || ohiQ[6] && !ohiQ[7] || ohiQ[6]<0 || (Ko_type.substr(0,3)=='4t-' && ohiRQ[3]+ohiRQ[4]>0)) {
ohiInsert(f,ohiQ,ohiQ=[0,0,0,0,0,0,c,0,0]);
i=0;
}
else if(ohiQ[6]=ohiQ[6]||c) {
ohiInsert(f,0,ohiQ);
i=1;
}
return i;
}
if(NFD_stack.phoneme.length) NFD_hangeul_input(f,key,c); // 첫가끝 방식으로 옛한글 조합하기
else ohiInsert(f,0,c);
return 0;
}
function convert_syllable_into_phonemes(f) {
// 낱내자를 낱자로 풀어 넣기 (풀어쓰기)
var c,i,j,k,chars=[];
var single_phonemes=[], hangeul_conversion_function;
if(!(ohiQ[0]+ohiQ[3]+ohiQ[6]) && !NFD_stack.phoneme.length) return;
var _ohiQ = ohiQ.slice();
if(option.phonemic_writing_in_halfwidth_letter && !is_old_hangeul_input()) hangeul_conversion_function = convert_into_halfwidth_hangeul_letter;
else hangeul_conversion_function = convert_into_compatibility_hangeul_letter;
if(is_old_hangeul_input() || option.only_NFD_hangeul_encoding || NFD_stack.phoneme.length && !convert_NFD_into_NFC(NFD_stack.phoneme)) {
// 첫가끝 조합형을 쓸 때
var _phoneme = NFD_stack.phoneme.slice();
var _combined_phoneme = NFD_stack.combined_phoneme.slice();
if(unicode_ggeut.indexOf(_combined_phoneme[0])>=0) j=3;
else if(unicode_ga.indexOf(_combined_phoneme[0])>=0 || _combined_phoneme[0]==0x1160) j=2;
for(i=0; i<j; ++i) ohiBackspace(f);
for(i=_combined_phoneme.length-1;i>=0;--i) {
// 첫소리 ㅇ 넣지 않기 (풀어쓰기)
if(option.phonemic_writing_initial_ieung_ellipsis && _combined_phoneme[i]==0x110B) continue;
// 채움 문자 넣지 않기
if(_combined_phoneme[i]==0x115F || _combined_phoneme[i]==0x1160) continue;
single_phonemes = [];
if(!option.phonemic_writing_in_single_phoneme) single_phonemes.push(_combined_phoneme[i]);
else single_phonemes = convert_into_single_phonemes(_combined_phoneme[i]);
for(j=0;j<single_phonemes.length;++j) {
if(option.only_NFD_hangeul_encoding) NFD_hangeul_single_phoneme_syllable_input(f,single_phonemes[j]);
else {
c = hangeul_conversion_function(single_phonemes[j]);
if(compatibility_hangeul_phoneme.indexOf(c)>=0 || unicode_NFD_hangeul_phoneme.indexOf(c)>=0) ohiInput(f,0,c);
// 호환 자모에 없는 첫소리에 채움 문자를 붙임
if(single_phonemes.length==1 && unicode_NFD_hangeul_phoneme.indexOf(single_phonemes[0]>=0) && unicode_cheos.indexOf(c)>=0) ohiInput(f,0,0x1160);
}
}
initialize_NFD_stack();
}
}
else if(!is_old_hangeul_input() && ohiQ[0]+ohiQ[3]+ohiQ[6]) {
// 요즘한글 배열을 쓸 때
if(ohiQ[0]+ohiQ[3]+ohiQ[6]) {
ohiInsert(f,0,0);
ohiBackspace(f);
}
// 첫소리 ㅇ 넣지 않기
if(option.phonemic_writing_initial_ieung_ellipsis) {
if(_ohiQ[0]+_ohiQ[1]+_ohiQ[2]==23) _ohiQ[0]=_ohiQ[1]=_ohiQ[2]=0;
}
for(i=0;i<3;++i) {
if(!_ohiQ[i*3]) continue;
k=_ohiQ[i*3]+_ohiQ[i*3+1]+_ohiQ[i*3+2];
c=k+(i==0?127:i==1?35:0);
if(option.phonemic_writing_in_single_phoneme) {
// 겹낱자를 홑낱자로 풀어 넣기
single_phonemes = convert_into_single_phonemes(c);
if(single_phonemes.length) {
for(j=0;j<single_phonemes.length;++j) {
if(option.only_NFD_hangeul_encoding) NFD_hangeul_single_phoneme_syllable_input(f,single_phonemes[j]);
else ohiInsert(f,0,hangeul_conversion_function(single_phonemes[j]));
}
continue;
}
}
c = 0x3130+_ohiQ[i*3]+_ohiQ[i*3+1]+_ohiQ[i*3+2];
if(option.phonemic_writing_in_halfwidth_letter) c = convert_into_halfwidth_hangeul_letter(c);
ohiInsert(f,0,c);
}
}
}
function ohiHangeul3_moa(f,e) { // 모아치기 세벌식 자판 처리
var i,j,k,l,m;
var c;
var layout=current_layout_info.layout;
var extended_sign_layout;
var combination_table;
var pressed_chars = [];
var temp_pressed_chars = [];
var backup_prev_pressed_keys = [];
var special_keys = [32,13,8]; // 사이띄개(32), 줄바꾸개(13), 뒷걸음쇠(8)
var chars=[];
var cheos = [], ga = [], ggeut = [];
var front_etc = [], rear_etc = [];
var front_special = [], rear_special = [];
var necessary_backspaces_cheos=0;
var necessary_backspaces_ga=0;
var necessary_backspaces_ggeut=0;
var necessary_backspaces_sign=0;
if(option.enable_sign_ext && typeof current_layout_info.extended_sign_layout != 'undefined' && sign_ext_state) {
// 기호 확장 배열에서 기호를 넣음
if(sign_layout_input(f,e,pressed_keys[0])) pressed_keys.splice(0,1);
if(pressed_keys.length>1) esc_ext_state();
}
for(i=0;i<pressed_keys.length;++i) {
// 특수 글쇠를 추림
if(special_keys.indexOf(pressed_keys[i])>=0) {
if(!i) front_special.push(pressed_keys[0]);
else rear_special.push(pressed_keys[i]);
pressed_keys.splice(i,1);
}
else pressed_chars.push(convert_into_unicode_hangeul_phoneme(layout[pressed_keys[i]-33]));
}
if(typeof current_layout_info.moachigi_multikey_abbreviation_table != 'undefined') {
// 모아치기 글쇠 기준 줄임말·예외 조합 (모아치기 조합 가운데 가장 먼저 적용됨)
combination_table = current_layout_info.moachigi_multikey_abbreviation_table;
backup_prev_pressed_keys = prev_pressed_keys.slice();
backup_prev_cursor_position = -1;
chars = seek_moachigi_abbreviation(combination_table);
if(chars.length==1 && combination_table[i].chars[0]<0 && combination_table[i].chars[0]>=-3) {
// 부호값이 -1 ~ -3이면 기호 확장 배열 상태로 들어감
sign_layout_input(f,e,combination_table[i].chars[0]);
return;
}
if(chars.length) {
// 줄임말 조합을 글쇠 조합으로 이어서 하는 때에 먼저 들어간 줄임말을 지우고 다음 줄임말을 넣음
if(backup_prev_pressed_keys.length && !prev_pressed_keys.length && prev_cursor_position > -1) ohiHangeul_moa_backspace(f,e);
insert_chars(f,chars);
if(backup_prev_cursor_position > -1) prev_cursor_position = backup_prev_cursor_position;
return;
}
}
if(typeof current_layout_info.moachigi_hangeul_abbreviation_table != 'undefined') {
// 모아치기 한글 낱자 기준 줄임말·예외 조합
combination_table = current_layout_info.moachigi_hangeul_abbreviation_table;
for(i=0;i<combination_table.length;++i) {
if(pressed_chars.length != combination_table[i].phonemes.length) continue;
for(j=0;j<combination_table[i].phonemes.length;++j) {
if(pressed_chars.indexOf(combination_table[i].phonemes[j])<0) break;
}
if(j!=combination_table[i].phonemes.length) continue;
insert_chars(f,combination_table[i].chars);
return;
}
}
chars = [];
if(typeof current_layout_info.moachigi_hangeul_combination_table != 'undefined') {
// 모아치기 한글 낱자 조합 규칙 (낱자 차례를 따지지 않음)
combination_table = current_layout_info.moachigi_hangeul_combination_table;
for(i=0;i<combination_table.length;++i) {
temp_pressed_chars = pressed_chars.slice();
for(j=0;j<combination_table[i].phonemes.length;++j) {
k=temp_pressed_chars.indexOf(combination_table[i].phonemes[j]);
if(k<0) break;
temp_pressed_chars.splice(k,1);
}
if(j!=combination_table[i].phonemes.length) continue;
chars.push(combination_table[i].char);
for(j=0;j<combination_table[i].phonemes.length;++j) {
k=pressed_chars.indexOf(combination_table[i].phonemes[j]);
pressed_keys.splice(k,1);
pressed_chars.splice(k,1);
}
}
}
for(i=0;i<pressed_keys.length;++i) {
if(special_keys.indexOf(pressed_keys[i])>=0) c=pressed_keys[i];
else c=convert_into_unicode_hangeul_phoneme(layout[pressed_keys[i]-33]);
chars.push(c);
}
for(i=0;i<front_special.length;++i) {
c=front_special[i];
if(c==8) { // 뒷걸음쇠(backspace)
if(ohiHangeul_moa_backspace(f,e)) continue;
if(e.preventDefault) e.preventDefault();
ohiBackspace(f);
}
else ohiInsert(f,0,c);
if(c==13) complete_hangeul_syllable(f); // 줄바꾸개(enter)
}
for(i=0;i<chars.length;++i) {
c=chars[i];
if(!i && special_keys.indexOf(c)>=0) {
special.push(c);
continue;
}
if(unicode_cheos.indexOf(c)>=0) cheos.push(c);
else if(unicode_ga.indexOf(c)>=0) ga.push(c);
else if(unicode_ggeut.indexOf(c)>=0) ggeut.push(c);
else {
if(!cheos.length && !ga.length && !ggeut.length) front_etc.push(c);
else rear_etc.push(c);
}
}
prev_cursor_position = -1;
for(i=0;i<front_etc.length;++i) ohiInsert(f,0,front_etc[i]);
insert_chars(f,cheos.concat(ga,ggeut));
for(i=0;i<rear_etc.length;++i) {
prev_cursor_position = -1;
ohiInsert(f,0,rear_etc[i]);
}
for(i=0;i<rear_special.length;++i) {
c=rear_special[i];
prev_cursor_position = -1;
if(c==8) { // 뒷걸음쇠(backspace)
if(!ohiHangeul_moa_backspace(f,e)) continue;
if(e.preventDefault) e.preventDefault();
ohiBackspace(f);
}
else ohiInsert(f,0,c);
if(c==13) complete_hangeul_syllable(f); // 줄바꾸개(enter)
}
pressed_keys = [];
return;
}
function insert_chars(f,combination_table_chars) { // 여러 문자를 넣음 (줄임말을 넣을 때)
if(typeof combination_table_chars == 'undefined' || typeof combination_table_chars.length == 'undefined') return;
var chars = combination_table_chars.slice();
var a=[],h=0,i,j,k,l;
abbreviation_processing_state=1;
if(is_phonemic_writing_input() && option.phonemic_writing_in_single_phoneme) {
// 겹낱자를 홑낱자로 풀어서 풀어쓰기할 때
for(i=0;i<chars.length;++i) {
a=convert_into_single_phonemes(chars[i]);
if(a.length) for(j=0;j<a.length;++j) chars.splice(i+j, !j?1:0, a[j]);
}
}
for(i=0;i<chars.length;++i) {
// 완성형 낱내자 부호값(NFC)을 첫가끝 조합형 낱자 단위 부호값(NFD)으로 바꿈
a = convert_NFC_into_NFD(chars[i]);
if(a.constructor == Array && a.length==3 && a[0]+a[1]+a[2]) {
chars.splice(i, 1, a[0],a[1]);
if(a[2]) chars.splice(i+2, 0, a[2]);
}
}
if(chars.length) {
prev_cursor_position = f.selectionEnd;
if(f.selectionStart!=f.selectionEnd && is_phonemic_writing_input()) {
prev_cursor_position += ohiQ[0]+ohiQ[3]+ohiQ[6] ? 1:0;
if(NFD_stack.phoneme.length) prev_cursor_position += NFD_stack.combined_phoneme.length + (NFD_stack.combined_phoneme.length==3 && !option.phonemic_writing_NFD_ggeut_to_cheos ? 1:0);
}
for(i=0;i<chars.length;++i) {
if(unicode_NFD_hangeul_phoneme.indexOf(chars[i])>=0) ohiHangeul3(f,0,chars[i]); // 한글 낱자일 때
else ohiInsert(f,0,chars[i]); // 한글 낱자가 아닐 때
}
}
abbreviation_processing_state = 0;
}
function sign_layout_input(f,e,key) {
var c, i;
var layout_info = find_current_layout_info();
var sign_layout = find_extended_sign_layout();
if(!option.enable_sign_ext || !sign_layout) return 0;
if(Ko_type.substr(0,3)=='3m-') { // 세모이 자판을 비롯한 모아치기 자판
if(sign_ext_state>0) {
// 기호 확장 배열에서 기호를 넣음
c=sign_layout[key-33][sign_ext_state-1];
ohiInsert(f,0,c);
esc_ext_state();
return 1;
}
else if(key<0 && key>-4) {
// 기호 확장 배열로 들어감
sign_ext_state=-key;
show_keyboard_layout();
return 0;
}
esc_ext_state();
return 0;
}
// 요즘한글 3-2011, 3-2012 자판의 특수기호 확장 배열
if(!is_old_hangeul_input() && (Ko_type=='3-2011' || Ko_type=='3-2012')) {
if((key==118 || key==56) && ( !NFD_stack.phoneme.length&&(!ohiQ[0]&&!ohiQ[3] || ohiQ[3]) || NFD_stack.phoneme.length&&unicode_cheos.indexOf(NFD_stack.combined_phoneme[0])<0&&(unicode_ga.indexOf(NFD_stack.combined_phoneme[0])>=0 || unicode_ggeut.indexOf(NFD_stack.combined_phoneme[0])>=0)) ) {
// 3-2011, 3-2012 자판의 왼쪽 특수기호 확장 글쇠(ㅗ·ㅢ)가 눌린 횟수 더하기
i=0;
if(key==118 && !sign_layout[key-33][sign_ext_state%10-1]) { // 왼쪽 ㅗ
if(sign_ext_state<10) ++sign_ext_state;
else sign_ext_state+=2;
i=1;
}
if(key==56 && !sign_layout[key-33][sign_ext_state%10-1]) { // 오른쪽 ㅢ
if(!sign_ext_state) sign_ext_state=10;
if(sign_ext_state>=10) ++sign_ext_state;
else sign_ext_state+=2;
i=1;
}
if(i) {
if(sign_ext_state%10>3) sign_ext_state=0;
show_keyboard_layout(Ko_type);
return 1;
}
}
if(sign_ext_state) {
if(NFD_stack.phoneme.length) complete_hangeul_syllable(f);
c=sign_layout[key-33][sign_ext_state%10-1];
ohiInsert(f,0,c);
esc_ext_state();
return 1;
}
}
if(!ohiHangeul3_HanExtKey && (key==0x2F || key==0x39)
&& (((!ohiQ[0]&&!ohiQ[3] || ohiQ[3] || ohiQ[0]&&(key==0x2F || key==0x39) && sign_ext_state)
&& ((unicode_cheos.indexOf(NFD_stack.phoneme[0])<0&&unicode_ga.indexOf(NFD_stack.phoneme[0])<0&&unicode_ggeut.indexOf(NFD_stack.phoneme[0])<0) || unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 || unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0)))
) { // 나머지 공세벌식 자판의 기호 확장 글쇠가 눌린 횟수 더하기
if(ohiRQ[3] && !ohiQ[4] && !ohiQ[6] && (ohiQ[3]==39 || ohiQ[3]==44)) {
// 받침이 들어가지 않은 때에 오른쪽 ㅗ 또는 ㅜ 자리 글쇠가 두 번 눌리면 ㅗ 또는 ㅜ를 지움 (첫소리 다음에 확장 기호를 넣기 위함)
ohiRQ[3]=0;
ohiHangeul_backspace(f,e);
}
var i,j,k,l,sign_layout_depth=[0,0];
if(sign_layout[0].constructor == Array) {
for(i=0;i<sign_layout.length;++i) {
for(j=0;j<2;++j) {
if(sign_layout[i][j].constructor != Array) continue;
for(k=sign_layout_depth[j];k<sign_layout[i][j].length;++k)
if(sign_layout[i][j][k] && k+1 > sign_layout_depth[j]) sign_layout_depth[j] = k+1;
}
}
}
if(key==0x2F && sign_ext_state%10<sign_layout_depth[0]) { // 밑기호 글쇠(오른쪽 ㅗ)가 눌렸을 때
if(sign_ext_state>10) esc_ext_state();
++sign_ext_state;
if(sign_ext_state>sign_layout_depth[0]) esc_ext_state();
else show_keyboard_layout(Ko_type);
return 1;
}
if(key==0x39 && sign_ext_state%10<sign_layout_depth[1]) { // 윗기호 글쇠(오른쪽 ㅜ)가 눌렸을 때
if(sign_ext_state<11) {esc_ext_state(); sign_ext_state=10;}
++sign_ext_state;
if(sign_ext_state%10>sign_layout_depth[1]) esc_ext_state();
else show_keyboard_layout(Ko_type);
return 1;
}
}
if(sign_ext_state) {
// 나머지 공세벌식 자판의 기호 확장 배열
if(sign_layout[key-33].constructor == Array) {
if(sign_ext_state<11) c=sign_layout[key-33][0][sign_ext_state-1];
if(sign_ext_state>10) c=sign_layout[key-33][1][sign_ext_state%10-1];
}
else c=sign_layout[key-33];
if(NFD_stack.phoneme.length && key!=8 && unicode_NFD_hangeul_sidedot.indexOf(c)<0) complete_hangeul_syllable(f);
ohiInsert(f,0,c);
esc_ext_state();
return 1;
}
return 0;
}
function Sin3_extended_sign_layout_input(f,key,c1) { // 첫소리 ㅇ,ㄱ,ㅈ,ㅂ 자리 글쇠를 쓰는 신세벌식 자판의 확장 배열 기호 넣기
var c;
var extended_sign_layout = find_extended_sign_layout();
if(is_old_hangeul_input() && checkCapsLock()) return 0;
if(option.enable_sign_ext && sign_ext_state && extended_sign_layout) {
// 신세벌식 기호 확장 배열에서 문자를 넣을 때
c = extended_sign_layout[key-33][sign_ext_state-1];
ohiBackspace(f);
ohiInsert(f,0,c);
esc_ext_state();
initialize_NFD_stack();
return -1;
}
else if(option.enable_sign_ext && !sign_ext_state && extended_sign_layout && NFD_stack.phoneme.length==1 && NFD_stack.phoneme[0]==0x110B/*ㅇ*/ && (c1==0x1100/*ㄱ*/ || c1==0x110C/*ㅈ*/ || c1==0x1107/*ㅂ*/)) {
// 신세벌식 기호 확장 배열 상태로 넘어가는 조건이 갖추어졌을 때 (NFD)
if(c1==0x1100) sign_ext_state=1;
else if(c1==0x110C) sign_ext_state=2;
else if(c1==0x1107) sign_ext_state=3;
show_keyboard_layout('Sin3-ext');
return -1;
}
else if(option.enable_sign_ext && !sign_ext_state && extended_sign_layout && ohiQ[0]==150-92-35 && (c1==128 || c1==151 || c1==145) && !ohiQ[3] && !ohiQ[6]) {
// 신세벌식 기호 확장 배열 상태로 넘어가는 조건이 갖추어졌을 때 (NFC)
if(c1==128) sign_ext_state=1;
else if(c1==151) sign_ext_state=2;
else if(c1==145) sign_ext_state=3;
show_keyboard_layout('Sin3-ext');
return -1;
}
return 0;
}
function NFD_hangeul_input(f,key,c) { // 첫가끝(세벌식) 부호계를 쓰는 요즘한글/옛한글 처리
if(c==0x1160 && NFD_stack.phoneme[0]==0x1160) return; // 가운뎃소리 채움 문자가 잇달아 들어오면 처리하지 않음
if(unicode_NFD_hangeul_sidedot.indexOf(c)>=0) { // 성조를 나타내는 방점일 때
ohiInsert(f,0,c);
return;
}
ohiSelection(f,0);
var diphthong=0; // 겹홀소리의 첫 홀소리인지 (갈마들이 자판)
if(c<0) {
c=-c;
diphthong=1;
}
var type_name='';
if(typeof current_layout_info.type_name != 'undefined') type_name = current_layout_info.type_name;
else if(is_old_hangeul_input() && typeof current_layout_info.old_hangeul_layout_type_name != 'undefined') type_name = current_layout_info.old_hangeul_layout_type_name;
if(!is_old_hangeul_input() && !option.only_NFD_hangeul_encoding) c = convert_into_unicode_hangeul_phoneme(c);
if(is_old_hangeul_input() && Ko_type.substr(0,2)=='3-' && ['3-2011','3-2011-y','3-2012','3-2012-y','3-2014','3-2014-y','3-2015P','3-2015P-y'].indexOf(Ko_type)>=0) {
// 전환 글쇠를 쓰는 한글 확장 배열 처리 (3-2011 / 3-2012 / 3-2014 / 3-2015P 옛한글)
if(key==55 || c==0x1168) { // 첫째 한글 확장 글쇠(ㅖ 자리 글쇠)가 눌렸을 때
if(ohiHangeul3_HanExtKey%0x10==2 || ohiHangeul3_HanExtKey==0x11) { esc_ext_state(); complete_hangeul_syllable(f); return false;}
if(ohiHangeul3_HanExtKey>0x10) {esc_ext_state(); return false;}
ohiHangeul3_HanExtKey = (ohiHangeul3_HanExtKey&&ohiHangeul3_HanExtKey)*0x10+1;
show_keyboard_layout('3-2012y_han_ext');
return false;
} else if(key==56 || c==0x1174) { // 두째 한글 확장 글쇠(ㅢ 자리 글쇠)가 눌렸을 때
if(ohiHangeul3_HanExtKey%0x10==1 || ohiHangeul3_HanExtKey==0x12) { esc_ext_state(); complete_hangeul_syllable(f); return false;}
if(ohiHangeul3_HanExtKey>0x10) {esc_ext_state(); return false;}
ohiHangeul3_HanExtKey = (ohiHangeul3_HanExtKey&&ohiHangeul3_HanExtKey)*0x10+2;
show_keyboard_layout('3-2012y_han_ext');
return false;
}
if(ohiHangeul3_HanExtKey) { // 한글 확장 배열에서 넣기
layout = K3_2012y_extended_hangeul_layout;
c=layout[key-33][ohiHangeul3_HanExtKey%0x10-1][ohiHangeul3_HanExtKey/0x10];
c=layout[key-33][ohiHangeul3_HanExtKey%0x10-1][ohiHangeul3_HanExtKey>0x10 ? 1:0];
}
}
// 공세벌식 자판에서 첫소리가 들어온 뒤 [ 자리 글쇠가 눌렸을 때 아래아를 넣음
if(Ko_type.substr(0,2)=='3-' && key==0x5B && unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 && !ohiHangeul3_HanExtKey) c=0x119E;
if(NFD_stack.phoneme[0]!=0x1160 && NFD_stack.combined_phoneme[0]==0x1160 && unicode_cheos.indexOf(NFD_stack.combined_phoneme[1])>=0 && (unicode_NFD_hangeul_phoneme.indexOf(c)>=0 || c==0x1160)) {
// 바로 앞서 첫소리가 들어 왔고 한글 낱자가 들어왔다면 가운뎃소리 채움 문자를 지움
ohiBackspace(f);
NFD_stack.combined_phoneme.splice(0,1);
}
// 가운뎃소리 채움 문자 (두벌식 자판)
if(c==0x1160) {
if(unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 || NFD_stack.phoneme[0]==0x1160) {
// 바로 앞에 가운뎃소리가 들어왔으면 조합 끊음
complete_hangeul_syllable(f);
if(is_phonemic_writing_input() && option.phonemic_writing_adding_space_every_syllable_end) ohiInput(f,0,32); // 풀어쓰기할 때 낱내자 뒤에 빈칸 넣기 (한글 조합이 새로 이어질 때)
}
// 한글을 조합하지 않던 상태였으면 첫소리 채움 문자를 넣음
if(!NFD_stack.phoneme.length) {
NFD_stack.combined_phoneme.unshift(0x115F);
ohiInput(f,0,0x115F); // 첫소리 채움
}
// 가운뎃소리 채움 문자가 들어가지 않았으면 가운뎃소리 채움 문자를 넣음
if(NFD_stack.combined_phoneme[0]!=0x1160) {
NFD_stack.phoneme.unshift(c);
NFD_stack.phoneme_R.unshift(0);
NFD_stack.combined_phoneme.unshift(0x1160);
ohiInput(f,0,0x1160);
}
ohiSelection(f,NFD_stack.combined_phoneme.length);
return;
}
var combined_phoneme=combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],c);
// 앞 낱자와 조합하지 않는 첫소리나 한글이 아닌 문자가 들어왔을 때에 조합을 끊음
if(!combined_phoneme&&unicode_cheos.indexOf(c)>=0 || unicode_NFD_hangeul_code.indexOf(c)<0) {
if(unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 && NFD_stack.combined_phoneme.indexOf(0x1160)<0) {
// 첫소리만 들어 있었으면 가운뎃소리 채움 문자를 넣음
ohiInput(f,0,0x1160);
NFD_stack.combined_phoneme.unshift(0x1160);
}
i = unicode_NFD_hangeul_code.indexOf(c)>=0 && NFD_stack.phoneme.length ? 1 : 0;
complete_hangeul_syllable(f);
if(i && is_phonemic_writing_input() && option.phonemic_writing_adding_space_every_syllable_end) ohiInput(f,0,32); // 풀어쓰기할 때 낱내자 뒤에 빈칸 넣기 (한글 조합이 새로 이어질 때)
}
if(!combined_phoneme && (unicode_ga.indexOf(c)>=0 || compatibility_ga.indexOf(c)>=0) && unicode_cheos.indexOf(NFD_stack.phoneme[0])<0) {
// 앞에 첫소리가 없이 가운뎃소리가 들어왔을 때
i = /*unicode_NFD_hangeul_code.indexOf(c)>=0 &&*/ NFD_stack.phoneme.length ? 1 : 0;
complete_hangeul_syllable(f);
if(i && option.phonemic_writing_adding_space_every_syllable_end && is_phonemic_writing_input()) ohiInput(f,0,32); // 풀어쓰기할 때 낱내자 뒤에 빈칸 넣기 (한글 조합이 새로 이어질 때)
ohiInput(f,0,0x115F); // 첫소리 채움 문자 넣음
NFD_stack.combined_phoneme = [];
NFD_stack.combined_phoneme.unshift(0x115F);
}
else if(!combined_phoneme && unicode_ggeut.indexOf(c)>=0) {
// 끝소리가 들어왔을 때
if(!is_old_hangeul_input() && !option.only_NFD_hangeul_encoding && NFD_stack.combined_phoneme.length==2 && unicode_ga.indexOf(NFD_stack.combined_phoneme[0])>=0 && unicode_cheos.indexOf(NFD_stack.combined_phoneme[1])<0) {
// 요즘한글 자판이고 앞에 첫소리 없이 홀소리가 들어왔다면
complete_hangeul_syllable(f);
ohiInsert(f,0,ohiQ=[convert_into_ohi_hangeul_phoneme(c),0,0,0,0,0,0,0,0]);
return;
}
if(unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 && NFD_stack.combined_phoneme.indexOf(0x1160)<0) {
// 바로 앞에 첫소리가 들어왔다면 가운뎃소리 채움 문자 넣음
ohiInput(f,0,0x1160);
NFD_stack.combined_phoneme.unshift(0x1160);
}
else if(unicode_cheos.indexOf(NFD_stack.phoneme[0])<0 && unicode_ga.indexOf(NFD_stack.phoneme[0])<0 && NFD_stack.combined_phoneme.indexOf(0x115F)<0 && NFD_stack.combined_phoneme.indexOf(0x1160)<0) {
// 바로 앞에 한글 낱자나 채움 문자가 들어오지 않았을 때 첫소리·가운뎃소리 채움 문자를 넣음
i = unicode_NFD_hangeul_code.indexOf(c)>=0 && NFD_stack.phoneme.length ? 1 : 0;
complete_hangeul_syllable(f);
if(i && is_phonemic_writing_input() && option.phonemic_writing_adding_space_every_syllable_end) ohiInput(f,0,32); // 풀어쓰기할 때 낱내자 뒤에 빈칸 넣기 (한글 조합이 새로 이어질 때)
ohiInput(f,0,0x115F); // 첫소리 채움
ohiInput(f,0,0x1160); // 가운뎃소리 채움
NFD_stack.combined_phoneme.unshift(0x1160,0x115F);
}
}
NFD_stack.phoneme.unshift(c);
NFD_stack.phoneme_R.unshift(diphthong);
if(combined_phoneme) {
NFD_stack.combined_phoneme[0] = combined_phoneme;
ohiBackspace(f);
ohiInput(f,0,combined_phoneme);
}
else {
if(unicode_ggeut.indexOf(c)>=0 && unicode_ggeut.indexOf(NFD_stack.combined_phoneme[0])>=0) {
// 먼저 들어온 끝소리와 조합하지 않는 끝소리가 들어왔으면 낱내 조합을 끊고 채움 문자를 넣음
complete_hangeul_syllable(f);
ohiInput(f,0,0x115F); // 첫소리 채움
ohiInput(f,0,0x1160); // 가운뎃소리 채움
NFD_stack.combined_phoneme.unshift(0x1160,0x115F);
NFD_stack.phoneme.unshift(c);
}
NFD_stack.combined_phoneme.unshift(c);
ohiInsert(f,0,c);
}
if(unicode_cheos.indexOf(c)>=0) {
// 넣은 낱자가 첫소리이면 가운뎃소리 채움 문자 넣음
ohiInput(f,0,0x1160);
NFD_stack.combined_phoneme.unshift(0x1160);
}
esc_ext_state();
if(NFD_stack.combined_phoneme.length && unicode_NFD_hangeul_phoneme.indexOf(c)>=0) {
ohiSelection(f,NFD_stack.combined_phoneme.length);
}
}
function NFD_hangeul_single_phoneme_syllable_input(f,c) {
var a=[],i;
c=convert_into_unicode_hangeul_phoneme(c);
if(is_phonemic_writing_input() && option.phonemic_writing_in_single_phoneme && option.phonemic_writing_NFD_ggeut_to_cheos) {
if(unicode_ggeut.indexOf(c)>=0) { // 풀어쓰기 끝소리 → 첫소리
a=convert_into_single_phonemes(c);
if(a.length>1) for(i=0;i<a.length;++i) NFD_hangeul_single_phoneme_syllable_input(f,a[i]);
else c = unicode_ggeut_to_cheos[unicode_ggeut.indexOf(c)];
}
}
if(unicode_cheos.indexOf(c)>=0) ohiInsert(f,0,c); // 첫소리 넣기
else if(unicode_ga.indexOf(c)>=0 || unicode_ggeut.indexOf(c)>=0) ohiInput(f,0,0x115F); // 첫소리 채움 문자 넣기
if(unicode_ga.indexOf(c)>=0) ohiInsert(f,0,c); // 첫소리 채움 문자 넣기
else if(unicode_cheos.indexOf(c)>=0 || unicode_ggeut.indexOf(c)>=0) ohiInput(f,0,0x1160); // 가운뎃소리 채움 문자 넣기
if(unicode_ggeut.indexOf(c)>=0) ohiInsert(f,0,c); // 끝소리 넣기
initialize_NFD_stack();
}
function converting_for_special_galmadeuli_layouts(f, e, key, c1, c2, sub_c1, sub_c2, transform) {
// 신세벌식 자판과 다른 배열 방식을 쓰는 갈마들이 세벌식 자판을 신세벌식 자판의 배열 방식으로 처리할 수 있게 문자값 자리를 바꿈
var layout = find_current_layout();
var a = [c1, sub_c1, c2, sub_c2];
var _c1, _c2;
if(Ko_type.substr(0,9)=='Sin3-Cham') { // 참신세벌식
if(NFD_stack.phoneme.length==1 && unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 && unicode_ga.indexOf(c1)>=0 && combine_unicode_NFD_hangeul_phoneme(NFD_stack.phoneme[0],c1)) {
// 홀소리만 들어갔는데 먼저 들어간 것과 조합되는 홀소리이면 홀소리를 넣음
}
else if(NFD_stack.phoneme.length && unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0 && unicode_ga.indexOf(c1)>=0 && !combine_unicode_NFD_hangeul_phoneme(NFD_stack.phoneme[0],sub_c1)) {
// 앞의 끌소리와 조합되지 않는 끝소리이면 홀소리를 넣음
}
else if((ohiQ[0] || NFD_stack.phoneme.length) && unicode_ga.indexOf(c1)>=0 && unicode_ggeut.indexOf(sub_c1)>=0) {
// 한글을 조합하고 있고 가운뎃소리와 끝소리가 있는 글쇠 자리
a = [sub_c1, c2, c1, sub_c2];
} else if(unicode_ga.indexOf(c1)>=0 && unicode_cheos.indexOf(sub_c1)>=0) {
if(!ohiQ[0] || !NFD_stack.phoneme.length || unicode_cheos.indexOf(NFD_stack.phoneme[0])<0) {
// 기본 배열(c1)에 가운뎃소리가 있고 보조 배열(sub_c1)에 첫소리가 있는 자리 (첫소리 ㅋ과 ㅑ가 있는 b 자리)
a = [sub_c1, c1, c2, sub_c2];
}
} else if(unicode_NFD_hangeul_phoneme.indexOf(c1)<0 && unicode_ggeut.indexOf(sub_c1)>=0
&& (ohiQ[0] && ohiQ[3] && !ohiQ[6] || unicode_ga.indexOf(NFD_stack.phoneme[0])>=0)) {
// 끝소리 ㅋ (B 자리)
a = [sub_c1, c1, c2, sub_c2];
}
else if(sub_c1==0x1B && (ohiQ[0]+ohiQ[3]+ohiQ[6] || NFD_stack.phoneme.length)) {
// 한글 조합을 멈춤 (escape)
a = [0, 0, c2, sub_c2];
complete_hangeul_syllable(f);
}
return [a[0], a[1], a[2], a[3], transform];
}
if(Ko_type.substr(0,2)=='3-') {
transform = true;
if(Ko_type == '3-18Na') {
if(with_shift_key(key) && unicode_ggeut.indexOf(sub_c2)>=0 && unicode_ga.indexOf(c2)>=0
&& (NFD_stack.phoneme.length && unicode_ggeut.indexOf(NFD_stack.phoneme[0])<0 || (ohiQ[0]+ohiQ[3])&&!ohiQ[6])) {
// 윗글쇠 눌러 겹받침 넣기
a = [sub_c2, sub_c1, c2, c1];
}
else if((!with_shift_key(key) && unicode_ggeut.indexOf(c2)>=0 && unicode_ggeut.indexOf(sub_c2)>=0 && unicode_ga.indexOf(c1)>=0)
|| (with_shift_key(key) && unicode_ggeut.indexOf(c1)>=0 && unicode_ggeut.indexOf(sub_c1)>=0 && unicode_ga.indexOf(c2)>=0)) {
// 끝소리가 들어갔고 가운뎃소리와 끝소리가 있는 글쇠가 눌렸을 때
if( (unicode_non_combined_ggeut.indexOf(convert_into_unicode_hangeul_phoneme(ohiQ[6]))>=0 || NFD_stack.phoneme.length && unicode_non_combined_ggeut.indexOf(NFD_stack.combined_phoneme[0])>=0) &&
(!with_shift_key(key) && unicode_ggeut.indexOf(sub_c2)>=0 && (NFD_stack.phoneme.length && unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0 && sub_c2 != NFD_stack.phoneme[0] && c2 == NFD_stack.phoneme[0] && c2==NFD_stack.combined_phoneme[0] || ohiQ[6] && !ohiQ[7] /*&& sub_c2 != convert_into_unicode_hangeul_phoneme(ohiQ[6]) && c2 == convert_into_unicode_hangeul_phoneme(ohiQ[6])*/)
|| with_shift_key(key) && unicode_ggeut.indexOf(sub_c1)>=0 && (NFD_stack.phoneme.length && unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0 && sub_c1 != NFD_stack.phoneme[0] && c1 == NFD_stack.phoneme[0] && c1==NFD_stack.combined_phoneme[0] || ohiQ[6] && !ohiQ[7] /*&& sub_c1 != convert_into_unicode_hangeul_phoneme(ohiQ[6]) && c1 == convert_into_unicode_hangeul_phoneme(ohiQ[6])*/)) ) {
// 보조 배열에도 끝소리가 있고 끝소리가 하나만 들어갔을 때
// 먼저 들어간 것과 조합되는 끝소리이면 끝소리를 넣고, 그렇지 않으면 가운뎃소리를 넣음 (나빌 입력기에 없는 처리) (조합이 막히는 때를 막음)
_c1 = with_shift_key(key) ? sub_c1 : sub_c2;
_c2 = with_shift_key(key) ? sub_c2 : sub_c1;
if(!combine_unicode_NFD_hangeul_phoneme((ohiQ[6] ? convert_into_unicode_hangeul_phoneme(ohiQ[6]) : NFD_stack.combined_phoneme[0]), _c1)) {
if(_c1 == convert_into_unicode_hangeul_phoneme(ohiQ[6]) || _c1 == NFD_stack.combined_phoneme[0]) {_c1 = c1;}
else {
if(!ohiQ[0] && !NFD_stack.phoneme.length) ohiBackspace(f,e);
else ohiHangeul_backspace(f,e);
}
} else if(combine_unicode_NFD_hangeul_phoneme(convert_into_unicode_hangeul_phoneme(ohiQ[6]), sub_c2)) {
_c1 = sub_c2;
} else {
_c1 = c1;
}
return [_c1, a[1], a[2], a[3], 1];
}
if(ohiQ[6] && with_shift_key(key) && unicode_ggeut.indexOf(c1)>=0 && unicode_non_combined_ggeut.indexOf(convert_into_unicode_hangeul_phoneme(ohiQ[6]+ohiQ[7]))<0) {
// 겹받침이 조합된 다음에 받침이 또 들어왔으면 조합 끊기 (완성형)
complete_hangeul_syllable(f);
}
}
else if(!with_shift_key(key) && unicode_ggeut.indexOf(c1)>=0 && unicode_ga.indexOf(sub_c1)<0 && unicode_ga.indexOf(c2)<0 && unicode_ga.indexOf(sub_c2)<0) {
if(unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0 && NFD_stack.combined_phoneme[0] != c1
|| ohiQ[6] && convert_into_ohi_hangeul_phoneme(c1) != ohiQ[6]+ohiQ[7]) {
// 끝소리만 있는 받침 ㅈ 자리(; 자리) 글쇠가 거듭 눌렸을 때 조합 끊기
complete_hangeul_syllable(f);
}
}
}
}
if(Ko_type.substr(0,5)=='LGG3-') { // 조합되지 않는 받침이 들어왔으면 조합을 끊음
if(ohiQ[6] && ohiQ[7] && unicode_ggeut.indexOf(c1)>=0 && (ohiQ[8] || !combine_unicode_NFD_hangeul_phoneme(convert_into_unicode_hangeul_phoneme(ohiQ[6]+ohiQ[7]),convert_into_unicode_hangeul_phoneme(c1)))) {complete_hangeul_syllable(f);}
}
if(Ko_type.substr(0,5)=='Sin3-') {
// 신세벌식 원안과 달리 홀소리를 아랫글 자리에 두고 받침을 윗글 자리에 두는 배열 방식이면 transform = true
if(typeof layout[64] != 'number') i=layout[64][0], j=layout[shift_table[64]][0]; // a 자리
else i=layout[64], j=layout[shift_table[64]];
if(!with_shift_key(key) && unicode_ga.indexOf(i)>=0) transform = true;
else if(with_shift_key(key) && unicode_ga.indexOf(j)>=0) transform = true;
}
// 홀소리를 아랫글 자리에 두고 받침을 윗글 자리에 두는 신세벌식 자판을 함께 처리하기 위한 작업
if(transform && /*!with_shift_key(key) && */unicode_ga.indexOf(c1)>=0 && unicode_ggeut.indexOf(c2)>=0
&& (NFD_stack.phoneme.length && (unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 || NFD_stack.phoneme.length>1 && unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 || unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0 && combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],c2))
|| ohiQ[0]&&!ohiQ[3]&&!ohiQ[6] || ohiQ[0]&&ohiQ[3]&&!ohiQ[6] || ohiQ[0]&&ohiQ[3]&&ohiQ[6]&&!ohiQ[7])) {
// 한글을 차례대로 조합하고 있는데 현재 들어간 낱자와 조합되는 낱자일 때
a = [c2, sub_c1, c1, sub_c2];
}
return [a[0], a[1], a[2], a[3], transform];
}
function NFC_galmadeuli_preprocess(f,e,key) { // 유니코드 완성형 한글 부호계를 쓸 때의 갈마들이 세벌식 자판 전처리 함수 (신세벌식 자판을 기준으로 함)
var a, i, j, c, c1, c2, sub_c1, sub_c2;
var sublayout = find_sublayout();
var transform = false; // 홀소리와 받침의 자리가 신세벌식 자판과 맞바뀐 배열 방식을 쓰는지
// c1가 아랫글 자리이면 c2는 윗글 자리, 아니면 그 반대임
a = find_galmadeuli_chars(key);
c1 = a[0], c2 = a[1], sub_c1 = a[2], sub_c2 = a[3];
if(Sin3_extended_sign_layout_input(f,key,convert_into_ohi_hangeul_phoneme(c1))==-1) return -1;
a = converting_for_special_galmadeuli_layouts(f, e, key, c1, c2, sub_c1, sub_c2, transform);
c1 = a[0], sub_c1 = a[1], c2 = a[2], sub_c2 = a[3], transform = a[4];
c = c1;
ohi_c1 = convert_into_ohi_hangeul_phoneme(c1);
ohi_c2 = convert_into_ohi_hangeul_phoneme(c2);
ohi_sub_c1 = convert_into_ohi_hangeul_phoneme(sub_c1);
ohi_sub_c2 = convert_into_ohi_hangeul_phoneme(sub_c2);
if(Ko_type.substr(0,5)=='Sin3-' && ohi_c2<31 && with_shift_key(key) && !ohiQ[0] && !ohiQ[3] && ohiQ[6] && !ohiQ[7] && ohiDoubleJamo(2,ohiQ[6],ohi_c2)) {
// 홑받침만 들어갔는데 윗글쇠와 함께 왼쪽 글쇠가 눌렸을 때 겹받침 조합하기
ohiQ[7]=ohiDoubleJamo(2,ohiQ[6],ohi_c2);
ohiInsert(f,0,ohiQ);
return -1;
}
if(Ko_type.substr(0,5)=='Sin3-' && ohi_c1<31 && !with_shift_key(key) && !ohiQ[0] && !ohiQ[3] && ohiQ[6] && !ohiQ[7]) {
// 홑받침만 들어가 있는데 윗글쇠를 누르지 않은 채로 받침 자리 글쇠가 눌렸을 때 조합 끊기 (홑받침 쓰는 초성체 조합)
complete_hangeul_syllable(f);
}
else if(option.enable_double_final_ext && with_shift_key(key) && sub_c1
&& (ohiQ[0] || NFD_stack.phoneme.length&&unicode_cheos.indexOf(NFD_stack.phoneme[NFD_stack.phoneme.length-1])>=0) && (ohiQ[3] && !ohiQ[6] || with_shift_key(key) && NFD_stack.phoneme.length&&unicode_ga.indexOf(NFD_stack.phoneme[0])>=0)) {
// 윗글쇠를 함께 눌렀을 때 왼쪽 윗글 자리의 겹받침 넣기 (겹받침 확장 입력)
c = ohi_sub_c1;
}
else if(Ko_type.substr(0,5)=='LGG3-' && !with_shift_key(key) && ohiQ[0] && !ohiQ[3] && !sub_c1 && unicode_cheos.indexOf(c1)>=0 && unicode_ga.indexOf(c2)>=0) {
// 첫소리만 들어갔을 때 첫소리와 조합용이 아닌 홀소리가 든 글쇠가 눌리면 홀소리를 넣음 (이건구 한 손 세벌식 자판)
c = c2;
}
else if(!with_shift_key(key) && ohiQ[0] && !ohiQ[3] && unicode_ga.indexOf(sub_c1)>=0) {
// 첫소리만 들어갔을 때 보조 배열(sublayout)에서 겹홀소리 조합용 ㅗ, ㅜ, ㅡ, ㆍ 등을 넣음
c = ohi_sub_c1;
ohiRQ[3]=1;
}
else if((ohiRQ[3] || backup_ohiRQ[3]) && ohi_c1<31 && NFD_stack.phoneme[0]==0x119E && !(NFD_stack.phoneme.length>1 && (unicode_ga.indexOf(NFD_stack.phoneme[1])>=0 || unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0))) {
// 아래아가 들어 있을 때에 ㆎ(아래애), ᆢ(쌍아래아) 조합하기
if(key==100) c1=0x1175; // ㆎ(아래애) 조합하기
else if(key==122 && NFD_stack.phoneme[1]!=0x119E) c1=0x119E; // 쌍아래아(ᆢ) 조합하기
NFD_hangeul_input(f,key,c1);
return -1;
}
else if(NFD_stack.phoneme.length && ohi_c1<31 && unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0) {
// 첫가끝 조합 상태에서 받침이 들어 있는데 또 받침이 들어왔을 때
if(unicode_ggeut.indexOf(NFD_stack.phoneme[1])<0) {
if(option.enable_double_final_ext && sub_c1 && NFD_stack.phoneme[0]==convert_into_unicode_hangeul_phoneme(c1)) {
// 같은 받침 글쇠가 거듭 눌렸을 때 겹받침 확장 배열 적용하기
c = c1;
NFD_stack.phoneme.unshift(c);
NFD_stack.phoneme_R.unshift(0);
NFD_stack.combined_phoneme[0]=sub_c1;
ohiSelection(f,0);
ohiBackspace(f);
ohiInsert(f,0,sub_c1);
ohiSelection(f,NFD_stack.combined_phoneme.length);
return -1;
}
}
}
else if(ohiRQ[3] && /*ohi_c1<31 &&*/ !ohiQ[4] && !ohiQ[6] && combine_unicode_NFD_hangeul_phoneme(convert_into_unicode_hangeul_phoneme(ohiQ[3]+35),c2)) {
// 먼저 들어간 조합용 홀소리와 겹홀소리를 이룰 수 있는 홀소리일 때
c = ohi_c2;
}
else if(!with_shift_key(key) && ohi_c1<31 && ohiQ[0]&&!ohiQ[3]&&!ohiQ[6] && (ohi_c2>65 && ohi_c2<87 || key==122)) {
// 왼손 쪽 아랫글 자리에서 가운뎃소리 넣기
c = ohi_c2;
if(key==122 && (c2==0x119E/* || c2>157*/)) c = 0x119E; // Z 자리 아래아
ohiRQ[3]=0;
}
else if((transform || Ko_type.substr(0,5)=='LGG3-') && ohi_c1<31 && ohiQ[6] && !ohiQ[7] && (i=combine_unicode_NFD_hangeul_phoneme(convert_into_unicode_hangeul_phoneme(ohiQ[6]),c1))) {
// 받침을 윗글 자리에 두는 신세벌식 자판 또는 이건구 한 손 세벌식 자판의 두번째 들어온 조합되는 받침 처리
ohiQ[7]=convert_into_ohi_hangeul_phoneme(i)-ohiQ[6];
ohiInsert(f,0,ohiQ);
return -1;
}
else if(transform && with_shift_key(key) && unicode_ggeut.indexOf(c1)>=0 && unicode_ga.indexOf(c2)>=0) {
// 받침을 윗글 자리에 두는 신세벌식 자판이고 홀소리와 받침이 있는 글쇠가 윗글쇠와 함께 눌렸을 때
// 마지막으로 들어간 홀소리와 조합되는 것이면 홀소리를 넣음
if(ohiQ[3] && combine_unicode_NFD_hangeul_phoneme(convert_into_unicode_hangeul_phoneme(ohiQ[3]+ohiQ[4]+35),c2)) {
c = c2;
}
}
return c;
}
function NFD_galmadeuli_preprocess(f,e,key) { // 첫가끝 조합형을 쓸 때의 갈마들이 세벌식 자판 전처리 함수 (신세벌식 자판을 기준으로 함)
var a, i, j, c, c1, c2, sub_c1, sub_c2;
var sublayout = find_sublayout();
var transform = false; // 홀소리와 받침의 자리가 신세벌식 자판과 맞바뀐 배열 방식을 쓰는지
// c1가 아랫글 자리이면 c2는 윗글 자리, 아니면 그 반대임
a = find_galmadeuli_chars(key);
c1 = a[0], c2 = a[1], sub_c1 = a[2], sub_c2 = a[3];
if(Sin3_extended_sign_layout_input(f,key,c1)==-1) return -1;
a = converting_for_special_galmadeuli_layouts(f, e, key, c1, c2, sub_c1, sub_c2, transform);
c1 = a[0], sub_c1 = a[1], c2 = a[2], sub_c2 = a[3], transform = a[4];
c = c1;
if(Ko_type.substr(0,5)=='Sin3-' && with_shift_key(key) && NFD_stack.phoneme.length && unicode_cheos.indexOf(NFD_stack.phoneme[NFD_stack.phoneme.length-1])<0 && unicode_ga.indexOf(NFD_stack.phoneme[0])<0 && unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0 && combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],c2)) {
// 홑받침만 들어갔는데 윗글쇠와 함께 왼쪽 글쇠가 눌렸을 때 겹받침 조합하기
c = combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],c2);
NFD_stack.phoneme.unshift(c);
NFD_stack.phoneme_R.unshift(0);
NFD_stack.combined_phoneme[0]=c;
ohiSelection(f,0);
ohiBackspace(f);
ohiInsert(f,0,c);
ohiSelection(f,NFD_stack.combined_phoneme.length);
return -1;
}
if(Ko_type.substr(0,5)=='Sin3-' && option.use_hangeul_compatibility_jamo_when_entering_old_hangeul && is_old_hangeul_input() && !option.only_NFD_hangeul_encoding
&& unicode_cheos.indexOf(NFD_stack.phoneme[NFD_stack.phoneme.length-1])<0 && unicode_ga.indexOf(NFD_stack.phoneme[0])<0 && unicode_ggeut.indexOf(NFD_stack.phoneme[0])>=0 && !combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],c2)) {
// 신세벌식 자판에서 따로 들어간 받침의 조합을 끊어 호환 자모로 바꿈 (초성체)
complete_hangeul_syllable(f);
}
else if(option.enable_double_final_ext && with_shift_key(key) && sub_c1
&& (NFD_stack.phoneme.length&&unicode_cheos.indexOf(NFD_stack.phoneme[NFD_stack.phoneme.length-1])>=0) && (ohiQ[3] && !ohiQ[6] || with_shift_key(key) && NFD_stack.phoneme.length&&unicode_ga.indexOf(NFD_stack.phoneme[0])>=0)) {
// 윗글쇠를 함께 눌렀을 때 왼쪽 윗글 자리의 겹받침 넣기 (겹받침 확장 입력)
c = sub_c1;
}
else if(Ko_type.substr(0,5)=='LGG3-' && !with_shift_key(key) && NFD_stack.phoneme.length && unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 && !sub_c1 && unicode_cheos.indexOf(c1)>=0 && unicode_ga.indexOf(c2)>=0) {
// 첫소리만 들어갔을 때 첫소리와 조합용이 아닌 홀소리가 든 글쇠가 눌리면 홀소리를 넣음 (이건구 한손 세벌식 자판)
c = c2;
}
else if(option.enable_Sin3_diphthong_key && !with_shift_key(key) && NFD_stack.phoneme.length && unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 && unicode_ga.indexOf(sub_c1)>=0) {
// 첫소리만 들어갔을 때 보조 배열(sublayout)에서 겹홀소리 조합용 ㅗ, ㅜ, ㅡ, ㆍ 등을 넣음
c = -sub_c1;
}
else if(with_shift_key(key) && NFD_stack.phoneme.length && unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 && unicode_ga.indexOf(c1)>=0 && unicode_ggeut.indexOf(c2)>=0) {
// 첫소리만 들어갔고, 왼손 쪽의 끝소리가 있는 글쇠가 윗글쇠와 함께 눌렸을 때 끝소리를 넣음 (홀소리만 빠진 미완성 낱내자 조합하기)
c = c2;
}
else if(option.enable_Sin3_adding_cheos_with_shift_key && (with_shift_key(key) || !c1) && NFD_stack.phoneme.length && unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 && unicode_cheos.indexOf(c2)>=0 && (unicode_ga.indexOf(c1)>=0 || unicode_ga.indexOf(sub_c2)>=0)) {
// 첫소리만 들어갔고, 오른손 쪽의 홀소리가 있는 첫소리 글쇠를 윗글쇠와 함께 눌렀을 때 첫소리를 넣음
c = c2;
}
else if(NFD_stack.combined_phoneme.length>1 && NFD_stack.phoneme_R[0] && unicode_ga.indexOf(c2)>=0 && sub_c1 != c2 && combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],c2)) {
// 겹홀소리 조합용 가운뎃소리가 먼저 들어갔고 윗글 자리에 있는 홀소리가 있는 글쇠가 눌렸을 때
// 1타에 한하여 먼저 들어간 홀소리와 결합되는 홀소리이면 윗글 자리의 홀소리를 넣게 함
c = c2;
}
else if(!with_shift_key(key) && unicode_ggeut.indexOf(c1)>=0 && unicode_cheos.indexOf(NFD_stack.phoneme[0])>=0 && unicode_ga.indexOf(c2)>=0) {
// 첫소리만 들어갔을 때 왼손 쪽 끝소리가 함께 있는 글쇠 자리에서 가운뎃소리 넣기
c = c2;
}
else if(transform && unicode_ggeut.indexOf(c1)>=0 && unicode_ga.indexOf(c2)>=0) {
// 본래 신세벌식 자판과 홀소리와 받침 자리가 뒤바뀐 꼴이고 홀소리와 받침이 있는 글쇠가 눌렸을 때
if(with_shift_key(key) && NFD_stack.phoneme.length && unicode_cheos.indexOf(NFD_stack.phoneme[NFD_stack.phoneme.length-1])>=0 && unicode_ga.indexOf(NFD_stack.phoneme[0])>=0 && combine_unicode_NFD_hangeul_phoneme(NFD_stack.combined_phoneme[0],c2)) {
// 윗글쇠와 함께 눌렸고 마지막으로 들어간 홀소리와 조합되는 것이면 홀소리를 넣음
c = c2;
}
else if(!with_shift_key(key) && NFD_stack.phoneme.length && unicode_cheos.indexOf(NFD_stack.phoneme[NFD_stack.phoneme.length-1])<0 && unicode_ga.indexOf(NFD_stack.phoneme[0])>=0) {
// 윗글쇠를 누르지 않았고 첫소리 없이 홀소리만 들어갔다면 홀소리를 넣음
c = c2;
}
}
return c;
}
function hangeul_typewriter(f,key) { // 타자기 자판
var layout = find_current_layout();
var ch;
var c1=layout[key-33];
var c2=layout[ukey[dkey.indexOf(key)]-33]; // 윗글 자리
var ohi_c1=convert_into_ohi_hangeul_phoneme(c1);
var ohi_c2=convert_into_ohi_hangeul_phoneme(c2); // 윗글 자리
ch=c1;
if(Ko_type=='4t-1969') {
if(ohiQ[3]==68-35 && !ohiQ[4] && (!ohiRQ[3]&&c1==86 || ohiRQ[3]==1&&c1==0x3163)) {
// ㅑ+ㅣ→ㅒ
ohiQ[4]=1;
ohiInsert(f,0,ohiQ);
return -1;
}
if(ohiQ[3]==72-35 && !ohiQ[4] && !ohiRQ[3]&&c1==86) {
// ㅕ+ㅣ→ㅖ
ohiQ[4]=1;
ohiInsert(f,0,ohiQ);
return -1;
}
}
if(Ko_type=='4t-1985') {
if(shift_lock) {
ch=c2;
//if(ohi_ga.indexOf(c1)<0 || ohi_ga.indexOf(c2)<0) shift_lock=0;
if(unicode_ga.indexOf(c1)<0 || unicode_ga.indexOf(c2)<0) shift_lock=0;
// 홀소리만 든 글쇠를 누르면 받침 글쇠가 풀리지 않음. 그밖의 글쇠를 누르면 받침 글쇠가 풀림
}
}
if(ohi_cheos.indexOf(convert_into_ohi_hangeul_phoneme(ch))>=0) {
ohiRQ = [0,0,0,0,0,0,0,0,0];
}
if(compatibility_ga.indexOf(ch)>=0) { // 받침 안 붙는 홀소리
if(is_old_hangeul_input() || option.only_NFD_hangeul_encoding) {
ch=convert_into_unicode_hangeul_phoneme(ohi_ga[compatibility_ga.indexOf(ch)]);
if(NFD_stack.phoneme[0]) NFD_stack.phoneme_R[0]=1;
NFD_stack.phoneme.unshift(ch);
NFD_stack.phoneme_R.unshift(1);
NFD_stack.combined_phoneme.unshift(0x1160);
return -1;
}
else {
ch=ohi_ga[compatibility_ga.indexOf(ch)];
if(!ohiQ[3]) ohiRQ[3]=1;
else ohiRQ[4]=1;
}
}
return ch;
//return convert_into_ohi_hangeul_phoneme(ch);
//return (is_old_hangeul_input() || option.only_NFD_hangeul_encoding) ? ch : convert_into_ohi_hangeul_phoneme(ch);
}
function is_galmadeuli_input() {
var type_name = current_layout_info.type_name;
if(type_name.substr(0,2)=='2-') {
for(var i=0; i<current_layout_info.layout.length; ++i)
if(typeof current_layout_info.layout[i] == 'object') return true;
return false;
}
if(type_name.substr(0,5)=='Sin3-') return true;
if(type_name.substr(0,5)=='LGG3-') return true;
if(type_name.substr(-3)=='_gm') return true;
if(type_name.substr(0,4)=='3-20' && Number(type_name.substr(2,4))>2013) return true;
if(type_name.substr(0,3)=='3-P') return true;
if(type_name.substr(0,3)=='3-D') return true;
var a=['3-18Na'];
if(a.indexOf(type_name)>=0) return true;
return false;
}
function can_be_galmadeuli_key(key) { // 첫가끝 갈마들이를 할 수 있게 아랫글/윗글 자리에 한글 낱자가 들어간 글쇠인지
shift_key = shift_table[key-33];
var mainlayout = find_mainlayout();
var sublayout = find_sublayout();
if(unicode_cheos.indexOf(mainlayout[key-33])>=0) {
if(unicode_ga.indexOf(mainlayout[shift_key-33])>=0 || unicode_ga.indexOf(sublayout[key-33])>=0) return true;
}
if(unicode_ga.indexOf(mainlayout[key-33])>=0) {
if(unicode_ggeut.indexOf(mainlayout[shift_key-33])>=0 || unicode_ggeut.indexOf(sublayout[key-33])>=0) return true;
if(unicode_cheos.indexOf(mainlayout[shift_key-33])>=0 || unicode_cheos.indexOf(sublayout[key-33])>=0) return true;
}
if(unicode_ggeut.indexOf(mainlayout[key-33])>=0) {
if(unicode_ga.indexOf(mainlayout[shift_key-33])>=0 || unicode_ga.indexOf(sublayout[key-33])>=0) return true;
}
return false;
}
function is_moachigi_input() {
if(current_layout_info.type_name.substr(0,3)!='3m-') return false;
if(option.force_normal_typing) return false;
return true;
}
function is_left_key(key) {
if(key<0x21 || key>0x7E) return false;
if(key==0x21) return true;
if(key==0x22) return false;
if(key<=0x25) return true;
if(key==0x26) return false;
if(key<=0x2F) return false;
if(key<=0x35) return true;
if(key<=0x3F) return false;
if(key<=0x47) return true;
if(key<=0x50) return false;
if(key<=0x54) return true;
if(key==0x55) return false;
if(key<=0x58) return true;
if(key==0x59) return false;
if(key==0x5A) return true;
if(key<=0x5F) return false;
if(key<=0x67) return true;
if(key<=0x70) return false;
if(key<=0x74) return true;
if(key==0x75) return false;
if(key<=0x78) return true;
if(key==0x79) return false;
if(key==0x7A) return true;
if(key<=0x7D) return false;
if(key==0x7E) return true;
}
function is_right_key(key) {
if(key<0x21 || key>0x7E) return false;
return !is_left_key(key);
}
function with_shift_key(key) { // 윗글쇠를 누르고 친 글쇠인지
if(key<0x21) return false;
else if(key<=0x26) return true;
else if(key==0x27) return false;
else if(key<=0x2B) return true;
else if(key<=0x39) return false;
else if(key==0x3A) return true;
else if(key==0x3B) return false;
else if(key==0x3C) return true;
else if(key==0x3D) return false;
else if(key<=0x5A) return true;
else if(key<=0x5D) return false;
else if(key<=0x5F) return true;
else if(key<=0x7A) return false;
else if(key<=0x7E) return true;
return false;
}
function is_old_hangeul_input() {
if(current_layout_info.type_name && current_layout_info.type_name.substr(-2)=='-y') return true;
if(option.enable_old_hangeul_input && typeof current_layout_info.old_hangeul_layout_type_name != 'undefined') return true;
return false;
}
function is_phonemic_writing_input() {
if(option.phonemic_writing) return true;
return false;
}
function push_to_key_table(u,d,t) {
u.push(
[t[93],t[0],t[31],t[2],t[3],t[4],t[61],t[5],t[9],t[7],t[8],t[62],t[10],''],
['',t[48],t[54],t[36],t[49],t[51],t[56],t[52],t[40],t[46],t[47],t[90],t[92],t[91]],
['',t[32],t[50],t[35],t[37],t[38],t[39],t[41],t[42],t[43],t[25],t[1],''],
['',t[57],t[55],t[34],t[53],t[33],t[45],t[44],t[27],t[29],t[30],'']);
d.push(
[t[63],t[16],t[17],t[18],t[19],t[20],t[21],t[22],t[23],t[24],t[15],t[12],t[28],''],
['',t[80],t[86],t[68],t[81],t[83],t[88],t[84],t[72],t[78],t[79],t[58],t[60],t[59]],
['',t[64],t[82],t[67],t[69],t[70],t[71],t[73],t[74],t[75],t[26],t[6],''],
['',t[89],t[87],t[66],t[85],t[65],t[77],t[76],t[11],t[13],t[14]],'');
}
function push_layout_to_key_table(u,d,b) {
var a,c,bas=[];
for(var i=0;i<94;++i) {
a = typeof b[i] == 'number' ? b[i] : b[i][0];
if(a<0) c=a;
else c = String.fromCharCode(a);
bas.push(c);
}
push_to_key_table(u,d,bas);
}
function push_extended_hangeul_layout_to_key_table(u,d,ext_layout) {
var i,c,str,charCode;
var ext=[];
for(i=0;i<94;++i) {
if(typeof ext_layout[i][0] == 'object' && typeof ext_layout[i][0][0] == 'number') // 3-2012 옛한글 자판에 들어간 확장 배열
c = ext_layout[i][ohiHangeul3_HanExtKey%0x10-1][ohiHangeul3_HanExtKey>0x10 ? 1:0];
else if(typeof ext_layout[i][0] == 'number')
c = ext_layout[i][ohiHangeul3_HanExtKey-1];
else
c=ext_layout[i];
if(c<0) s=c;
else s = String.fromCharCode(c);
ext.push(s);
}
push_to_key_table(u,d,ext);
}
function push_extended_sign_layout_to_key_table(u,d,e) {
var ext=[], c, i, j=(sign_ext_state-1)%10;
if(j>=0) {
if(!is_old_hangeul_input() && (Ko_type=='3-2011' || Ko_type=='3-2012')) {
if(j<3) {
for(i=0;i<94;++i) {
c=e[i][j]>0 ? e[i][j] : 0;
ext.push(String.fromCharCode(c));
}
}
}
else if(Ko_type.substr(0,2)=='3-') {
if(sign_ext_state<11) {
for(i=0;i<94;++i) {
c=e[i][0][j]>0 ? e[i][0][j] : 0;
ext.push(String.fromCharCode(c));
}
}
if(sign_ext_state>10) {
for(i=0;i<94;++i) {
c=e[i][1][j]>0 ? e[i][1][j] : 0;
ext.push(String.fromCharCode(c));
}
}
}
else { // 신세벌식
for(i=0;i<94;++i) {
c=e[i][j]>0 ? e[i][j] : 0;
ext.push(String.fromCharCode(c));
}
}
}
push_to_key_table(u,d,ext);
}
function insert_sublayout_table(ue, de, uh, dh, sublayout) {
var u=[], d=[], sub=[], c, i, j, ds, us;
for(i=0;i<94;++i) {
c = sublayout[i];
if(special_chars.indexOf(c)>=0) c = general_chars[special_chars.indexOf(c)];
s=String.fromCharCode(convert_into_unicode_hangeul_phoneme(c));
sub.push(s);
}
push_to_key_table(u,d,sub);
for(i=0;i<de.length;++i) {
for(j=0;j<de[i].length;++j) {
if( (!u[i][j] || !u[i][j].charCodeAt(0)) && (!d[i][j] || !d[i][j].charCodeAt(0)) ) continue;
ds = de[i][j];
us = ue[i][j];
if(ue[i][j].charCodeAt(0)) {
if(d[i][j].charCodeAt(0)) {
if(u[i][j].charCodeAt(0) && d[i][j]!=u[i][j]) us=u[i][j];
ds=d[i][j];
}
else us=u[i][j];
}
else if(d[i][j].charCodeAt(0)) ds=d[i][j];
if(!(us==ue[i][j] || us==uh[i][j] || us==dh[i][j])) ue[i][j] = us;
if(!(ds==ue[i][j] || ds==uh[i][j] || ds==dh[i][j])) de[i][j] = ds;
}
}
}
function show_ohiStatusBar(op) { // 보람줄(상태 표시줄) 보이기/감추기
if(typeof op != 'undefined' && (op=='off' || op=='0' || !op)) ohiStatus.style.display='none';
else ohiStatus.style.display='block';
}
function ohiChange_enable_double_final_ext(op) { // 겹받침 확장 기능 켜기/끄기
if(op===undefined || op==1) option.enable_double_final_ext=1;
else option.enable_double_final_ext=0;
show_keyboard_layout();
}
function show_NCR_text(op) { // 문자를 유니코드 부호값과 맞대어 나타내기 (Numeric Character Reference)
if(typeof op != 'undefined') {
if(op) converting_option.NCR_text=1;
else converting_option.NCR_text=0;
}
// 수정됨: inputText -> wpTextbox1
var f = document.getElementById('wpTextbox1');
var t = document.getElementById('NCR_text');
if(!f || !t) return;
var opt, opts = document.getElementById('NCR_options');
if(opts) {
if(ohi_menu_num && ohi_menu_num<3) {
if(converting_option.NCR_text) opts.style.display = 'block';
else opts.style.display = 'inline';
}
else opts.style.display = 'none';
opt = document.getElementById('converting_option_NCR_text');
if(!opt) opt = appendChild(opts,'div','option','converting_option_NCR_text','<input name="NCR_text" class="checkbox" onclick="show_NCR_text(this.checked);inputText_focus()" type="checkbox"' + (converting_option.show_NCR_text ? ' checked="checked"' : '') + '><label title="'한글'을 &#xD55C;&#xAE00; 꼴로 나타내기">HTML 문자 참조</label>');
opt = document.getElementById('converting_option_convert_only_NFD_hangeul_encoding_in_NCR_text');
if(!opt) opt = appendChild(opts,'div','option','converting_option_convert_only_NFD_hangeul_encoding_in_NCR_text','<input name="convert_only_NFD_hangeul_encoding_in_NCR_text" class="checkbox" onclick="converting_option.convert_only_NFD_hangeul_encoding_in_NCR_text=this.checked;show_NCR_text();inputText_focus()" type="checkbox"' + (converting_option.convert_only_NFD_hangeul_encoding_in_NCR_text ? ' checked="checked"' : '') + '><label title="완성형으로 나타낼 수 있는 한글은 바꾸지 않기">첫가끝 조합형만 바꾸기</label>');
if(t && converting_option.NCR_text) {
t.style.display='inline-block';
opt.style.display='inline-block';
}
else {
t.style.display='none';
opt.style.display='none';
}
opt = document.getElementById('NCR_text_copy_button');
if(!opt) {
opt = appendChild(opts,'div','option','NCR_text_copy_button','<button onclick="copyToClipboard(document.getElementById(\'NCR_text\'))">베끼기</button>');
opt.style.cssFloat = 'right';
}
if(converting_option.NCR_text) opt.style.display='inline-block';
else opt.style.display='none';
}
var ref_char, char_code, ref_text='';
for(i=0;i<f.value.length;++i) {
char_code = f.value.charCodeAt(i);
ref_char = '&#x'+ char_code.toString(16).toUpperCase() + ';';
if(converting_option.convert_only_NFD_hangeul_encoding_in_NCR_text) {
// 첫가끝 조합형 한글만 바꿀 때
if(unicode_NFD_hangeul_code.indexOf(char_code)<0 && unicode_NFD_hangeul_sidedot.indexOf(char_code)<0) ref_char = f.value.charAt(i);
}
ref_text += ref_char;
}
if(ref_text=='') ref_text=' ';
t.innerHTML = ref_text;
}
function show_direct_typing_text(op) { // 쿼티 글쇠 배열 기준으로 문자열을 글쇠값들로 바꾸기
if(typeof op != 'undefined') {
if(op) converting_option.direct_typing_text=1;
else converting_option.direct_typing_text=0;
}
// 수정됨: inputText -> wpTextbox1
var f = document.getElementById('wpTextbox1');
var t = document.getElementById('direct_typing_text');
if(!f || !t) return;
var opt, opts = document.getElementById('direct_typing_text_options');
var mainlayout = find_mainlayout();
if(opts) {
if(ohi_menu_num && ohi_menu_num<3 && !is_moachigi_input()) {
if(converting_option.direct_typing_text) opts.style.display = 'block';
else opts.style.display = 'inline';
}
else opts.style.display = 'none';
opt = document.getElementById('converting_option_direct_typing_text');
if(!opt) {
opt = appendChild(opts,'div','option','converting_option_direct_typing_text','<input name="direct_typing_text" class="checkbox" onclick="show_direct_typing_text(this.checked);inputText_focus()" type="checkbox"' + (converting_option.direct_typing_text ? ' checked="checked"' : '') + '><label title="글에 들어간 문자열을 쿼티 배열 기준 글쇠값들로 바꾸기 ">문자열→글쇠값</label>');
opt.style.display='inline-block';
}
opt = document.getElementById('converting_option_extended_hangeul_layout_reflection');
if(!opt) opt = appendChild(opts,'div','option','converting_option_extended_hangeul_layout_reflection','<input name="extended_hangeul_layout" class="checkbox" onclick="converting_option.extended_hangeul_layout_reflection=this.checked;show_keyboard_layout();inputText_focus()" type="checkbox"' + (converting_option.combination_table_reflection_priority ? ' checked="checked"' : '') + '><label title="한글 확장 배열 반영하기">한글 확장 배열</label>');
if(converting_option.direct_typing_text && mainlayout.indexOf(-1)>=0) opt.style.display='inline-block';
else opt.style.display='none';
opt = document.getElementById('converting_option_combination_table_reflection');
if(!opt) opt = appendChild(opts,'div','option','converting_option_combination_table_reflection','<input name="combination_table_reflection" class="checkbox" onclick="converting_option.combination_table_reflection=this.checked;show_keyboard_layout();inputText_focus()" type="checkbox"' + (converting_option.combination_table_reflection ? ' checked="checked"' : '') + '><label title="낱자 조합 규칙 반영하기">낱자 조합</label>');
if(converting_option.direct_typing_text) opt.style.display='inline-block';
else opt.style.display='none';
opt = document.getElementById('converting_option_combination_table_reflection_priority');
if(!opt) opt = appendChild(opts,'div','option','converting_option_combination_table_reflection_priority','<input name="combination_table_reflection_priority" class="checkbox" onclick="converting_option.combination_table_reflection_priority=this.checked;show_keyboard_layout();inputText_focus()" type="checkbox"' + (converting_option.combination_table_reflection_priority ? ' checked="checked"' : '') + '><label title="자판 배열에 따로 있는 겹낱자에까지 낱자 조합 규칙을 우선 반영하기">낱자 조합 우선</label>');
if(converting_option.direct_typing_text && converting_option.combination_table_reflection) opt.style.display='inline-block';
else opt.style.display='none';
opt = document.getElementById('converting_option_combination_table_reflection_ggeut_ss_exception');
if(!opt) opt = appendChild(opts,'div','option','converting_option_combination_table_reflection_ggeut_ss_exception','<input name="combination_table_reflection_priority" class="checkbox" onclick="converting_option.combination_table_reflection_ggeut_ss_exception=this.checked;show_keyboard_layout();inputText_focus()" type="checkbox"' + (converting_option.combination_table_reflection_ggeut_ss_exception ? ' checked="checked"' : '') + '><label title="받침 ㅆ이 아랫글 자리에 따로 있으면 조합하여 넣은 것으로 셈하지 않음">받침 ㅆ 예외</label>');
if(converting_option.direct_typing_text && converting_option.combination_table_reflection && converting_option.combination_table_reflection_priority
&& mainlayout.indexOf(0x11BB)>=0 && !with_shift_key(mainlayout.indexOf(0x11BB)+33)) opt.style.display='inline-block';
else opt.style.display='none';
opt = document.getElementById('direct_typing_text_copy_button');
if(!opt) {
opt = appendChild(opts,'div','option','direct_typing_text_copy_button','<button onclick="copyToClipboard(document.getElementById(\'direct_typing_text\'))">베끼기</button>');
opt.style.cssFloat = 'right';
}
if(converting_option.direct_typing_text) opt.style.display='inline-block';
else opt.style.display='none';
}
if(t && converting_option.direct_typing_text && !is_moachigi_input()) {
t.style.display='inline-block';
}
else {
t.style.display='none';
return;
}
var conv_text='';
var key_table = [];
making_key_table(key_table);
for(var i=0;i<f.value.length;++i) {
conv_text += convert_into_direct_typing_chars(key_table, f.value, i);
}
if(conv_text=='') conv_text=' ';
t.innerHTML = conv_text;
}
function show_reverse_direct_typing_text(op) { // 쿼티 글쇠 배열 기준으로 글쇠값들을 문자열로 바꾸기
if(typeof op != 'undefined') {
if(op) converting_option.reverse_direct_typing_text=1;
else converting_option.reverse_direct_typing_text=0;
}
// 수정됨: inputText -> wpTextbox1
var f = document.getElementById('wpTextbox1');
var t = document.getElementById('reverse_direct_typing_text');
if(!f || !t) return;
var opt, opts = document.getElementById('reverse_direct_typing_text_options');
var mainlayout = find_mainlayout();
if(opts) {
if(ohi_menu_num && ohi_menu_num<3 && !is_moachigi_input()) {
if(converting_option.reverse_direct_typing_text) opts.style.display = 'block';
else opts.style.display = 'inline';
}
else opts.style.display = 'none';
opt = document.getElementById('converting_option_reverse_direct_typing_text');
if(!opt) {
opt = appendChild(opts,'div','option','converting_option_reverse_direct_typing_text','<input name="reverse_direct_typing_text" class="checkbox" onclick="show_reverse_direct_typing_text(this.checked);inputText_focus()" type="checkbox"' + (converting_option.direct_typing_text ? ' checked="checked"' : '') + '><label title="쿼티 배열 기준 글쇠값들을 문자열로 바꾸기 ">글쇠값→문자열</label>');
opt.style.display='inline-block';
}
opt = document.getElementById('reverse_direct_typing_text_copy_button');
if(!opt) {
opt = appendChild(opts,'div','option','reverse_direct_typing_text_copy_button','<button onclick="copyToClipboard(document.getElementById(\'reverse_direct_typing_text\'))">베끼기</button>');
opt.style.cssFloat = 'right';
}
if(converting_option.reverse_direct_typing_text) opt.style.display='inline-block';
else opt.style.display='none';
opt = document.getElementById('reverse_direct_typing_text_conversion_button');
if(!opt) {
opt = appendChild(opts,'div','option','reverse_direct_typing_text_conversion_button','<button onclick="show_reverse_direct_typing_text();">바꾸기</button>');
opt.style.cssFloat = 'right';
}
if(converting_option.reverse_direct_typing_text) opt.style.display='inline-block';
else opt.style.display='none';
}
if(t && converting_option.reverse_direct_typing_text && !is_moachigi_input()) {
t.style.display='inline-block';
}
else {
t.style.display='none';
return;
}
var temp_t = document.createElement("textarea");
document.body.appendChild(temp_t);
for(var i=0;i<f.value.length;++i) {
convert_into_reverse_direct_typing_chars(temp_t, f.value, i);
}
complete_hangeul_syllable(temp_t);
t.innerHTML = temp_t.value;
if(t.innerHTML=='') t.innerHTML=' ';
document.body.removeChild(temp_t);
}
function making_key_table(table) { // 글쇠 기준 문자 변환에 쓰이는 부호값-글쇠 대응표 만들기
var i, j, k, key, keys = [];
var layout_info = find_current_layout_info();
var mainlayout = find_mainlayout();
var sublayout = find_sublayout();
var combination_table = find_combination_table();
var single_phonemes = [];
for(i=0x21; i<0x7E; ++i) { // 아스키 영역 일반 문자
key = mainlayout.indexOf(i)+33;
if(i>0x40 && i<0x5B || i>0x60 && i<0x7B) table.push({code: i, keys: [i]}); // 영문자
else table.push({code: i, keys: [key]}); // 숫자, 기호
}
for(i=0;i<compatibility_dah.length;++i) { // 호환 자모 닿소리
keys = [];
for(j=0;j<compatibility_dah_to_NFD_hotbadchim[i].length;++j) {
if(mainlayout.indexOf(compatibility_dah_to_NFD_hotbadchim[i][0])>=0)
key = mainlayout.indexOf(compatibility_dah_to_NFD_hotbadchim[i][j])+33;
else
key = mainlayout.indexOf(unicode_ggeut_to_cheos[unicode_ggeut.indexOf(compatibility_dah_to_NFD_hotbadchim[i][j])])+33;
if(key<33) break;
if(layout_info.type_name.substr(0,2)=='2-' && with_shift_key(key) && mainlayout[key-33]==mainlayout[shift_table[key-33]-33]) key = shift_table[key-33];
else if(j && layout_info.type_name.substr(0,4)=='Sin3' && !with_shift_key(key)) key = shift_table[key-33];
keys.push(key);
}
if(j==compatibility_dah_to_NFD_hotbadchim[i].length && keys.length) table.push({code: compatibility_modern_dah[i], keys: keys}); // 끝소리로 넣음
}
for(i=0;i<compatibility_hol.length;++i) { // 호환 자모 홀소리
keys = [];
for(j=0;j<compatibility_hol_to_NFD_hothol[i].length;++j) {
key = mainlayout.indexOf(compatibility_hol_to_NFD_hothol[i][j])+33;
if(key<33) break;
if(layout_info.type_name.substr(0,2)=='2-' && with_shift_key(key) && mainlayout[key-33]==mainlayout[shift_table[key-33]-33]) key = shift_table[key-33];
keys.push(key);
}
if(j==compatibility_hol_to_NFD_hothol[i].length && keys.length) table.push({code: compatibility_modern_hol[i], keys: keys}); // 끝소리로 넣음
}
codes = unicode_NFD_hangeul_phoneme.concat(mainlayout, sublayout);
if(mainlayout.indexOf(-1)>=0 && typeof layout_info.extended_hangeul_layout != 'undefined') // 한글 확장 배열
for(i=0;i<layout_info.extended_hangeul_layout.length;++i)
for(j=0;j<layout_info.extended_hangeul_layout[i].length;++j) codes.push(layout_info.extended_hangeul_layout[i][j]);
for(i=0; i<codes.length; ++i) { // 유니코드 한글 낱자들과 기본/보조 배열에 들어간 문자들을 살핌
keys = [];
if(unicode_non_combined_phoneme.indexOf(codes[i])<0) { // 요즘한글 홑낱자가 아닌 한글 낱자와 기호
if(is_galmadeuli_input() && unicode_ggeut.indexOf(codes[i])>=0 && option.enable_double_final_ext && sublayout.indexOf(codes[i])>=0 && with_shift_key(sublayout.indexOf(codes[i])+33)) { // 겹받침 확장
keys.push(sublayout.indexOf(codes[i])+33);
}
else if(mainlayout.indexOf(codes[i])>=0) {
key = mainlayout.indexOf(codes[i])+33;
if(with_shift_key(key) && is_galmadeuli_input() && unicode_NFD_hangeul_phoneme.indexOf(codes[i])>=0 && can_be_galmadeuli_key(key))
keys.push(shift_table[key-33]);
else keys.push(key);
}
else if(converting_option.extended_hangeul_layout_reflection && mainlayout.indexOf(-1)>=0 && typeof layout_info.extended_hangeul_layout != 'undefined') {
// 한글 확장 배열
key = layout_info.extended_hangeul_layout.findIndex(function(n){return n.indexOf(codes[i])>=0;})+33;
if(key>32) {
for(j=0;j<layout_info.extended_hangeul_layout[key-33].indexOf(codes[i])+1;++j) keys.push(mainlayout.indexOf(-1)+33);
keys.push(key);
}
}
if(keys.length) {
table.push({code: codes[i], keys: keys.slice()});
continue;
}
}
single_phonemes = convert_into_single_phonemes(codes[i]);
keys = find_direct_typing_keys(single_phonemes);
if(keys.length) table.push({code: codes[i], keys: keys.slice()});
}
if(layout_info.type_name.substr(0,1)=='2') { // 2벌식 자판
for(i=0;i<table.length;++i) {
if(unicode_cheos.indexOf(table[i].code)>=0) { // 끝소리→첫소리
var ggeut = unicode_cheos_to_ggeut[unicode_cheos.indexOf(table[i].code)];
table.push({code: ggeut, keys: table[i].keys.slice()});
}
}
for(i=0;i<unicode_ggeut.length;++i) { // 첫소리에 없는 겹받칩
keys=[];
if(unicode_non_combined_ggeut.indexOf(unicode_ggeut[i])>=0) continue;
single_phonemes = convert_into_single_phonemes(unicode_ggeut[i]);
for(j=0;j<single_phonemes.length;++j) { // 배열에 따로 없는 낱자를 홑낱자로 나누어서 글쇠 치는 차례를 찾음
k = table.filter(function(e) {return e.code == single_phonemes[j]});
if(k.length) keys.push(k[0].keys[0]);
}
if(keys.length) table.push({code: unicode_ggeut[i], keys: keys.slice()});
}
}
for(i=0; i<codes.length; ++i) { // 낱자 조합 규칙
if(converting_option.combination_table_reflection && combination_table.length && unicode_NFD_hangeul_phoneme.indexOf(codes[i])>=0) {
var divided_phonemes = [codes[i]];
do {
for(j=0,k=0;j<combination_table.length;++j) {
if(combination_table[j][1]==divided_phonemes[0]) {
divided_phonemes.splice(0,1,parseInt(combination_table[j][0]/0x10000),combination_table[j][0]%0x10000);
++k;
}
}
} while(k);
if(divided_phonemes.length && find_direct_typing_keys(divided_phonemes).length) {
if(converting_option.combination_table_reflection_priority) { // 낱자 조합 규칙을 우선 적용하기
if(codes[i]!=0x11BB || !converting_option.combination_table_reflection_ggeut_ss_exception) // 받침 ㅆ 예외에 걸리지 않으면
table.unshift({code: codes[i], keys: find_direct_typing_keys(divided_phonemes)});
}
else table.push({code: codes[i], keys: find_direct_typing_keys(divided_phonemes)});
}
}
}
}
function find_direct_typing_keys(single_phonemes) {
var i, j, k, key;
var keys = [];
var layout_info = find_current_layout_info();
var mainlayout = find_mainlayout();
var sublayout = find_sublayout();
for(i=0;i<single_phonemes.length;++i) { // 배열에 따로 없는 낱자를 홑낱자로 나누어서 글쇠 치는 차례를 찾음
if(mainlayout.indexOf(single_phonemes[i])>=0) {
key = -1;
for(j=0;j<mainlayout.length && j<sublayout.length;++j)
if(mainlayout[j]==single_phonemes[i] && sublayout[j]!=single_phonemes[i]) key = j+33;
if(key<0) key = mainlayout.indexOf(single_phonemes[i])+33;
if(with_shift_key(key) && mainlayout[key-33]==mainlayout[shift_table[key-33]-33]) {
// 윗글쇠를 눌렀고 아랫글 자리에도 같은 문자가 있을 때 아랫글 글쇠로 넣음
key = shift_table[key-33];
}
else if(with_shift_key(key) && is_galmadeuli_input()) {
// 윗글쇠를 눌렀고 갈마들이를 쓰고 있을 때 아랫글 글쇠로 넣음
if(unicode_ga.indexOf(single_phonemes[i])>=0 && (i>(sublayout.indexOf(single_phonemes[0])>=0 ? 1 : 0) || is_old_hangeul_input() && !option.enable_Sin3_diphthong_key && layout_info.type_name.substr(0,4)=='Sin3')) {}
else key = shift_table[key-33];
}
else if(!with_shift_key(key) && !is_galmadeuli_input() && !i && unicode_ga.indexOf(single_phonemes[i])>=0 && mainlayout.lastIndexOf(single_phonemes[i])>=0 && mainlayout.indexOf(single_phonemes[i]) != mainlayout.lastIndexOf(single_phonemes[i])) {
// 갈마들이를 쓰지 않는 세벌식 자판의 같은 것이 2개 있는 홑홀소리(ㅗ, ㅜ 등) 가려 넣기
j = ((unicode_cheos.indexOf(mainlayout[0x6A-33])>=0) + (single_phonemes.length>1) + is_left_key(mainlayout.indexOf(single_phonemes[i])+33));
if(j%2) key = mainlayout.lastIndexOf(single_phonemes[i])+33;
else key = mainlayout.indexOf(single_phonemes[i])+33;
}
if(i+1==single_phonemes.length || !combine_unicode_NFD_hangeul_phoneme(single_phonemes[i], single_phonemes[i+1])) {
// 마지막 낱자이거나 다음 낱자와 조합되지 않을 때
keys.push(key);
return keys;
}
else if(sublayout.indexOf(single_phonemes[i])<0) keys.push(key);
}
if(sublayout.indexOf(single_phonemes[i])>=0) {
if(unicode_ga.indexOf(single_phonemes[i])>=0 && is_old_hangeul_input() && !option.enable_Sin3_diphthong_key && layout_info.type_name.substr(0,4)=='Sin3') {}
else key = sublayout.indexOf(single_phonemes[i])+33;
keys.push(key);
}
if(converting_option.extended_hangeul_layout_reflection && mainlayout.indexOf(-1)>=0 && typeof layout_info.extended_hangeul_layout != 'undefined' && mainlayout.indexOf(single_phonemes[i])<0 && sublayout.indexOf(single_phonemes[i])<0) {
// 한글 확장 배열
key = layout_info.extended_hangeul_layout.findIndex(function(n){return n.indexOf(single_phonemes[i])>=0})+33;
if(key>32) keys.push(mainlayout.indexOf(-1)+33, key);
}
}
return keys;
}
function convert_into_direct_typing_chars(key_table, text, nth) { // 글에 들어간 문자를 쿼티 기준으로 글쇠 자리에 있는 문자로 바꿈 (글쇠 기준 문자 변환)
var layout_info = find_current_layout_info();
var i, j, r, key;
var char_codes = [], codes = [];
var str = '';
if(nth>0) {
var prev_char_code = text.charCodeAt(nth-1);
char_codes.push(prev_char_code);
} else char_codes.push(-1);
var char_code = text.charCodeAt(nth);
char_codes.push(char_code);
if(text.length>=nth) {
var next_char_code = text.charCodeAt(nth+1);
char_codes.push(next_char_code);
} else char_codes.push(-1);
if(typeof char_code == 'undefined') return '';
if(char_code==32) return ' '; // 사이띄개(Space Bar)
if(char_code==10) return '\n'; // Line Feed
if(char_code==9) return '\t'; // Tab
if(char_code==0x115F) return ''; // 첫소리 채움 문자
if(char_code==0x1160) { // 가운뎃소리 채움 문자
if(layout_info.type_name.substr(0,1)=='3' || layout_info.type_name.substr(0,4)=='Sin3') return '';
if(layout_info.type_name.substr(0,1)=='2') { // 2벌식 자판 : 앞 문자가 첫소리이고 뒤 문자가 끝소리가 아니면 가운뎃소리 채움 문자를 다루지 않음
if(unicode_cheos.indexOf(text.charCodeAt(nth-1))>=0 && text.length>=nth && unicode_ggeut.indexOf(next_char_code<0)) return '';
}
}
for(i=0;i<char_codes.length;++i) {
codes[i] = [];
if(char_codes[i]<0) continue;
if(char_codes[i]>=0xAC00 && char_codes[i]<=0xD7AF) { // 완성형 한글 낱내자
var p = convert_NFC_into_NFD(char_codes[i]);
for(j=0; j<p.length; ++j) {
if(!p[j]) continue;
codes[i].push(p[j]);
}
} else codes[i].push(char_codes[i]);
}
for(i=0; i<codes[1].length; ++i) {
r = key_table.filter(function(e) {return e.code==codes[1][i]})[0];
if(typeof r != 'undefined') {
for(j=0;j<r.keys.length;++j) {
if(r.keys[j]>-1) {
key = r.keys[j];
if(!j && nth>1 && layout_info.type_name.substr(0,4)=='Sin3')
if(unicode_cheos.indexOf(text.charCodeAt(nth-2))>=0 && prev_char_code==0x1160 && unicode_ggeut.indexOf(char_code)>=0)
key = shift_table[r.keys[0]-33]; // 신세벌식 자판 : 가운뎃소리가 빠진 미완성 낱내자의 끝소리는 윗글쇠를 눌러 넣음
str += String.fromCharCode(key);
}
}
}
}
if(layout_info.type_name.substr(0,1)=='2' && str.length && (unicode_modern_hangeul_phoneme.indexOf(codes[2][0])<0 || converting_option.combination_table_reflection_priority)) { // 2벌식 자판
if(unicode_ggeut.indexOf(codes[1][codes[1].length-1])>=0 && codes[2].length>=1 && unicode_cheos.indexOf(codes[2][0])>=0) {
// 현재 낱자가 끝소리이고 다음 낱자가 첫소리일 때
// 끝소리/첫소리 조합 경계를 따로 끊어 주어야 하는 때 (이ᄣᅢ, 입ᄯᅢ)
if(convert_into_single_phonemes(codes[2][0]).length>1) {
i = combine_unicode_NFD_hangeul_phoneme(codes[1][codes[1].length-1], unicode_cheos_to_ggeut[unicode_cheos.indexOf(convert_into_single_phonemes(codes[2][0])[0])]);
j = combine_unicode_NFD_hangeul_phoneme(i, unicode_cheos_to_ggeut[unicode_cheos.indexOf(convert_into_single_phonemes(codes[2][0])[1])]);
if(i || j) str += '🄴';
}
}
if(unicode_ga.indexOf(codes[1][codes[1].length-1])>=0 && unicode_cheos.indexOf(codes[2][0])>=0) {
// 현재 낱자가 홀소리이고 다음 낱자가 첫소리일 때
// 홀소리를 넣은 다음에 낱내자 조합 경계를 따로 끊어 주어야 하는 때 (차ᄡᅡᆯ)
if(convert_into_single_phonemes(codes[2][0]).length>1) str += '🄴';
}
}
if(!str.length) str = '■'; // 대응하는 글쇠 자리나 조합 규칙을 찾지 못한 문자를 ■로 바꿈
return str;
}
function convert_into_reverse_direct_typing_chars(f, text, nth) { // 글에 들어간 문자를 쿼티 기준으로 글쇠 자리에 있는 문자로 바꿈 (글쇠 기준 문자 변환)
var key = text.charCodeAt(nth);
if(key<0x21 || key>0x7e) {
complete_hangeul_syllable(f);
if(key==32) f.value += ' '; // 사이띄개(Space Bar)
else if(key==10) f.value += '\n'; // Line Feed
else if(key==9) f.value += '\t'; // Tab
}
else if(ohi_KE.substr(0,2)=='Ko') {
if(current_layout_info.type_name.substr(0,2)=='2-') ohiHangeul2(f,0,key);
else if(!ohiHangeul3_abbreviation(f,key)) ohiHangeul3(f,0,key);
}
}
function add_option(opts, opt_name, footer) {
var opt = document.getElementById('option_' + opt_name);
eval('var opt_var = option.' + opt_name);
var opt_html = '<input name="' + opt_name + '" class="checkbox" type="checkbox"' + (opt_var ? ' checked="checked"' : '') + ' onclick="option.' + opt_name + '=this.checked; ' + footer;
if(!opt) opt = appendChild(opts, 'div', 'option', 'option_' + opt_name, opt_html);
opt.style.display = 'block';
return opt;
}
function show_options() {
var i;
var KE=ohi_KE, opt, opt_name, ft;
var opts = document.getElementById('top_options');
var type_name = typeof current_layout_info.type_name != 'undefined' ? current_layout_info.type_name : '';
if(typeof ohi_menu_num == 'undefined') ohi_menu_num=0;
show_NCR_text();
show_direct_typing_text();
show_reverse_direct_typing_text();
if(opts) {
if(ohi_menu_num && ohi_menu_num<3) opts.style.display = 'block';
else opts.style.display = 'none';
opt_name = 'only_NFD_hangeul_encoding';
ft = 'show_options();inputText_focus()"><label title="한글을 모두 첫가끝 조합형으로 넣기">첫가끝 조합</label>';
add_option(opts, opt_name, ft);
opt_name = 'phonemic_writing';
ft = 'complete_hangeul_syllable();ohiChange_enable_phonemic_writing();inputText_focus()"><label title="한글을 낱자 단위로 풀어서 넣기">풀어쓰기</label>';
add_option(opts, opt_name, ft);
opt_name = 'phonemic_writing_in_halfwidth_letter';
ft = 'inputText_focus()"><label title="한글을 반각 낱자로 넣기">반각</label>';
opt = add_option(opts, opt_name, ft);
if(is_phonemic_writing_input() && !is_old_hangeul_input() && !option.only_NFD_hangeul_encoding) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'phonemic_writing_directly';
ft = 'show_options();inputText_focus()"><label title="낱자를 조합하지 않고 바로 넣기">바로 풀기</label>';
opt = add_option(opts, opt_name, ft);
if(is_phonemic_writing_input() && !is_old_hangeul_input() && !option.only_NFD_hangeul_encoding) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'phonemic_writing_adding_space_every_syllable_end';
ft = 'inputText_focus()"><label title="낱내자(음절자) 사이에 빈칸 넣기">낱내 띄기</label>';
opt = add_option(opts, opt_name, ft);
if(is_phonemic_writing_input() && (!option.phonemic_writing_directly || option.only_NFD_hangeul_encoding)) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'phonemic_writing_in_single_phoneme';
ft = 'show_options();inputText_focus()"><label title="모든 겹낱자를 풀어서 홑낱자로 나타내기">겹낱자 풀기</label>';
opt = add_option(opts, opt_name, ft);
if(is_phonemic_writing_input() && /*(!is_old_hangeul_input() || option.only_NFD_hangeul_encoding) &&*/ (!option.phonemic_writing_directly || option.only_NFD_hangeul_encoding)) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'phonemic_writing_NFD_ggeut_to_cheos';
ft = 'show_options();inputText_focus()"><label title="끝소리를 첫소리로 바꾸어 넣기">끝→첫</label>';
opt = add_option(opts, opt_name, ft);
if(is_phonemic_writing_input() && option.only_NFD_hangeul_encoding && option.phonemic_writing_in_single_phoneme) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'phonemic_writing_initial_ieung_ellipsis';
ft = 'inputText_focus()"><label title="첫소리 ㅇ(이응) 빼기">첫ㅇ 빼기</label>';
opt = add_option(opts, opt_name, ft);
if(is_phonemic_writing_input() && (!option.phonemic_writing_directly || option.only_NFD_hangeul_encoding)) opt.style.display = 'block';
else opt.style.display = 'none';
}
opts = document.getElementById('middle_options');
if(opts) {
opts.style.display = 'block';
opt = document.getElementById('text_copy_button');
if(!opt) {
opt = appendChild(opts,'div','option','text_copy_button','<button onclick="copyToClipboard(document.getElementById(\'inputText\'))">글 베끼기</button>');
//opt.style.marginLeft = '7px';
}
opt_name = 'turn_off_OHI';
ft = 'ohiStart();inputText_focus()"><label title="온라인 한글 입력기의 입력 기능 끄기">OHI 끄기</label>';
opt = add_option(opts, opt_name, ft);
if(ohi_menu_num && ohi_menu_num<3) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'sunalae';
ft = 'show_options();inputText_focus()"><label title="두벌식 자판으로 홀소리 글쇠를 거듭 눌러 겹닿소리(된소리) 넣기">순아래 조합 <a href="https://sites.google.com/site/tinyduckn/dubeolsig-sun-alae" target="_blank">ⓘ</a></label>';
opt = add_option(opts, opt_name, ft);
if(!is_old_hangeul_input() && !is_phonemic_writing_input() && !is_galmadeuli_input() && type_name.substr(0,2)=='2-' && type_name.substr(0,5)!='2-sun') opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'enable_sign_ext';
ft = 'show_keyboard_layout();inputText_focus()"><label title="기호 확장 배열">기호 확장</label>';
opt = add_option(opts, opt_name, ft);
if(KE=='Ko' && (typeof current_layout_info.extended_sign_layout != 'undefined' && current_layout_info.extended_sign_layout) && Ko_type!='Sin3-2015') opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'enable_old_hangeul_input';
ft = 'ohiChange_enable_old_hangeul_input();ohiStart();inputText_focus()"><label title="옛한글 넣기">옛한글</label>';
opt = add_option(opts, opt_name, ft);
if(typeof current_layout_info.old_hangeul_layout_type_name != 'undefined' && !(Ko_type.substr(0,2)=='2-' && option.sunalae)) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'enable_Sin3_diphthong_key';
ft = 'show_keyboard_layout();inputText_focus()"><label title="오른손 쪽에서 ㅗ,ㅜ,ㅡ,ㆍ 넣기">오른쪽 홀소리</label>';
opt = add_option(opts, opt_name, ft);
if(type_name.substr(0,5)=='Sin3-' && is_old_hangeul_input()) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'enable_Sin3_adding_cheos_with_shift_key';
ft = 'inputText_focus()"><label title="오른쪽 홀소리 자리에서 윗글쇠 눌러 첫소리 넣기">윗글 첫소리</label>';
opt = add_option(opts, opt_name, ft);
if(type_name.substr(0,5)=='Sin3-' && is_old_hangeul_input()) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'use_hangeul_compatibility_jamo_when_entering_old_hangeul';
ft = 'show_keyboard_layout();inputText_focus()"><label title="낱자를 호환 자모로 넣기">호환 자모</label>';
opt = add_option(opts, opt_name, ft);
if(is_old_hangeul_input() && !option.only_NFD_hangeul_encoding) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'abbreviation';
ft = 'inputText_focus()"><label title="이어치기 방식으로 쓰는 줄여넣기">줄임말 조합</label>';
opt = add_option(opts, opt_name, ft);
if(Ko_type.substr(0,3)!='3m-' && typeof current_layout_info.ieochigi_hangeul_abbreviation_table != 'undefined' && !is_old_hangeul_input()) opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'force_normal_typing';
ft = 'show_keyboard_layout();inputText_focus()"><label title="한 글쇠씩 이어서 넣는 방식으로 모아치기 자판 쓰기">이어치기</label>';
opt = add_option(opts, opt_name, ft);
if(KE=='Ko' && Ko_type.substr(0,3)=='3m-') opt.style.display = 'block';
else opt.style.display = 'none';
opt_name = 'convenience_combination';
ft = 'show_keyboard_layout();inputText_focus()"><label title="입력 편의를 높이기 위한 요즘한글 낱자 조합">편의 낱자 조합</label>';
opt = add_option(opts, opt_name, ft);
if(!is_old_hangeul_input() && typeof current_layout_info.hangeul_convenience_combination_table!='undefined') opt.style.display = 'block';
else opt.style.display = 'none';
var layout_info = find_current_layout_info();
var sublayout = find_sublayout(layout_info);
opt_name = 'enable_double_final_ext';
ft = 'ohiChange_enable_double_final_ext(this.checked);inputText_focus()"><label title="윗글쇠를 함께 누르거나 같은 글쇠를 거듭 눌러 겹받침 넣기">겹받침 확장</label>';
opt = add_option(opts, opt_name, ft);
if(Ko_type.substr(0,3)!='3m-' && !is_old_hangeul_input() && sublayout.indexOf(0x11AD)>=0) opt.style.display = 'block';
else opt.style.display = 'none';
}
opts = document.getElementById('bottom_options');
if(opts) {
opts.style.display = 'block';
opt_name = 'square_layout';
ft = 'show_keyboard_layout();inputText_focus()"><label>가지런한 배열표</label>';
opt = add_option(opts, opt_name, ft);
if(ohi_menu_num && ohi_menu_num<3 && option.show_layout) opt.style.display = 'block';
else opt.style.display = 'none';
}
}
function show_keyboard_layout(type) {
var e=window.event;
var rows = document.getElementById('keyboardLayout');
if(!rows) return false;
rows.style.position = 'relative';
rows.style.display = 'block';
var inner_html='';
shift_click=0;
var KE = ohi_KE;
if(typeof ohi_menu_num=='undefined') ohi_menu_num=0;
show_options();
if(typeof type=='undefined') type = current_layout_info.type_name;
else if(type==1) {
option.show_layout = 1;
type = current_layout_info.type_name;
}
else if(!type) {
option.show_layout = 0;
rows.innerHTML = '<div class="show_layout"><span class="menu" onclick="option.show_layout=1;show_keyboard_layout(1);inputText_focus()">배열표 보이기</span></div>';
var opt = document.getElementById('option_square_layout');
opt.style.display = 'none';
return false;
}
if(!option.show_layout) return;
if(ohi_menu_num && ohi_menu_num>2) {
rows.style.display = 'none';
var opts = document.getElementById('middle_options');
opts.style.display = 'none';
opts = document.getElementById('bottom_options');
opts.style.display = 'none';
}
var layout_info = find_current_layout_info();
var layout=[], ue=[], de=[], uh=[], dh=[], l=[];
layout = find_layout_info('En', En_type).layout; // 영문 자판 배열
for(i=0;i<layout.length;++i) l[i]=String.fromCharCode(layout[i]);
push_to_key_table(ue,de,l);
ue[0][13] = 'Back';
ue[1][0] = 'Tab';
ue[2][0] = ue[3][0] = ue[3][11] = 'Shift';
de[2][0] = 'Lock';
de[3][0] = de[3][11] = ' ';
ue[2][12] = 'Enter';
de[0][13] = 'Space';
for(i=0;i<ue.length;++i)
for(j=0;j<ue[i].length;++j)
if(typeof de[i][j] != 'undefined' && ue[i][j].toLowerCase()==de[i][j].toLowerCase()) de[i][j]=' ';
if(KE=='Ko') {
if(sign_ext_state > 0) { // 기호 확장 배열
if(typeof current_layout_info.extended_sign_layout != 'undefined') layout = current_layout_info.extended_sign_layout;
if(!layout) return;
if(is_old_hangeul_input() && typeof current_layout_info.old_hangeul_layout_type_name != 'undefined')
if(typeof find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).extended_sign_layout != 'undefined')
layout = find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).extended_sign_layout;
if(layout.length) push_extended_sign_layout_to_key_table(uh, dh, layout);
}
else if(ohiHangeul3_HanExtKey) { // 한글 확장 배열
layout = typeof layout_info.extended_hangeul_layout != 'undefined' ? layout_info.extended_hangeul_layout : K3_2012y_extended_hangeul_layout;
push_extended_hangeul_layout_to_key_table(uh, dh, layout);
}
else if(typeof current_layout_info != 'undefined' && typeof current_layout_info.layout != 'undefined') { // 기본 배열
layout = find_current_layout();
push_layout_to_key_table(uh, dh, layout);
}
}
var sublayout = find_sublayout();
if(sublayout.length && !is_old_hangeul_input()
&& (option.enable_double_final_ext || current_layout_info.type_name.substr(0,3)=='3m-' || current_layout_info.type_name=='3-18Na' || current_layout_info.type_name.substr(0,3)=='3-D' || current_layout_info.type_name.substr(0,9)=='Sin3-Cham')
&& sign_ext_state<=0) {
insert_sublayout_table(ue, de, uh, dh, sublayout);
}
ue.push(['기준','영문','한글','Space','한/영','2벌식','3벌식']);
de[4] = ['자판','바꿈','바꿈','','','바꿈','바꿈'];
inner_html += '<div id="keyboardLayoutInfo"></div><span class="menu" onclick="show_keyboard_layout(0);inputText_focus()" onmouseover="this.className=\'menu over\'" onmouseout="this.className=\'menu\'">배열표 숨기기</span>';
inner_html += '<div id="keyboardLayoutTable">';
for(i=0;i<5;++i) inner_html += '<div id="row'+i+'" class="row"></div>';
inner_html += '</div>';
rows.innerHTML = inner_html;
var charCode, charCode_uh;
for(i=0, k=-1; ue[i]; i++) {
var row = document.getElementById('row'+i);
for(j=0; ue[i][j]; j++) {
var tdclass = 'e1';
var tdid = 'key'+(++k);
if(dh[i] && dh[i][j]) {
if(typeof dh[i][j].charCodeAt == 'function') {
charCode = dh[i][j].charCodeAt(0);
dh[i][j] = String.fromCharCode(convert_into_compatibility_hangeul_letter(charCode));
if(compatibility_hangeul_phoneme.indexOf(dh[i][j].charCodeAt(0))<0) dh[i][j] = (unicode_ga.indexOf(charCode)>=0 ? String.fromCharCode(0x115F) : '') + (unicode_ggeut.indexOf(charCode)>=0 ? String.fromCharCode(0x115F)+String.fromCharCode(0x1160) : '') + dh[i][j];
}
else charCode = dh[i][j];
if(unicode_cheos.indexOf(charCode)>=0) tdclass = 'h1';
else if(Ko_type.substr(1,2)=='t-' && charCode>=0x314F && charCode<0x3164) tdclass = 'h2 gin-hol';
else if(unicode_ga.indexOf(charCode)>=0) tdclass = 'h2';
else if(unicode_ggeut.indexOf(charCode)>=0) tdclass = 'h3';
if(special_chars.indexOf(charCode)>=0) dh[i][j] = special_chars_string[special_chars.indexOf(charCode)];
if(tdclass.substr(0,1)!='h')
if(unicode_modern_ggeut.indexOf(uh[i][j].charCodeAt(0))>=0) tdclass = 'h3';
}
charCode = ue[i][j].charCodeAt(0);
if(KE=='En' && ue[i][j].length==1)
if(charCode>64 && charCode<91 || charCode>96 && charCode<123) tdclass = 'e2';
if(unicode_NFD_hangeul_phoneme.indexOf(charCode)>=0) {
ue[i][j] = String.fromCharCode(convert_into_compatibility_hangeul_letter(charCode));
}
var col = appendChild(row,'div',tdclass,tdid,'','36px','0 0 0 0');
col.onclick = function(e){
e=e||window.event;
tableKey_clicked(e, this.id.substr(3), dkey[this.id.substr(3)], ukey[this.id.substr(3)]);
};
col.tabindex = 0;
if(!option.square_layout) {
if(k==13) col.style.width = '62px'; // backspace
if(k==14) col.style.width = '56px'; // tab
if(k==27) col.style.width = '42px'; // \ 글쇠
if(k==28) col.style.width = '67px'; // shift lock
if(k==40) col.style.width = '71px'; // Enter
if(k==41) col.style.width = '87px'; // 왼쪽 shift
if(k==52) col.style.width = '91px'; // 오른쪽 shift
}
else { // 가지런한 배열표
if(k==0) col.style.width = '62px'; // ` 글쇠
if(k==13) col.style.letterSpacing = '-1px'; // backspace
if(k==14) col.style.width = '62px'; // tab
if(k==27) col.style.width = '36px'; // \ 글쇠
if(k==28) col.style.width = '62px'; // shift lock
if(k==40) col.style.width = '76px'; // Enter
if(k==41) col.style.width = '62px'; // 왼쪽 shift
if(k==52) col.style.width = '116px'; // 오른쪽 shift
}
if(ue[i][j]=='Back' || ue[i][j]=='Tab' || ue[i][j]=='Enter' || ue[i][j]=='Shift') col.style.textAlign = 'center';
if(i==4) {
if(ue[i][j]=='Space') col.style.width = '312px';
else col.style.width = '41px', col.className = 'e3 special';
}
var function_keys = [13,14,28,40,41,52,56];
if(function_keys.indexOf(k)>=0) col.className += ' function';
var up = appendChild(col,'div','up','up'+k);
appendChild(up,'div','ue','ue'+k,ue[i][j]);
if(uh[i]) {
if(uh[i][j]) {
if(typeof uh[i][j].charCodeAt == 'function') {
charCode = uh[i][j].charCodeAt(0);
uh[i][j] = String.fromCharCode(charCode);
if(compatibility_hangeul_phoneme.indexOf(uh[i][j].charCodeAt(0))<0) uh[i][j] = (unicode_ga.indexOf(charCode)>=0 ? String.fromCharCode(0x115F) : '') + (unicode_ggeut.indexOf(charCode)>=0 ? String.fromCharCode(0x115F)+String.fromCharCode(0x1160) : '') + uh[i][j];
}
else charCode = uh[i][j];
if(special_chars.indexOf(charCode)>=0) uh[i][j] = special_chars_string[special_chars.indexOf(charCode)];
if(Ko_type.substr(0,2)=='2-' && is_galmadeuli_input() && unicode_ga.indexOf(charCode)>=0) { // 두벌식 갈마들이 자판
// 윗글 자리에 홀소리가 있으면 홀소리 글쇠로 나타냄
if(unicode_ga.indexOf(charCode)>=0) document.getElementById('key'+k).className = 'h2';
}
if( (Ko_type.substr(0,2)=='3-' && is_galmadeuli_input() || typeof current_layout_info.sublayout != 'undefined') && unicode_modern_ggeut.indexOf(charCode)>=0 && unicode_modern_hotbadchim.indexOf(charCode)<0) {
// 갈마들이 공세벌식 자판의 기본 배열에 들어가는 겹받침을 회색으로 나타냄
uh[i][j] = '<span style="color:gray;">'+uh[i][j]+'</span>';
}
if(unicode_NFD_hangeul_phoneme.indexOf(charCode)>=0) {
uh[i][j] = String.fromCharCode(convert_into_compatibility_hangeul_letter(charCode));
}
if(uh[i][j]==dh[i][j] && uh[i][j]!=de[i][j]) uh[i][j]=' '; // 한글 배열에서 윗글과 아랫글 자리의 문자가 같을 때 윗글 자리를 나타내지 않음
}
if(!sign_ext_state&&uh[i][j]==ue[i][j] || uh[i][j]=='&'&&ue[i][j]=='&' || uh[i][j]=='<'&&ue[i][j]=='<' || uh[i][j]=='>'&&ue[i][j]=='>') uh[i][j]=' ';
appendChild(up,'div','uh','uh'+k,uh[i][j]);
}
if(de[i][j]) {
var down = appendChild(col,'div','down','down'+k);
charCode = de[i][j].charCodeAt(0);
if(unicode_NFD_hangeul_phoneme.indexOf(charCode)>=0) de[i][j] = String.fromCharCode(convert_into_compatibility_hangeul_letter(charCode));
appendChild(down,'div','de','de'+k,de[i][j]);
if(!sign_ext_state && dh[i] && (!dh[i][j] || dh[i][j]==de[i][j])) dh[i][j]=' ';
if(dh[i] && dh[i][j]) appendChild(down,'div','dh','dh'+k,dh[i][j]);
}
}
}
var sign_ext_tag = '<span style="margin-left:-1px;background:black;color:#fff;letter-spacing:0px;font-size:0.7em;">기호</div>';
var sign_ext_tag1 = '<span style="margin:0;padding:0;background:black;color:#fff;letter-spacing:-2px;font-size:0.7em;">기호①</span>';
var sign_ext_tag2 = '<span style="margin:0;padding:0;background:black;color:#fff;letter-spacing:-2px;font-size:0.7em">기호②</span>';
var han_ext_tag1 = '<span style="margin:0;padding:0;background:black;color:#fff;letter-spacing:-2px;font-size:0.7em;">한글①</span>';
var han_ext_tag2 = '<span style="margin:0;padding:0;background:black;color:#fff;letter-spacing:-2px;font-size:0.7em;">한글②</span>';
var Moachigi_modifier_tag = '<span style="background:black;color:#fff;font-size:1em;">⇦</span>';
if(option.enable_sign_ext && KE=='Ko' && Ko_type.substr(0,2)=='3-' && typeof current_layout_info.extended_sign_layout != 'undefined') { // 공세벌식 자판의 기호 확장 글쇠 나타내기
if(Ko_type=='3-87' || Ko_type=='3-891' || Ko_type=='3-91' || Ko_type=='3-95') {
document.getElementById('de51').innerHTML = sign_ext_tag;
}
else if(!is_old_hangeul_input() && (Ko_type=='3-2011' || Ko_type=='3-2012')) {
document.getElementById('de8').innerHTML = sign_ext_tag;
document.getElementById('de45').innerHTML = sign_ext_tag;
}
else {
document.getElementById('uh9').innerHTML = sign_ext_tag2;
document.getElementById('uh51').innerHTML = sign_ext_tag1;
}
}
if(KE=='Ko' && is_old_hangeul_input() && ['3-2011','3-2011-y','3-2012','3-2012-y','3-2014','3-2014-y','3-2015P','3-2015P-y'].indexOf(Ko_type)>=0) {
document.getElementById('dh7').innerHTML = han_ext_tag1;
document.getElementById('dh8').innerHTML = han_ext_tag2;
document.getElementById('uh7').innerHTML = '<span style="color:#666;font-size:0.8em">(ㅣ)</span>';
document.getElementById('uh8').innerHTML = '<span style="color:#666;font-size:0.8em">(ㅡ)</span>';
}
if(KE=='Ko' && Ko_type.substr(0,4)=='Sin3' && Ko_type.substr(0,9)!='Sin3-Cham') { // 신세벌식 자판의 첫소리 자리에서 넣는 홀소리들
if(!sign_ext_state && !(!option.enable_Sin3_diphthong_key && is_old_hangeul_input()) && !(checkCapsLock() && typeof layout_info.capslock_extended_sign_layout != 'undefined' && !layout_info.capslock_extended_sign_layout)) {
if(sublayout.length) {
if(sublayout[14]) // 빗금(/) 자리의 겹낱자 확장 배열 홀소리
document.getElementById('uh51').innerHTML = ohiHangeul3_HanExtKey ? '' : '<font size="1">('+String.fromCharCode(convert_into_compatibility_hangeul_letter(sublayout[14]))+')</font>';
if(sublayout[72]) // 신세벌식 P2의 오른쪽 ㅡ 자리 (i 자리)
document.getElementById('de22').innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="font-size:10px; letter-spacing:-2px;color:#333;">'+String.fromCharCode(convert_into_compatibility_hangeul_letter(sublayout[72]))+'</span>';
if(sublayout[78]) // 신세벌식 P2의 오른쪽 ㅜ 자리 (o 자리)
document.getElementById('de23').innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="font-size:10px; letter-spacing:-2px;color:#333;">'+String.fromCharCode(convert_into_compatibility_hangeul_letter(sublayout[78]))+'</span>';
if(sublayout[79]) {
if(sublayout[79]==0x119E) // P 자리의 겹낱자 확장 배열 (신세벌식 P2의 오른쪽 아래아)
document.getElementById('de24').innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="font-size:10px; letter-spacing:-3px;color:#333;">'+String.fromCharCode(convert_into_compatibility_hangeul_letter(sublayout[79]))+'</span>';
else
document.getElementById('de24').innerHTML = ohiHangeul3_HanExtKey ? '' : '<font size="1">'+String.fromCharCode(convert_into_compatibility_hangeul_letter(sublayout[79]))+'</font>';
}
}
else {
if(En_type=='Dvorak') document.getElementById('de51').innerHTML = '<font size="1">(ㅗ)</font>';
else if(typeof current_layout_info.layout[30] == 'number' && current_layout_info.layout[30]==0x3F) document.getElementById('uh51').innerHTML = ohiHangeul3_HanExtKey ? '' : '<font size="1">(ㅗ)</font>';
}
}
if(option.enable_sign_ext && typeof current_layout_info.extended_sign_layout != 'undefined' && current_layout_info.extended_sign_layout && !(checkCapsLock() && typeof layout_info.capslock_extended_sign_layout != 'undefined' && !layout_info.capslock_extended_sign_layout)) {
// 신세벌식 기호 확장 배열 전환 글쇠
document.getElementById('de35').innerHTML = ohiHangeul3_HanExtKey ? '' : sign_ext_tag;
for(i=0;i<3;++i)
document.getElementById('de'+(36+i)).innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="padding:0 1px;background:black;color:#fff;font-size:10px;">'+String.fromCharCode(0x2460+i)+'</span>';
}
if(is_old_hangeul_input() && !sign_ext_state) { // 신세벌식 P, P2 옛한글 받침 배열
document.getElementById('de32').innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="margin-left:-1px;background:black;color:#fff;letter-spacing:0px;font-size:0.7em;">받침</span>';
document.getElementById('de15').innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="color:#666">ㅿ</span>';
document.getElementById('de29').innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="color:#666">ㆁ</span>';
document.getElementById('de31').innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="color:#666">ㆆ</span>';
document.getElementById('de31').innerHTML = ohiHangeul3_HanExtKey ? '' : '<span style="color:#666">ㆆ</span>';
}
}
if(KE=='Ko' && En_type!='Dvorak' && !sign_ext_state && !ohiHangeul3_HanExtKey) {
if((Ko_type.substr(0,5)=='Sin3-' && typeof current_layout_info.sublayout != 'undefined' && current_layout_info.sublayout[58]==0x119E)
|| Ko_type.substr(0,3) == '3-P' || Ko_type.substr(0,7)=='3-2015P' || Ko_type.substr(0,6)=='3-2014' || Ko_type.substr(0,6)=='3-2012' || Ko_type=='3-90') {
document.getElementById('dh25').innerHTML = ohiHangeul3_HanExtKey ? '' : '<font size="1">(ㆍ)</font>';
}
}
if(KE=='Ko' && Ko_type=='3m-Semoe2014') document.getElementById('uh38').innerHTML += Moachigi_modifier_tag;
if(KE=='Ko' && Ko_type=='3m-Semoe2015') {
document.getElementById('uh24').innerHTML = Moachigi_modifier_tag;
document.getElementById('uh38').innerHTML = Moachigi_modifier_tag;
document.getElementById('ue45').removeAttribute('class');
document.getElementById('ue45').style.float = 'left';
document.getElementById('ue45').innerHTML = Moachigi_modifier_tag;
}
if(KE=='Ko' && Ko_type=='3m-Semoe2016') {
if(sign_ext_state<=0) {
document.getElementById('uh25').innerHTML = Moachigi_modifier_tag;
document.getElementById('uh38').innerHTML = Moachigi_modifier_tag;
document.getElementById('ue45').removeAttribute('class');
document.getElementById('ue45').style.float = 'left';
document.getElementById('ue45').innerHTML = Moachigi_modifier_tag;
}
if(option.enable_sign_ext) {
document.getElementById('uh35').innerHTML = sign_ext_tag;
if(sign_ext_state<0) {
for(i=0;i<3;++i)
document.getElementById('uh'+(36+i)).innerHTML = '<span style="padding:0 1px;background:black;color:#fff;font-size:10px;">'+String.fromCharCode(0x2460+i)+'</span>';
}
}
}
if(KE=='Ko' && Ko_type.substr(0,8)=='3m-Semoe' && (!Number(Ko_type.substr(0,8)) || Number(Ko_type.substr(0,8))>=2017)) {
if(sign_ext_state<=0) {
document.getElementById('uh38').innerHTML = Moachigi_modifier_tag;
document.getElementById('ue45').removeAttribute('class');
document.getElementById('ue45').style.float = 'left';
document.getElementById('ue45').innerHTML = Moachigi_modifier_tag;
document.getElementById('uh50').innerHTML = Moachigi_modifier_tag;
}
if(option.enable_sign_ext) {
document.getElementById('de35').innerHTML = sign_ext_tag;
for(i=0;i<3;++i)
document.getElementById('de'+(36+i)).innerHTML = '<span style="padding:0 1px;background:black;color:#fff;font-size:10px;">'+String.fromCharCode(0x2460+i)+'</span>';
}
}
if(KE=='Ko' && (Ko_type=='3t-Oesol' || Ko_type=='4t-1985')) {
document.getElementById('ue41').innerHTML = 'Shift';
document.getElementById('de41').innerHTML = '(받침)';
document.getElementById('ue52').innerHTML = 'Shift';
document.getElementById('de52').innerHTML = '(받침)';
if(Ko_type=='4t-1985') {
// 홀소리와 받침이 함께 든 글쇠를 받침이 든 글쇠와 같은 색으로 나타냄
document.getElementById('key25').className = 'h3';
document.getElementById('key38').className = 'h3';
document.getElementById('key39').className = 'h3';
}
}
if(shiftlock_click) {
var shiftlock = document.getElementById('key28');
shiftlock.style.backgroundColor = 'orange';
}
show_keyboard_layout_info();
}
function checkCapsLock() {
var e=window.event;
if(typeof e != 'undefined' && e.getModifierState && e.getModifierState("CapsLock")) return true;
return false;
}
function copyToClipboard(text) {
var t;
if(typeof text.value == 'undefined') {
t = document.createElement("textarea");
document.body.appendChild(t);
t.value = text.innerHTML;
}
else t=text;
t.select();
t.setSelectionRange(0, 99999); /* For mobile devices */
document.execCommand('copy');
if(typeof text.value == 'undefined') document.body.removeChild(t);
}
function ohiStart() {
var i;
// 수정됨: inputText -> wpTextbox1
var textarea=document.getElementById('wpTextbox1');
var inputs=document.getElementsByTagName("INPUT");
complete_hangeul_syllable(this);
if(option.turn_off_OHI) {
show_ohiStatusBar(0); // 보람줄(상태 표시줄) 감추기
if(textarea) textarea.style.imeMode = 'active';
if(inputs) {
for(i=0;i<inputs.length;++i) {
if(inputs[i].className=='text') inputs[i].style.imeMode = 'active';
}
}
return;
}
if(typeof current_layout_info=='undefined' || !current_layout_info || typeof current_layout_info.KE=='undefined' || !current_layout_info.KE) {
ohiChange(default_ohi_KE, default_ohi_KE=='En' ? default_En_type : default_Ko_type);
}
ohi_KE = current_layout_info.KE;
ohiStatus.innerHTML = '<a href="javascript:ohiChange_KE();" style="color:White;text-decoration:none;"> ' + ohi_KE.toUpperCase() + ' </a>'
+ ' | <a href="javascript:ohiChange_between_same_type(\'Ko\');"><span style="color:yellow">Ko:</span><span style="color:Aquamarine">' + (is_old_hangeul_input() && typeof current_layout_info.old_hangeul_layout_type_name != 'undefined' ? current_layout_info.old_hangeul_layout_type_name : Ko_type) + '</span></a>'
+ ' / <a href="javascript:ohiChange_between_same_type(\'En\');"><span style="color:LightPink">En:</span><span style="color:Aquamarine">' + En_type + '</span></a>'
+ ' | <a href="javascript:ohiChange_KBD_type();" style="color:WhiteSmoke;text-decoration:none;">' + ohi_KBD_type + ' </a>';
if(document.body) {
show_ohiStatusBar(1);
var onmousedown = function(e) {
complete_hangeul_syllable(this);
prev_cursor_position = -1;
};
if(textarea) {
textarea.style.imeMode = 'disabled';
textarea.onmousedown = onmousedown;
}
if(inputs) {
for(i=0;i<inputs.length;++i) {
if(inputs[i].className=='text') {
inputs[i].style.imeMode = 'disabled';
inputs[i].onmousedown = onmousedown;
}
}
}
if(document.all) {
ohiStatus.style.position = 'fixed';
ohiStatus.style.right = -(document.body.scrollLeft||document.documentElement.scrollLeft)+'px';
ohiStatus.style.bottom = -(document.body.scrollTop||document.documentElement.scrollTop)+'px';
}
if(document.body != ohiStatus.parentNode) {
if(!ohiStatus.style.position) {
ohiStatus.style.position = 'fixed';
ohiStatus.style.right = '0px';
ohiStatus.style.bottom = '0px';
}
ohiStatus.target = '_blank';
//ohiStatus.href = 'http://ohi.pat.im';
ohiStatus.style.fontFamily = 'GulimChe,monospace';
ohiStatus.style.fontWeight = 'normal';
ohiStatus.style.color = 'white';
ohiStatus.style.backgroundColor = 'royalblue';
ohiStatus.style.fontSize = '13px';
ohiStatus.style.lineHeight = '13px';
ohiStatus.style.zIndex = '2550000';
/* --- 수정됨: document 전역 대신 텍스트 영역에만 이벤트 리스너 바인딩 --- */
if(textarea) {
if(textarea.addEventListener) {
textarea.addEventListener('keypress', ohiKeypress, true);
textarea.addEventListener('keydown', ohiKeydown, true);
textarea.addEventListener('keyup', ohiKeyup, true);
} else {
textarea.onkeydown = ohiKeydown;
textarea.onkeypress = ohiKeypress;
textarea.onkeyup = ohiKeyup;
}
}
for(var i=0; i<window.frames.length; i++) {
try {
if(typeof(window.frames[i].document) == 'unknown') continue;
}
catch(e) {
continue;
}
var ohi = document.createElement('script');
ohi.type= 'text/javascript';
var js_list = ['keyboard_layouts','additional_layouts','ohi'];
for(var j=0;j<js_list.length;++j) {
ohi.src = '//ohi.pat.im/'+js_list[j]+'.js';
window.frames[i].document.body.appendChild(ohi);
}
}
show_NCR_text();
show_direct_typing_text();
}
}
else ohiTimeout = setTimeout("ohiStart()",100);
}
function show_keyboard_layout_info() {
var KE=ohi_KE;
var kbd = ohi_KBD_type=='QWERTY' ? '' : ':'+ohi_KBD_type;
var type_name = typeof current_layout_info.type_name != 'undefined' ? current_layout_info.type_name : '';
var name='', link='', keyboardLayoutInfo = document.getElementById('keyboardLayoutInfo');
if(keyboardLayoutInfo) {
if(KE=='En') {
name = '<strong>[영문' + kbd + ']</strong> ';
if(En_type=='QWERTY') name += '쿼티 (QWERTY)';
else if(En_type=='Dvorak') name += '드보락 (Dvorak)';
else if(En_type=='Colemak') name += '콜맥 (Colemak)';
else if(En_type=='Workman') name += '워크맨 (Workman)';
}
else {
var beol = '3';
if(type_name.substr(0,1)=='2') beol = '2';
else if(type_name.substr(0,1)=='4') beol = '4';
name = '<strong>[한글 ' + beol + '벌식' + kbd + (is_galmadeuli_input() ? ': 갈마들이' : '') + (is_moachigi_input() ? ': 모아치기' : '') + ']</strong> ';
var full_name = typeof current_layout_info.full_name != undefined ? current_layout_info.full_name : '';
if(is_old_hangeul_input()) {
if(typeof find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).full_name != 'undefined')
full_name = find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).full_name;
}
if(typeof current_layout_info.full_name != 'undefined') name += full_name;
if(is_old_hangeul_input() && typeof current_layout_info.old_hangeul_layout_type_name != 'undefined'
&& typeof find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).link != 'undefined'
&& find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).link)
link = find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).link;
else if(typeof current_layout_info.link != 'undefined' && current_layout_info.link)
link = current_layout_info.link;
if(link) name += ' <a href="'+link+'" target="_blank">ⓘ</a>';
}
keyboardLayoutInfo.innerHTML = name;
}
}
function find_combination_table() { // 낱자 조합 규칙 정보 찾기
var i;
var combination_table = [];
var layout_info = find_current_layout_info();
if(typeof current_layout_info.moachigi_hangeul_combination_table != 'undefined' && typeof current_layout_info.hangeul_combination_table == 'undefined') {
// 모아치기 자판을 이어치기 방식으로 쓸 때
combination_table = current_layout_info.moachigi_hangeul_combination_table;
}
else {
// 이어치기 자판
combination_table = is_old_hangeul_input() ? hangeul_combination_table_full : hangeul_combination_table_default;
// 자판 배열 정보에서 지정한 낱자 조합 규칙
if(typeof layout_info.hangeul_combination_table != 'undefined') {
combination_table = layout_info.hangeul_combination_table;
}
if(is_old_hangeul_input() && option.enable_old_hangeul_input) {
if(typeof current_layout_info.old_hangeul_layout_type_name != 'undefined' && typeof find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).hangeul_combination_table != 'undefined')
combination_table = find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name).hangeul_combination_table;
}
// 편의를 높이기 위한 낱자 조합을 더함 (옛한글 자판이 아닌 한글 자판)
if(!is_old_hangeul_input() && option.convenience_combination && typeof layout_info.hangeul_convenience_combination_table != 'undefined')
combination_table = layout_info.hangeul_convenience_combination_table.concat(combination_table);
}
return combination_table;
}
function find_galmadeuli_chars(key) {
// 쿼티 자판 기준으로 p 자리 글쇠가 눌렸다면
// [c2, sub_c2] [0x003B,0x0000], /* 0x50 P: semicolon */
// [c1, sub_c1] [0x1111,0x119E], /* 0x70 p: choseong pieup, jungseong alae-a */
// c1 : 기본 배열 아랫글 자리 (0x1111)
// c2 : 기본 배열 윗글 자리 (0x003B)
// sub_c1 : 보조 배열 아랫글 자리 (0x119E)
// sub_c2 : 보조 배열 윗글 자리 (0x0000)
var c1, c2, sub_c1=0, sub_c2=0;
var layout = find_current_layout();
var sublayout = find_sublayout();
if(typeof layout[key-33] == 'number') {
c1 = layout[key-33];
c2 = layout[shift_table[key-33]-33];
if(sublayout.length) {
sub_c1 = sublayout[key-33];
sub_c2 = sublayout[shift_table[key-33]-33];
}
}
else {
c1 = layout[key-33][0];
c2 = layout[shift_table[key-33]-33][0];
sub_c1 = layout[key-33][1];
sub_c2 = layout[shift_table[key-33]-33][1];
}
return [c1, c2, sub_c1, sub_c2];
}
function find_character_combination_table() {
var layout_info = find_current_layout_info();
var character_combination_table = [];
if(typeof layout_info.character_combination_table != 'undefined')
character_combination_table = layout_info.character_combination_table.slice();
if(is_old_hangeul_input())
character_combination_table = character_combination_table.concat(yeshangeul_sign_combination);
return character_combination_table;
}
function find_layout_info(KE, type_name) {
if(typeof type_name == 'undefined' || !type_name) return false;
var i,j;
var a=[keyboard_layouts];
if(typeof additional_layouts != 'undefined') a.push(additional_layouts);
if(typeof test_layouts != 'undefined') a.push(test_layouts);
for(i=0;i<a.length;++i)
for(j=0;j<a[i].length;++j)
if(KE==a[i][j].KE && typeof a[i][j].type_name != 'undefined' && type_name.toLowerCase()==a[i][j].type_name.toLowerCase())
return a[i][j];
return false;
}
function find_current_layout() {
var layout_info = find_current_layout_info();
var layout = layout_info.layout;
if(checkCapsLock() && typeof layout_info.capslock_layout != 'undefined') layout = layout_info.capslock_layout;
return layout;
}
function find_current_layout_info() {
var layout_info = current_layout_info;
if(is_old_hangeul_input() && typeof current_layout_info.old_hangeul_layout_type_name != 'undefined') layout_info = find_layout_info('Ko', current_layout_info.old_hangeul_layout_type_name);
return layout_info;
}
function find_extended_sign_layout(layout_info) {
if(typeof layout_info == 'undefined') layout_info = find_current_layout_info();
var sign_layout = null;
if(typeof layout_info.extended_sign_layout != 'undefined') sign_layout = layout_info.extended_sign_layout;
if(checkCapsLock()) {
if(typeof layout_info.capslock_extended_sign_layout != 'undefined') sublayout=layout_info.capslock_extended_sign_layout;
}
return sign_layout;
}
function find_mainlayout(layout_info) {
var layout = typeof layout_info == 'undefined' ? find_current_layout() : layout_info.layout;
var mainlayout = [];
if(typeof layout[0] == 'number') for(i=0;i<layout.length;++i) mainlayout.push(layout[i]);
else for(i=0;i<layout.length;++i) mainlayout.push(layout[i][0]);
return mainlayout;
}
function find_sublayout(layout_info) {
if(typeof layout_info == 'undefined') layout_info = find_current_layout_info();
var sublayout = [];
if(checkCapsLock() && typeof layout_info.capslock_sublayout != 'undefined') sublayout=layout_info.capslock_sublayout;
else if(checkCapsLock() && typeof layout_info.capslock_layout != 'undefined' && typeof layout_info.capslock_layout[0] != 'number') {
for(i=0;i<layout_info.capslock_layout.length;++i) sublayout.push(layout_info.capslock_layout[i][1]);
}
else if(typeof layout_info.sublayout != 'undefined') sublayout=layout_info.sublayout;
else if(typeof layout_info.layout != 'undefined' && typeof layout_info.layout[0] != 'number') {
for(i=0;i<layout_info.layout.length;++i) sublayout.push(layout_info.layout[i][1]);
}
return sublayout;
}
function ohiChange(KE, type_name) {
// 수정됨: inputText -> wpTextbox1
var f=document.getElementById('wpTextbox1');
inputText_focus();
if(NFD_stack.phoneme.length && f) complete_hangeul_syllable(f);
esc_ext_state();
var prev_layout_info = typeof current_layout_info != 'undefined' ? current_layout_info : null;
if(KE.toLowerCase()=='en') KE='En';
else if(KE.toLowerCase()=='ko' || KE.toLowerCase()=='k2' || KE.toLowerCase()=='k3') KE='Ko';
ohi_KE = ohi_KE.replace(/(En|Ko)/, KE.substr(0,2));
var layout_info = find_layout_info(KE, type_name);
if(layout_info) {
current_layout_info = layout_info;
if(KE=='En') En_type = current_layout_info.type_name;
else Ko_type = current_layout_info.type_name;
}
if(layout_info != prev_layout_info) {
ohiStart();
show_keyboard_layout(KE=='En' ? En_type : Ko_type);
}
}
function ohiChange_between_same_type(type) { // 같은 한·영 종류의 배열 바꾸기 (Ko는 주요 배열만 간추림)
var i,j=-1;
var En_type_array = ['QWERTY','Dvorak','Colemak','Workman'];
var Ko_type_array = ['2-KSX5002','2-KPS9256','Sin3-P2','3m-Semoe','3-P3'];
if(type=='En') {
for(i=0;i<En_type_array.length;++i)
if(En_type.toLowerCase()==En_type_array[i].toLowerCase()) j=i;
En_type = En_type_array[(j+1)%i];
ohiChange('En',En_type);
return;
}
var a=[keyboard_layouts];
if(typeof additional_layouts != 'undefined') a.push(additional_layouts);
if(typeof test_layouts != 'undefined') a.push(test_layouts);
for(i=0;i<Ko_type_array.length;++i) {
if(type=='K2' && Ko_type_array[i].substr(0,1)!='2' || type=='K3' && Ko_type_array[i].substr(0,1)=='2') {
// 두벌식 자판 이름(type_name)의 앞에 2이 붙지 않았거나 세벌식 자판 이름에 2이 붙은 것은 뺌
Ko_type_array.splice(i--,1);
}
}
if(type!='Ko') {
// type이 K2이면 Ko_type_array에 모든 두벌식 자판 이름을 넣고, K3이면 모든 세벌식 자판 이름을 넣음
for(i=0;i<a.length;++i)
for(j=0;j<a[i].length;++j)
if(a[i][j].KE=='Ko' && typeof a[i][j].type_name != 'undefined' && Ko_type_array.indexOf(a[i][j].type_name)<0) {
if(type=='K2' && a[i][j].type_name.substr(0,1)=='2') Ko_type_array.push(a[i][j].type_name);
if(type=='K3' && a[i][j].type_name.substr(0,1)!='2') Ko_type_array.push(a[i][j].type_name);
}
}
for(i=0;i<Ko_type_array.length;++i)
if(Ko_type.toLowerCase()==Ko_type_array[i].toLowerCase()) j=i;
if(type!='Ko' && (Ko_type.substr(0,1)=='2'&&Ko_type_array[(j+1)%i].substr(0,1)!='2' || Ko_type.substr(0,1)!='2'&&Ko_type_array[(j+1)%i].substr(0,1)=='2')) Ko_type = Ko_type_array[0];
else Ko_type = Ko_type_array[(j+1)%i];
ohiChange('Ko',Ko_type);
}
function ohiChange_KE(type) { // 한·영 상태 바꾸기
var KE = ohi_KE;
if(type === undefined || !type) {
if(KE=='En') ohiChange('Ko',Ko_type);
else if(KE=='Ko') ohiChange('En',En_type);
}
else if(type=='En') ohiChange('En',En_type);
else if(type=='Ko') ohiChange('Ko',Ko_type);
}
function ohiChange_KBD_type(type) { // 기준 자판 바꾸기
if(type === undefined || !type) {
ohi_KBD_type = ohi_KBD_type=='QWERTY' ? 'QWERTZ' : ohi_KBD_type=='QWERTZ' ? 'AZERTY' : 'QWERTY';
ohiStart();
}
else {
ohi_KBD_type = type;
ohiStart();
}
show_keyboard_layout(option.show_layout);
}
function ohiChange_enable_sign_ext(op) {
if(op=='off' || op=='0') option.enable_sign_ext = 0;
else option.enable_sign_ext = 1;
show_keyboard_layout(option.show_layout);
var checkbox = document.getElementsByName('sign_extension');
if(checkbox && typeof checkbox[0].checked != 'undefined') checkbox[0].checked = option.enable_sign_ext;
}
function ohiChange_enable_old_hangeul_input(op) {
if(typeof op != 'undefined') {
if(op=='off' || op=='0') option.enable_old_hangeul_input = 0;
else option.enable_old_hangeul_input = 1;
}
complete_hangeul_syllable();
Sin3_hangeul_extension();
show_keyboard_layout(option.show_layout);
}
function ohiChange_enable_phonemic_writing(op) {
if(op=='off' || op=='0') option.enable_phonemic_writing = 0;
else option.enable_phonemic_writing = 1;
show_keyboard_layout(option.show_layout);
}
function ohiChange_force_normal_typing(op) { // 모아치기 자판을 이어치기(일반 타자법)으로 치게 하기
if(typeof op != 'undefined' && op=='off' || op=='0') option.force_normal_typing = 0;
else option.force_normal_typing = 1;
var checkbox = document.getElementsByName('force_normal_typing');
if(checkbox && typeof checkbox[0].checked != 'undefined') checkbox[0].checked = option.force_normal_typing;
}
function Sin3_hangeul_extension() {
if(Ko_type.substr(0,5)!='Sin3-') return;
var opt = document.getElementById('option_enable_Sin3_diphthong_key');
if(opt) {
if(option.enable_old_hangeul_input && current_layout_info.type_name.substr(0,5)=='Sin3-') opt.style.display = 'block';
else opt.style.display = 'none';
}
}
function ohiKeyswap(e,key) {
var KE=ohi_KE;
var i=0, swaped=[];
if(ohi_KBD_type=='QWERTZ') swaped=[89,90,90,89,121,122,122,121];
if(ohi_KBD_type=='AZERTY') swaped=[65,81,81,65,87,90,90,87,97,113,113,97,119,122,122,119,77,58,109,59,44,109,58,46,59,44];
while(swaped[i] && swaped[i]!=key) i+=2;
if(i!=swaped.length) key=swaped[i+1];
if(KE!='En' || En_type!='QWERTY') {
// 영문 쿼티 자판이 아니면 Caps Lock이 적용되지 않게 함
if(key>64 && key<91 && !e.shiftKey) key+=32;
if(key>96 && key<123 && e.shiftKey) key-=32;
}
return key;
}
function ohiKeypress(e) {
if(onkeypress_skip) return false;
var key_pressed=0; // 특수 기능 글쇠가 아닌 글쇠(일반 글쇠)가 눌렸는지
var KE=ohi_KE.substr(0,2);
var e=e||window.event, f=e.target||e.srcElement, n=f.nodeName||f.tagName, key=e.which||e.which==0?e.which:e.keyCode;
key=ohiKeyswap(e,key);
if(f.id=='inputText') {show_NCR_text();show_direct_typing_text();}
if(option.turn_off_OHI) {
tableKey_press(key);
return false;
}
var i = ohiQ[0]+ohiQ[3]+ohiQ[6] || NFD_stack.phoneme.length ? 1 : 0;
if(f.type=='text' && n=='INPUT' || n=='TEXTAREA') {
if((key==13 || key==32) && !e.ctrlKey && !e.shiftKey && !e.altKey) { // 줄바꾸개(enter)와 사이띄개(space bar)
if(!(browser=="MSIE" && browser_ver<9)) {
if(key==32) if(e.preventDefault) e.preventDefault();
if(!i && f.selectionEnd!=f.selectionStart) ohiBackspace(f);
}
prev_cursor_position = -1;
complete_hangeul_syllable(f);
// 풀어쓰기를 하고 있고 '낱내 뒤 빈칸 넣기' 기능을 쓰는데 한글을 조합하다가 사이띄개가 눌리면 빈칸을 더하여 넣음
if(i && key==32 && is_phonemic_writing_input() && option.phonemic_writing_adding_space_every_syllable_end) ohiInsert(f,0,32);
ohiInsert(f,0,key);
esc_ext_state();
}
else if((key==10 || key==13 || key==32) && (e.ctrlKey^e.shiftKey)) { // Toggle
if(e.preventDefault) e.preventDefault();
if((key==10 || key==13) && e.ctrlKey) ohiChange_KBD_type(); // 기준 자판 바꾸기
else if(key==32 && (e.ctrlKey || e.shiftKey)) ohiChange_KE(); // 한·영 상태 바꾸기
key_pressed=0;
}
else if(key==49 && e.altKey && !e.ctrlKey && !e.shiftKey) { // 영문 배열 종류 바꾸기 (QWERTY/Dvorak/Colemak)
ohiChange_between_same_type('En');
if(e.preventDefault) e.preventDefault();
key_pressed=0;
}
else if(key==50 && e.altKey && !e.ctrlKey && !e.shiftKey) { // 두벌식 배열 종류 바꾸기 (한국/조선 표준 자판)
ohiChange_between_same_type('K2');
if(e.preventDefault) e.preventDefault();
key_pressed=0;
}
else if(key==51 && e.altKey && !e.ctrlKey && !e.shiftKey) { // 세벌식 배열 종류 바꾸기
ohiChange_between_same_type('K3');
if(e.preventDefault) e.preventDefault();
key_pressed=0;
}
else if(ohi_KE.substr(0,2)=='En' && key>32 && key<127 && e.keyCode<127 && !e.altKey && !e.ctrlKey) { // 영문 상태에서 일반 글쇠
if(e.preventDefault) e.preventDefault();
ohiRoman(f,e,key);
key_pressed=1;
}
else if(ohi_KE.substr(0,2)!='En' && key>32 && key<127 && e.keyCode<127 && !e.altKey && !e.ctrlKey) { // 영문 상태가 아닐 때 일반 글쇠
if(e.preventDefault) e.preventDefault();
key_pressed=1;
if(is_moachigi_input()) pressed_key_accumulation(f,e,key);
else {
if((document.selection && document.selection.createRange().text.length!=1) || (f.selectionEnd+1 && f.selectionEnd-f.selectionStart!=1))
ohiInsert(f,0,0);
if(ohi_KE.substr(0,2)=='Ko') {
if(current_layout_info.type_name.substr(0,2)=='2-') ohiHangeul2(f,e,key);
else if(!ohiHangeul3_abbreviation(f,key)) ohiHangeul3(f,e,key);
}
}
}
}
tableKey_press(key);
return false;
}
function ohiKeydown(e) {
if(option.turn_off_OHI) {
show_NCR_text();
show_direct_typing_text();
return false;
}
onkeypress_skip=0; // 참이면 ohiKeypress() 처리를 건너뜀
onkeyup_skip=0; // 참이면 ohiKeyup() 처리를 건너뜀
var i=0;
var e=e||window.event, f=e.target||e.srcElement, n=f.nodeName||f.tagName, key=e.which||e.which==0?e.which:e.keyCode;
var KE = ohi_KE;
if(f.type=='text' && n=='INPUT' || n=='TEXTAREA') {
if(e.keyCode>=96 && e.keyCode<=111) { // 오른쪽 숫자판(키패드) 글쇠일 때
onkeypress_skip=1;
esc_ext_state();
var c=Array(/*0*/48,/*1*/49,/*2*/50,/*3*/51,/*4*/52,/*5*/53,/*6*/54,/*7*/55,/*8*/56,/*9*/57,
/***/42,/*+*/43,0,/*-*/45,/*.*/46,/*/*/47)[e.keyCode-96];
ohiInsert(f,0,c);
if(e.preventDefault) e.preventDefault();
return false;
}
if(e.keyCode==8) { // Backspace
tableKey_press(e.keyCode);
if(is_moachigi_input()) {
if(e.preventDefault) e.preventDefault();
pressed_key_accumulation(f,e,key);
onkeyup_skip=0;
return false;
}
else if(option.abbreviation && prev_cursor_position>=0) {
// 이어치기 자판으로 줄임말을 넣은 뒤
ohiHangeul_moa_backspace(f,e);
return false;
}
if(character_combination_queue.length && ohiCombinedCharacter_backspace(f,e)) return false;
if(ohiHangeul_backspace(f,e)) return false;
if(e.preventDefault) e.preventDefault();
ohiBackspace(f);
onkeyup_skip=1;
}
if(e.keyCode==13) { // Enter (한글 조합 상태)
tableKey_press(e.keyCode);
if(character_combination_queue.length) ohiSelection(f,0);
prev_class = [];
if(is_moachigi_input()) {
if(e.preventDefault) e.preventDefault();
if(!(ohiQ[0]+ohiQ[3]+ohiQ[6]) && !NFD_stack.phoneme.length && typeof f.selectionEnd != 'undefined' && f.selectionStart != f.selectionEnd) ohiBackspace(f);
pressed_key_accumulation(f,e,key);
esc_ext_state();
return false;
}
}
if(e.keyCode==32) { // Space
tableKey_press(e.keyCode);
if(NFD_stack.phoneme.length || ohiQ[0]+ohiQ[3]+ohiQ[6] || character_combination_queue.length) ohiSelection(f,0);
prev_class = [];
if(is_moachigi_input()) {
if(!pressing_keys) return false;
if(e.preventDefault) e.preventDefault();
pressed_key_accumulation(f,e,key);
onkeyup_skip = 0;
return false;
}
}
if(e.keyCode==20) { // Caps Lock
//tableKey_press(e.keyCode);
show_keyboard_layout();
}
if((e.keyCode>=35 && e.keyCode<=40) || e.keyCode==45 || e.keyCode==46) { // end(35), home(36), 화살표(37~40), insert(45), del(46)
if(NFD_stack.phoneme.length || ohiQ[0]+ohiQ[3]+ohiQ[6]) { // 한글 조합 상태
complete_hangeul_syllable(f);
prev_class = [];
prev_cursor_position = -1;
}
esc_ext_state();
}
if(e.keyCode==17) { // ctrl
if(!pressing_keys && pressed_keys.indexOf(17)<0) pressed_key_accumulation(f,e,key);
}
if(e.keyCode!=17 && pressing_keys && pressed_keys.indexOf(17)>=0) { // ctrl + ?
pressed_keys=[];
pressing_keys=0;
if(ohiQ[0]+ohiQ[3]+ohiQ[6] || NFD_stack.phoneme.length) {
complete_hangeul_syllable(f);
}
}
/*
if(e.keyCode==18) { // Alt
}
if(e.keyCode==91 || e.keyCode==93) { // menu
}
if(e.keyCode>=112 && e.keyCode<=123) { // F1~F12
}
*/
if(e.keyCode==16) { // shift
if(KE=='Ko' && Ko_type=='2-Gaon26KM') {
pressed_key_accumulation(f,e,key);
tableKey_press(e.keyCode);
}
if(KE=='Ko' && Ko_type=='4t-1985') tableKey_press(e.keyCode);
if(Ko_type.substr(0,3)=='3m-' && !option.force_normal_typing) tableKey_press(e.keyCode);
}
if(e.keyCode<45 && e.keyCode!=16 && e.keyCode!=17 && e.keyCode!=18 && e.keyCode!=13 && e.keyCode!=32) {
if(NFD_stack.phoneme.length || ohiQ[0]+ohiQ[3]+ohiQ[6]) complete_hangeul_syllable(f);
esc_ext_state();
prev_class = [];
prev_cursor_position = -1;
}
}
if(f.id=='inputText') {show_NCR_text();show_direct_typing_text();}
}
function ohiKeyup(e) {
var e=e||window.event, f=e.target||e.srcElement;
var KE=ohi_KE.substr(0,2);
var exceptional_keys = [32,13,8,16]; // 사이띄개(32), 줄바꾸개(13), 뒷걸음쇠(8), 윗글쇠(16)
if(onkeyup_skip || option.turn_off_OHI || (e.keyCode<47 && exceptional_keys.indexOf(e.keyCode)<0)) {}
else if(!option.force_normal_typing && KE=='Ko' && Ko_type.substr(0,3)=='3m-') {
if(e.keyCode==16 || pressing_keys && !--pressing_keys) { // 윗글쇠(16)를 떼었거나 모든 글쇠를 뗌
while(pressed_keys.indexOf(16)>=0) pressed_keys.splice(pressed_keys.indexOf(16),1);
if(pressed_keys.length) ohiHangeul3_moa(f,e);
prev_pressed_keys = pressed_keys.slice();
pressed_keys=[];
pressing_keys=0;
}
}
else if(KE=='Ko' && Ko_type=='2-Gaon26KM') {
if(pressing_keys && !--pressing_keys) {
// 윗글쇠(shift)가 눌렸으면 한글 조합을 끊음
if(pressed_keys.length==1 && pressed_keys[0]==16 && e.keyCode==16) complete_hangeul_syllable(f);
pressed_keys=[];
}
}
else if(KE=='Ko' && Ko_type=='4t-1985') {
// 윗글쇠(shift)를 때에 따라 고정되는 윗글쇠(shift lock)로 씀
if(e.keyCode==16) shift_lock=1;
}
if(e.keyCode==17 && pressing_keys && pressed_keys.indexOf(17)>=0) { // ctrl을 떼었을 때
--pressing_keys;
pressed_keys.splice(pressed_keys.indexOf(17));
}
if(f.id=='inputText') {show_NCR_text();show_direct_typing_text();}
}
function pressed_key_accumulation(f,e,key) {
if(pressed_keys.length && pressed_keys[pressed_keys.length-1]==key) {
// 한 글쇠가 오래 눌려서 같은 문자가 들어왔을 때
ohiHangeul3_moa(f,e);
pressed_keys=[];
pressing_keys=0;
prev_cursor_position = -1;
if(key==8) {
ohiHangeul_moa_backspace(f,e);
return;
}
ohiHangeul3(f,e,key);
}
else {
++pressing_keys;
pressed_keys.push(key);
}
}
function inputText_focus(f) {
// 수정됨: inputText -> wpTextbox1
if(typeof f =='undefined') f=document.getElementById('wpTextbox1');
if(f) f.focus();
}
function inputText_rows(r) { // 글상자(textarea)의 줄 수를 바꿈
// 수정됨: inputText -> wpTextbox1
var f=document.getElementById('wpTextbox1');
if(f) f.rows=r.toString();
}
function url_query() {
var field, value, TF;
var address = unescape(location.href);
var fields = (address.slice(address.indexOf('?')+1,address.length)).split('&');
for(var i=0; i<fields.length; ++i){
field = fields[i].split('=')[0].toLowerCase();
value = fields[i].split('=')[1];
TF = !value || value=='0' || value.toLowerCase=='f' || value.toLowerCase=='false' ? 0 : 1;
if(value===undefined || !value) continue;
if(field=='kbd') { // 기준 자판
if(value.toUpperCase()=='QWERTY' || value.toUpperCase()=='QWERTZ' || value.toUpperCase()=='AZERTY')
ohiChange_KBD_type(value.toUpperCase());
}
else if(field=='en') { // 영문 자판
ohiChange('En',value.toLowerCase());
}
else if(field=='ko' || field=='k2' || field=='k3') { // 한글 자판
ohiChange('Ko',value.toLowerCase());
}
else if(field=='statusbar') { // 오른쪽 아래에 보람줄 나타내기
setTimeout(function(){show_ohiStatusBar(TF);}, 250);
}
else if(field=='sign_ext') { // 기호 확장
ohiChange_enable_sign_ext(TF);
}
else if(field=='double_final_ext' || field=='df_ext') { // 겹받침 확장
ohiChange_enable_double_final_ext(TF);
}
else if(field=='sl' || field=='square layout') {
option.square_layout = TF;
}
else if(field=='normal_typing' || field=='nt') { // 모아치기 자판을 이어치기로 쓰기
option.force_normal_typing = TF;
}
else if(field=='ncr') { // HTML 문자 참조 보이기
option.NCR = TF;
}
else if(field=='ncr_only_cgg') { // 첫가끝 조합형 한글만 HTML 문자 참조로 바꾸어 보이기
converting_option.convert_only_NFD_hangeul_encoding_in_NCR_text = TF;
}
else if(field=='y') { // 신세벌식 자판으로 옛한글 겹낱자 조합하기
option.enable_old_hangeul_input = TF;
ohiChange_enable_old_hangeul_input();
}
else if(field=='hcj') { // 옛한글 자판을 쓸 때에 낱자를 호환용 한글 자모(Hangul Compatibility Jamo)로 넣기
option.use_hangeul_compatibility_jamo_when_entering_old_hangeul = TF;
}
else if(field=='diph' || field=='diphthong') { // 신세벌식 자판으로 옛한글을 조합할 때 오른쪽 아랫글 자리에서 홀소리를 넣을지
option.enable_Sin3_diphthong_key = TF;
}
else if(field=='pw' || field=='phonemic_writing') { // 풀어쓰기
option.phonemic_writing = TF;
}
else if(field=='abbr' || field=='abbreviation') { // 이어치기 자판에서 줄임말 기능 쓰기
option.abbreviation = TF;
}
else if(field=='cc' || field=='convenience_combination') { // 입력 편의를 높이는 한글 편의 조합 쓰기 (조합표가 지정되어 있을 때)
option.convenience_combination = TF;
}
else if(field=='sun' || field=='sunalae') { // 두벌식 자판 순아래 조합
option.sunalae = TF;
}
else if(field == 'row') { // 글상자(textarea)의 줄 수
setTimeout(function(){inputText_rows(value);},250);
}
else if(field == 'sq') { // 가지런한 배열표로 나타내기
option.square_layout = TF;
}
}
}
function tableKey_press(key) {
var shift1 = document.getElementById('key41');
var shift2 = document.getElementById('key52');
if(!option.show_layout || !shift1) return;
shift1.className = shift1.className.substr(0,2);
shift2.className = shift2.className.substr(0,2);
var layout_name = current_layout_info.type_name;
if(key==188) key=44; // , 자리 글쇠
if(key==190) key=46; // . 자리 글쇠
if(key==222) key=39; // ' 자리 글쇠
if(key==219) key=91; // [ 자리 글쇠
if(key==221) key=93; // ] 자리 글쇠
if(key==220) key=92; // \ 자리 글쇠
if(key==173) key=45; // - 자리 글쇠
if(key==191) key=47; // / 자리 글쇠
if(key==192) key=96; // ` 자리 글쇠
if(key==16 || current_layout_info.type_name=='4t-1985'&&shift_lock) {
shift1.className += ' pressed';
shift2.className += ' pressed';
}
var key_td;
for(j=0;j<dkey.length;++j) {
if(j==41 || j==52) continue;
key_td = document.getElementById('key'+j);
key_td.className = key_td.className.replace(/ clicked| pressed/,'');
if(key==dkey[j] || key==ukey[j] || (layout_name.substr(0,3)=='3m-' && !option.force_normal_typing && (pressed_keys.indexOf(dkey[j])>=0 || pressed_keys.indexOf(ukey[j])>=0))) {
key_td.className += ' pressed';
}
if(key==ukey[j] && key!=dkey[j]) {
shift1.className += ' pressed';
shift2.className += ' pressed';
}
}
}
function tableKey_clicked(e, key_num, dk, uk){
inputText_focus();
// 수정됨: inputText -> wpTextbox1
var key, f = document.getElementById('wpTextbox1');
var n=f.nodeName||f.tagName;
if(!f || n!='TEXTAREA') return false;
KE=ohi_KE.substr(0,2);
var shiftlock = document.getElementById('key28');
var shift1 = document.getElementById('key41');
var shift2 = document.getElementById('key52');
if(dk==20) { // 배열표에서 Shift Lock이 눌렸을 때
if(!shiftlock_click) {
shiftlock.style.backgroundColor = 'orange';
shiftlock_click = 1;
}
else {
shiftlock.style.backgroundColor = '';
shiftlock_click = 0;
}
}
if(dk==16 && !shift_click) { // 배열표에서 윗글쇠가 눌렸을 때
shift_click = 1;
shift1.style.backgroundColor = 'orange';
shift2.style.backgroundColor = 'orange';
return;
}
if((dk==32 || dk==13 || dk==9) && !shift_click) { // 사이띄개(32), 줄바꾸개(13), Tab(9)
complete_hangeul_syllable(f);
esc_ext_state();
ohiInsert(f,0,dk);
return;
}
if(dk==8 && !shift_click) { // Backspace
if(option.abbreviation && prev_cursor_position>=0) {
// 이어치기 자판으로 줄임말을 넣은 뒤
ohiHangeul_moa_backspace(f,e);
return false;
}
if(ohiHangeul_backspace(f,e)) return;
ohiBackspace(f);
inputText_focus();
esc_ext_state();
return;
}
if(dk==-1) { // 기준 자판 바꾸기 단추
ohiChange_KBD_type();
inputText_focus();
}
if(dk==-2) { // 영문 자판 바꾸기 단추
ohiChange_between_same_type('En');
}
if(dk==-3) { // 한글 자판 바꾸기 단추
ohiChange_between_same_type('Ko');
inputText_focus();
}
if(dk==-11) { // 3벌식 자판 바꾸기 단추
ohiChange_between_same_type('K3');
inputText_focus();
}
if(dk==-12) { // 2벌식 자판 바꾸기 단추
ohiChange_between_same_type('K2');
}
if(dk==-13) { // 한·영 상태 바꾸기 단추
ohiChange_KE();
inputText_focus();
}
key = (shift_click+shiftlock_click)%2 ? uk : dk;
if(ohi_KE.substr(0,2)=='En' && key>32 && key<127) ohiRoman(f,0,key);
if(ohi_KE.substr(0,2)!='En' && key>32 && key<127) {
if(document.selection && document.selection.createRange().text.length!=1) ohiInsert(f,0,0);
if(KE=='Ko') {
if(current_layout_info.type_name.substr(0,2)=='2-') ohiHangeul2(f,e,key);
else if(!ohiHangeul3_abbreviation(f,key)) ohiHangeul3(f,e,key);
}
}
for(var j=0;j<dkey.length;++j) {
var key_td =document.getElementById('key'+j);
key_td.className = key_td.className.replace(/ clicked| pressed/g,'');
}
if(dk!=16 && dk!=20) document.getElementById('key'+key_num).className += ' clicked';
shift_click = 0;
shift1.style.backgroundColor = '';
shift2.style.backgroundColor = '';
}
function ohi_code_tables() {
var i;
ohi_cheos = [/*ㄱ*/128,/*ㄲ*/129,/*ㄴ*/131,/*ㄷ*/134,/*ㄸ*/135,/*ㄹ*/136,/*ㅁ*/144,/*ㅂ*/145,/*ㅃ*/146,/*ㅅ*/148,/*ㅆ*/149,/*ㅇ*/150,/*ㅈ*/151,/*ㅉ*/152,/*ㅊ*/153,/*ㅋ*/154,/*ㅌ*/155,/*ㅍ*/156,/*ㅎ*/157];
ohi_ga = [/*ㅏ*/66,/*ㅐ*/67,/*ㅑ*/68,/*ㅒ*/69,/*ㅓ*/70,/*ㅔ*/71,/*ㅕ*/72,/*ㅖ*/73,/*ㅗ*/74,/*ㅘ*/75,/*ㅙ*/76,/*ㅚ*/77,/*ㅛ*/78,/*ㅜ*/79,/*ㅝ*/80,/*ㅞ*/81,/*ㅟ*/82,/*ㅠ*/83,/*ㅡ*/84,/*ㅢ*/85,/*ㅣ*/86];
ohi_ggeut = [/*ㄱ*/1,/*ㄲ*/2,/*ㄳ*/3,/*ㄴ*/4,/*ㄵ*/5,/*ㄶ*/6,/*ㄷ*/7,/*ㄹ*/9,/*ㄺ*/10,/*ㄻ*/11,/*ㄼ*/12,/*ㄽ*/13,/*ㄾ*/14,/*ㄿ*/15,/*ㅀ*/16,
/*ㅁ*/17,/*ㅂ*/18,/*ㅄ*/20,/*ㅅ*/21,/*ㅆ*/22,/*ㅇ*/23,/*ㅈ*/24,/*ㅊ*/26,/*ㅋ*/27,/*ㅌ*/28,/*ㅍ*/29,/*ㅎ*/30];
ohi_hangeul_phoneme = ohi_cheos.concat(ohi_ga,ohi_ggeut);
ohi_hotbadchim = [/*ㄱ*/1,/*ㄴ*/4,/*ㄷ*/7,/*ㄹ*/9,/*ㅁ*/17,/*ㅂ*/18,/*ㅅ*/21,/*ㅇ*/23,/*ㅈ*/24,/*ㅊ*/26,/*ㅋ*/27,/*ㅌ*/28,/*ㅍ*/29,/*ㅎ*/30];
unicode_modern_hotbadchim = [/*ㄱ*/0x11A8,/*ㄴ*/0x11AB,/*ㄷ*/0x11AE,/*ㄹ*/0x11AF,/*ㅁ*/0x11B7,/*ㅂ*/0x11B8,/*ㅅ*/0x11BA,/*ㅇ*/0x11BC,/*ㅈ*/0x11BD,/*ㅊ*/0x11BE,/*ㅋ*/0x11BF,/*ㅌ*/0x11C0,/*ㅍ*/0x11C1,/*ㅎ*/0x11C2];
compatibility_cheos = [/*ㄱ*/0x3131,/*ㄲ*/0x3132,/*ㄴ*/0x3134,/*ㄵ*/0x3135,/*ㄶ*/0x3136,/*ㄷ*/0x3137,/*ㄸ*/0x3138,/*ㄹ*/0x3139,/*ㄺ*/0x313A,/*ㄻ*/0x313B,/*ㄼ*/0x313C,/*ㄽ*/0x313D,/*ㅀ*/0x3140,
/*ㅁ*/0x3141,/*ㅂ*/0x3142,/*ㅃ*/0x3143,/*ㅄ*/0x3144,/*ㅅ*/0x3145,/*ㅆ*/0x3146,/*ㅇ*/0x3147,/*ㅈ*/0x3148,/*ㅉ*/0x3149,/*ㅊ*/0x314A,/*ㅋ*/0x314B,/*ㅌ*/0x314C,/*ㅍ*/0x314D,/*ㅎ*/0x314E,
/*ㅥ*/0x3165,/*ㅦ*/0x3166,/*ㅧ*/0x3167,/*ㅪ*/0x316A,/*ㅮ*/0x316E,/*ㅯ*/0x316F,/*ㅱ*/0x3171,/*ㅲ*/0x3172,/*ㅳ*/0x3173,/*ㅴ*/0x3174,/*ㅵ*/0x3175,/*ㅶ*/0x3176,/*ㅷ*/0x3177,/*ㅸ*/0x3178,/*ㅹ*/0x3179,/*ㅺ*/0x317A,/*ㅻ*/0x317B,/*ㅼ*/0x317C,/*ㅽ*/0x317D,/*ㅾ*/0x317E,
/*ㅿ*/0x317F,/*ㆀ*/0x3180,/*ㆁ*/0x3181,/*ㆄ*/0x3184,/*ㆅ*/0x3185,/*ㆆ*/0x3186];
compatibility_modern_cheos = [0x3131,0x3132,0x3134,0x3137,0x3138,0x3139,0x3141,0x3142,0x3143,0x3145,0x3146,0x3147,0x3148,0x3149,0x314A,0x314B,0x314C,0x314D,0x314E, 0x317F,0x3181,0x3186];
i=0x314F; while(i<=0x3163) compatibility_ga.push(i++); compatibility_ga.push(0x318D);
compatibility_ggeut = [0x3131,0x3132,0x3133,0x3134,0x3135,0x3136,0x3137,0x3139,0x313A,0x313B,0x313C,0x313D,0x313E,0x313F,0x3140,0x3141,0x3142,0x3144,0x3145,0x3146,0x3147,0x3148,0x314A,0x314B,0x314C,0x314D,0x314E,
0x317F,0x3181,0x3186];
compatibility_dah = [], compatibility_modern_dah = [], compatibility_hol = [], compatibility_modern_hol = [];
i=0x3131; while(i<=0x314E) {compatibility_dah.push(i); compatibility_modern_dah.push(i++);}
i=0x3165; while(i<=0x3186) compatibility_dah.push(i++);
i=0x314F; while(i<=0x3163) {compatibility_hol.push(i); compatibility_modern_hol.push(i++);}
i=0x3187; while(i<=0x318E) compatibility_hol.push(i++);
compatibility_hangeul_phoneme = compatibility_dah.concat(compatibility_hol);
compatibility_modern_hangeul_phoneme = compatibility_modern_dah.concat(compatibility_modern_hol);
compatibility_cheos_to_NFD_hotdah = [/*ㄱ*/[0x1100],/*ㄲ*/[0x1100,0x1100],/*ㄴ*/[0x1102],/*ㄵ*/[0x1102,0x110C],/*ㄶ*/[0x1102,0x1112],/*ㄷ*/[0x1103],/*ㄸ*/[0x1103,0x1103],/*ㄹ*/[0x1105],/*ㄺ*/[0x1105,0x1100],/*ㄻ*/[0x1105,0x1106],/*ㄼ*/[0x1105,0x1107],/*ㄽ*/[0x1105,0x1109],/*ㅀ*/[0x1105,0x1112],
/*ㅁ*/[0x1106],/*ㅂ*/[0x1107],/*ㅃ*/[0x1107,0x1107],/*ㅄ*/[0x1107,0x1109],/*ㅅ*/[0x1109],/*ㅆ*/[0x1109,0x1109],/*ㅇ*/[0x110B],/*ㅈ*/[0x110C],/*ㅉ*/[0x110C,0x110C],/*ㅊ*/[0x110E],/*ㅋ*/[0x110F],/*ㅌ*/[0x1110],/*ㅍ*/[0x1111],/*ㅎ*/[0x1112],
/*ㅥ*/[0x1102,0x1102],/*ㅦ*/[0x1102,0x1103],/*ㅧ*/[0x1102,0x1109],/*ㅪ*/[0x1105,0x1103],/*ㅮ*/[0x1106,0x1107],/*ㅯ*/[0x1106,0x1109],/*ㅱ*/[0x1106,0x110B],/*ㅲ*/[0x1107,0x1100],/*ㅳ*/[0x1107,0x1103],/*ㅴ*/[0x1107,0x1109,0x1100],/*ㅵ*/[0x1107,0x1109,0x1103],/*ㅶ*/[0x1107,0x110C],/*ㅷ*/[0x1107,0x1110],/*ㅸ*/[0x1107,0x110B],/*ㅹ*/[0x1107,0x1107,0x110B],/*ㅺ*/[0x1109,0x1100],/*ㅻ*/[0x1109,0x1102],/*ㅼ*/[0x1109,0x1103],/*ㅽ*/[0x1109,0x1107],/*ㅾ*/[0x1109,0x110C],
/*ㅿ*/[0x1140],/*ㆀ*/[0x110B,0x110B],/*ㆁ*/[0x114C],/*ㆄ*/[0x1111,0x110B],/*ㆅ*/[0x1112,0x1112],/*ㆆ*/[0x1159]];
compatibility_dah_to_NFD_hotbadchim = [/*ㄱ*/[0x11A8],/*ㄲ*/[0x11A8,0x11A8],/*ㄳ*/[0x11A8,0x11BA],/*ㄴ*/[0x11AB],/*ㄵ*/[0x11AB,0x11BD],/*ㄶ*/[0x11AB,0x11C2],/*ㄷ*/[0x11AE],/*ㄸ*/[0x11AE,0x11AE],/*ㄹ*/[0x11AF],/*ㄺ*/[0x11AF,0x11A8],/*ㄻ*/[0x11AF,0x11B7],/*ㄼ*/[0x11AF,0x11B8],[0x11AF,0x11BA],[0x11AF,0x11C0],[0x11AF,0x11C1],[0x11AF,0x11C2],[0x11AE],[0x11B8],[0x11B8,0x11B8],[0x11B8,0x11BA],[0x11BA],[0x11BA,0x11BA],[0x11BC],[0x11BD],[0x11BD,0x11BD],[0x11BE],[0x11BF],[0x11C0],[0x11C1],[0x11C2],
/*ㅥ*/[0x11AB,0x11AB],/*ㅦ*/[0x11AB,0x11AE],/*ㅧ*/[0x11AB,0x11BA],/*ㅨ*/[0x11AB,0x11EB],/*ㅩ*/[0x11AF,0x11A8,0x11BA],/*ㅪ*/[0x11AF,0x11AE],/*ㅫ*/[0x11AF,0x11B8,0x11BA],/*ㅬ*/[0x11AF,0x11EB],/*ㅭ*/[0x11AF,0x11F9],/*ㅮ*/[0x11B7,0x11B8],/*ㅯ*/[0x11B7,0x11BA],/*ㅰ*/[0x11B7,0x11EB],/*ㅱ*/[0x11B7,0x11BC],/*ㅲ*/[0x11B8,0x11A8],/*ㅳ*/[0x11B8,0x11AE],/*ㅴ*/[0x11B8,0x11BA,0x11A8],/*ㅵ*/[0x11B8,0x11BA,0x11AE],/*ㅶ*/[0x11B8,0x11BD],/*ㅷ*/[0x11B8,0x11C0],/*ㅸ*/[0x11B8,0x11BC],/*ㅹ*/[0x11B8,0x11B8,0x11BC],/*ㅺ*/[0x11BA,0x11A8],/*ㅻ*/[0x11BA,0x11AB],/*ㅼ*/[0x11BA,0x11AE],/*ㅽ*/[0x11BA,0x11B8],/*ㅾ*/[0x11BA,0x11BD],/*ㅿ*/[0x11EB],/*ㆀ*/[0x11BC,0x11BC],/*ㆁ*/[0x11F0],/*ㆂ*/[0x11F0,0x11BA],/*ㆃ*/[0x11F0,0x11EB],/*ㆄ*/[0x11C1,0x11BC],/*ㆅ*/[0x11C2,0x11C2],/*ㆆ*/[0x11F9]];
compatibility_hol_to_NFD_hol = [0x1161,0x1162,0x1163,0x1164,0x1165,0x1166,0x1167,0x1168,0x1169,0x116A,0x116B,0x116C,0x116D,0x116E,0x116F,0x1170,0x1171,0x1172,0x1173,0x1174,0x1175,
/*ㆇ*/0x1184,/*ㆈ*/0x1185,/*ㆉ*/0x1188,/*ㆊ*/0x1191,/*ㆋ*/0x1192,/*ㆌ*/0x1194,/*ㆍ*/0x119E,/*ㆎ*/0x11A1];
compatibility_hol_to_NFD_hothol = [[0x1161],[0x1162],[0x1163],[0x1164],[0x1165],[0x1166],[0x1167],[0x1168],[0x1169],[0x1169,0x1161],[0x1169,0x1162],[0x1169,0x1175],[0x116D],[0x116E],[0x116E,0x1165],[0x116E,0x1166],[0x116E,0x1175],[0x1172],[0x1173],[0x1173,0x1175],[0x1175],
/*ㆇ*/[0x116D,0x1163],/*ㆈ*/[0x116D,0x1164],/*ㆉ*/[0x116D,0x1175],/*ㆊ*/[0x1172,0x1167],/*ㆋ*/[0x1172,0x1168],/*ㆌ*/[0x1172,0x1175],/*ㆍ*/[0x119E],/*ㆎ*/[0x119E,0x1175]];
halfwidth_cheos = [0xFFA1,0xFFA2,0xFFA4,0xFFA7,0xFFA8,0xFFA9,0xFFB1,0xFFB2,0xFFB3,0xFFB5,0xFFB6,0xFFB7,0xFFB8,0xFFB9,0xFFBA,0xFFBB,0xFFBC,0xFFBD,0xFFBE];
for(i=0;i<4;++i) for(j=0;j<(i==3?3:6);++j) halfwidth_ga.push(0xFFC2+i*8+j);
halfwidth_ggeut = [0xFFA1,0xFFA2,0xFFA3,0xFFA4,0xFFA5,0xFFA6,0xFFA7,0xFFA9,0xFFAA,0xFFAB,0xFFAC,0xFFAD,0xFFAE,0xFFAF,0xFFB0,
0xFFB1,0xFFB2,0xFFB4,0xFFB5,0xFFB6,0xFFB7,0xFFB8,0xFFBA,0xFFBB,0xFFBC,0xFFBD,0xFFBE];
halfwidth_hangeul_phoneme = halfwidth_cheos.concat(halfwidth_ga, halfwidth_ggeut);
i=0x1100; while(i<=0x115E) unicode_cheos.push(i++);
i=0xA960; while(i<=0xA97C) unicode_cheos.push(i++);
i=0x1161; while(i<=0x11A7) unicode_ga.push(i++);
i=0xD7B0; while(i<=0xD7C6) unicode_ga.push(i++);
i=0x11A8; while(i<=0x11FF) unicode_ggeut.push(i++);
i=0xD7CB; while(i<=0xD7FB) unicode_ggeut.push(i++);
unicode_ggeut_to_cheos = [0x1100,0x1101,0,0x1102,0,0,0x1103,0x1105,0,0,0,0,0,0,0,0x1106,0x1107,0,0x1109,0x110A,0x110B,0x110C,0x110E,0x110F,0x1110,0x1111,0x1112,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x111D,0,0,0,0x112B,0,0,0,0,0x1140,0,0,0,0,0x114C,0,0,0,0x1157,0,0,0,0,0x1159,0,0,0,0,0,0, // ~ 0x11F9
0,0,0x1104,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x1108,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x110D,0,0]; // 0xD7CB ~ 0xD7FB
unicode_cheos_to_ggeut = [0x11A8,0x11A9,0x11AB,0x11AE,0xD7CD,0x11AF,0x11B7,0x11B8,0xD7E6,0x11BA,0x11BB,0x11BC,0x11BD,0xD7F9,0x11BE,0x11BF,0x11C0,0x11C1,0x11C2,
0,0,0,0,0,0,0,0,0xD7DD,0,0x11E2,0,0,0,0,0,0,0,0,0,0,0,0,0,0x11E6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x11EB,0,0,0,0,0,0,0,0,0,0,0,0x11F0,0,0,0,0,0,0,0,0,0,0,0,0,0x11F9];
unicode_NFD_hangeul_phoneme = unicode_cheos.concat(unicode_ga, unicode_ggeut); // 첫가끝 조합형 한글 낱자
unicode_NFD_hangeul_filler = [0x115F,0x1160]; // 첫가끝 조합형 첫소리·가운뎃소리 채움 문자
unicode_NFD_hangeul_code = unicode_NFD_hangeul_phoneme.concat(unicode_NFD_hangeul_filler); // 첫가끝 조합형에서 한글 낱내자를 나타내는 데에 쓰이는 낱자/채움 부호값들
unicode_NFD_hangeul_sidedot = [0x302E,0x302F]; // 옛한글에서 성조를 나타내는 방점
i=0x1100; while(i<=0x1112) unicode_modern_cheos.push(i++);
i=0x1161; while(i<=0x1175) unicode_modern_ga.push(i++);
i=0x11A8; while(i<=0x11C2) unicode_modern_ggeut.push(i++);
unicode_modern_hangeul_phoneme = unicode_modern_cheos.concat(unicode_modern_ga, unicode_modern_ggeut); // 유니코드 조합형 요즘한글 낱자
unicode_non_combined_cheos = [0x1100,0x1102,0x1103,0x1105,0x1106,0x1107,0x1109,0x110B,0x110C,0x110E,0x110F,0x1110,0x1111,0x1112];
unicode_non_combined_ga = [0x1161,0x1162,0x1164,0x1165,0x1166,0x1167,0x1168,0x1169,0x116D,0x116E,0x1172,0x1173,0x1175,0x119E];
unicode_non_combined_ggeut = [0x11A8,0x11AB,0x11AE,0x11AF,0x11B7,0x11B8,0x11BA,0x11BC,0x11BD,0x11BE,0x11BF,0x11C0,0x11C1,0x11C2];
unicode_non_combined_phoneme = unicode_non_combined_cheos.concat(unicode_non_combined_ga, unicode_non_combined_ggeut);
var han_ext_tag = '<span style="margin:0;padding:0;background:black;color:#fff;letter-spacing:0px;font-size:0.7em;">한글</span>';
special_chars = [-1, 0x08, 0x0D, 0x1B, 0x1160];
special_chars_string = [han_ext_tag, '⌫', '⏎', '🄴', '🄵'];
// 쿼티를 기준으로 한 화상 배열표의 아랫글 자리 부호값
dkey = [96,49,50,51,52,53,54,55,56,57,48,45,61,8,
9,113,119,101,114,116,121,117,105,111,112,91,93,92,
20,97,115,100,102,103,104,106,107,108,59,39,13,
16,122,120,99,118,98,110,109,44,46,47,16,
-1,-2,-3,32,-13,-12,-11];
// 쿼티를 기준으로 한 화상 배열표의 윗글 자리 부호값
ukey = [126,33,64,35,36,37,94,38,42,40,41,95,43,8,
9,81,87,69,82,84,89,85,73,79,80,123,125,124,
20,65,83,68,70,71,72,74,75,76,58,34,13,
16,90,88,67,86,66,78,77,60,62,63,16,
-1,-2,-3,32,-13,-12,-11];
shift_table = [
0x31, /* 0x21 exclam: 1 */
0x27, /* 0x22 quotedbl: apostrophe */
0x33, /* 0x23 numbersign: 3 */
0x34, /* 0x24 dollar: 4 */
0x35, /* 0x25 percent: 5 */
0x37, /* 0x26 ampersand: 7 */
0x22, /* 0x27 apostrophe: quotatioin mark */
0x39, /* 0x28 parenleft */
0x30, /* 0x29 parenright */
0x38, /* 0x2A asterisk: 8 */
0x3D, /* 0x2B plus: equal */
0x3C, /* 0x2C comma: less */
0x5F, /* 0x2D minus: underscore */
0x3E, /* 0x2E period: greater */
0x3F, /* 0x2F slash: question */
0x29, /* 0x30 0: parenright */
0x21, /* 0x31 1: exclam */
0x40, /* 0x32 2: at */
0x23, /* 0x33 3: numbersign */
0x24, /* 0x34 4: dollar */
0x25, /* 0x35 5: percent */
0x5E, /* 0x36 6: asciicircum */
0x26, /* 0x37 7: ampersand */
0x2A, /* 0x38 8: asterisk */
0x28, /* 0x39 9: parenleft */
0x3B, /* 0x3A colon: semicolon */
0x3A, /* 0x3B semicolon: colon */
0x2C, /* 0x3C less: comma */
0x2B, /* 0x3D equal: plus */
0x2E, /* 0x3E greater: period */
0x2F, /* 0x3F question: slash */
0x32, /* 0x40 at: 2 */
0x61, /* 0x41 A: a */
0x62, /* 0x42 B: b */
0x63, /* 0x43 C: c */
0x64, /* 0x44 D: d */
0x65, /* 0x45 E: e */
0x66, /* 0x46 F: f */
0x67, /* 0x47 G: g */
0x68, /* 0x48 H: h */
0x69, /* 0x49 I: i */
0x6A, /* 0x4A J: j */
0x6B, /* 0x4B K: k */
0x6C, /* 0x4C L: l */
0x6D, /* 0x4D M: m */
0x6E, /* 0x4E N: n */
0x6F, /* 0x4F O: o */
0x70, /* 0x50 P: p */
0x71, /* 0x51 Q: q */
0x72, /* 0x52 R: r */
0x73, /* 0x53 S: s */
0x74, /* 0x54 T: t */
0x75, /* 0x55 U: u */
0x76, /* 0x56 V: v */
0x77, /* 0x57 W: w */
0x78, /* 0x58 X: x */
0x79, /* 0x59 Y: y */
0x7A, /* 0x5A Z: z */
0x7B, /* 0x5B bracketleft: braceleft */
0x7C, /* 0x5C backslash: bar */
0x7D, /* 0x5D bracketright: braceright */
0x36, /* 0x5E asciicircum: 6 */
0x2D, /* 0x5F underscore: minus */
0x7E, /* 0x60 quoteleft: asciitilde */
0x41, /* 0x61 a: A */
0x42, /* 0x62 b: B */
0x43, /* 0x63 c: C */
0x44, /* 0x64 d: D */
0x45, /* 0x65 e: E */
0x46, /* 0x66 f: F */
0x47, /* 0x67 g: G */
0x48, /* 0x68 h: H */
0x49, /* 0x69 i: I */
0x4A, /* 0x6A j: J */
0x4B, /* 0x6B k: K */
0x4C, /* 0x6C l: L */
0x4D, /* 0x6D m: M */
0x4E, /* 0x6E n: N */
0x4F, /* 0x6F o: O */
0x50, /* 0x70 p: P */
0x51, /* 0x71 q: Q */
0x52, /* 0x72 r: R */
0x53, /* 0x73 s: S */
0x54, /* 0x74 t: T */
0x55, /* 0x75 u: U */
0x56, /* 0x76 v: V */
0x57, /* 0x77 w: W */
0x58, /* 0x78 x: X */
0x59, /* 0x79 y: Y */
0x5A, /* 0x7A z: Z */
0x5B, /* 0x7B braceleft: bracketleft */
0x5C, /* 0x7C bar: backslash */
0x5D, /* 0x7D braceright: bracketright */
0x60 /* 0x7E asciitilde: quoteleft */
];
} // ohi_code_tables()
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function(searchElement, fromIndex) {
var k;
if(!this) throw new TypeError('"this" is null or not defined');
var O=Object(this);
var len=O.length >>> 0;
if(len===0) return -1;
var n= +fromIndex || 0;
if(Math.abs(n) === Infinity) n=0;
if(n>=len) return -1;
k = Math.max(n>=0 ? n : len-Math.abs(n), 0);
while(k<len) {
var kValue;
if (k in O && O[k] === searchElement) return k;
k++;
}
return -1;
};
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr
if('ab'.substr(-1) != 'b') {
String.prototype.substr = function(substr) {
return function(start, length) {
if(start<0) start=this.length+start;
return substr.call(this, start, length);
}
}(String.prototype.substr);
}
ohi_code_tables();
browser_detect();
// 미디어위키 훅을 사용하여 편집 화면 로드 완료 시 OHI 초기화 및 UI 삽입
$(function() {
mw.hook('wikipage.editform').add(function() {
ohiStart();
// 미디어위키 Edittools 요소 찾기
var $editTools = $('.mw-editTools').length ? $('.mw-editTools') : $('#editpage-specialchars');
// OHI UI 뼈대가 아직 없고 편집도구가 존재한다면 HTML 삽입
if ($editTools.length && $('#ohi_wrap').length === 0) {
var uiHtml =
'<div id="ohi_wrap" style="margin: 10px 0; background:#f8f9fa; border:1px solid #a2a9b1; padding:10px;">' +
'<div id="ohi_menu" style="border-bottom: 1px solid #ccc; padding-bottom: 5px;">' +
'<strong style="font-size:1.1em; color:#202122; margin-right: 15px;">화상 한글 자판 (OHI)</strong>' +
'<div class="this" id="menu1" style="display:inline-block; font-size: 12px; font-weight: bold; padding: 2px 8px; border: 1px solid #c8ccd1; background-color: #eaecf0; cursor: pointer;">자판 배열표</div>' +
'</div>' +
'<div id="ohi_content" style="padding-top: 10px;">' +
'<div id="top_options" style="text-align: left; margin-bottom: 5px;"></div>' +
'<div id="middle_options" style="text-align: left; margin-bottom: 5px;"></div>' +
'<div id="keyboardLayout" style="text-align: center;"></div>' +
'<div id="bottom_options" style="text-align: left;"></div>' +
'</div>' +
'<div id="converting_options">' +
'<div id="NCR_options" class="options"></div><div id="NCR_text" class="converting_text"></div>' +
'<div id="direct_typing_text_options" class="options"></div><div id="direct_typing_text" class="converting_text"></div>' +
'<div id="reverse_direct_typing_text_options" class="options"></div><div id="reverse_direct_typing_text" class="converting_text"></div>' +
'</div>' +
'</div>';
$editTools.after(uiHtml);
}
// 자판 배열표 렌더링 함수 실행
if (typeof show_keyboard_layout === 'function') {
show_keyboard_layout(1);
}
});
});
1opetcbcehejws47d33shlmnjgt9rjt
User:Johannes Richter (WMDE)/test
2
167021
740057
734966
2026-05-01T18:38:38Z
Johannes Richter (WMDE)
61456
test
740057
wikitext
text/x-wiki
According to scientists, the Sun is pretty big.<ref name="Miller" details="[[#CITEREFvan_Riemsdijk1994|van Riemsdijk, 1994]], p. 41">E. Miller, ''The Sun''. New York: Academic Press, 2005</ref> In fact, it is very big. Take their word for it.<ref name="Miller" details="Page 48." /> Don't look directly at the sun!<ref name="Miller" details="Page 23." />
{{sfn|van Riemsdijk|1994|p=41}} = <ref></ref>
[[#CITEREFMiller89|Miller, 1989]], p 2
== Bibliography ==
*{{cite book |last=van Riemsdijk |first=John T. |title=Compound Locomotives: An International Survey |year=1994 |publisher=Atlantic Books |location=Penryn |isbn=0-906899-61-3 }}
== References ==
{{Reflist}}
7p57gct2hqsebfkrildwctuva5hrd0w
User:Ponor/wAwB-worker.js
2
171632
740037
739044
2026-05-01T16:31:57Z
Ponor
47975
+mediawiki.page.gallery.styles
740037
javascript
text/javascript
/*
* wAwB – An in-browser application for automated editing of wiki pages.
* Features: customizable regex or JavaScript search-and-replace rules,
* custom JavaScript pre/post-processing functions and function libraries,
* granular protection or targeting of different parts of wikitext,
* a full-fledged CodeMirror editor, and options to move, delete, and protect pages.
* Author: [[User:Ponor]]
* Documentation: [[User:Ponor/wAwB]]
* License: GNU General Public License (GPL)
*/
//<nowiki>
mw.loader.using([
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows',
'mediawiki.api',
'mediawiki.diff.styles',
'mediawiki.util',
'mediawiki.page.gallery.styles',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-movement',
'oojs-ui.styles.icons-moderation',
'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]).then(function() {
// =====================================================================
// 1. STATE & CONFIGURATION
// =====================================================================
var SCRIPT_TIMEOUT_MS = window.wa_timeout || 5000;
var FETCH_SAFETY_LIMIT = window.wa_fetchLimit || 10000;
var APP_NAME = "wAwB";
var DO_TAG = false;
var SUMMARY_SUFFIX = window.wa_suffix || " [[:en:User:Ponor/wAwB| #wAwB]]";
var APP_VERSION = "0.6";
var DOC_URL = window.wa_docUrl || "https://en.wikipedia.org/wiki/User:Ponor/wAwB";
document.title = window.wa_editIn || "Edit in wAwB";
var PERMS = {
canSave: false,
allowBot: false,
saveDelay: 0
};
var IS_ADMIN = mw.config.get('wgUserGroups').includes('sysop');
var CAN_MOVE = IS_ADMIN || mw.config.get('wgUserGroups').includes('extendedmover') || mw.config.get('wgUserGroups').includes('filemover') || mw.config.get('wgUserGroups').includes('pagemover');
var WIKI = mw.config.get('wgDBname');
var SAVED_RUN = 0;
var SAVED_SESSION = 0;
var currentPageExists = false;
var isRunning = false;
var isFetching = false;
var currentTitle = null;
var currentVars = {};
var currentLibrary = {
name: null,
code: null
};
var originalWikitext = "";
var baseRevId = 0;
var currentViewMode = 'diff';
var autoSaveTimer = null;
var propNamesLoaded = false;
var hasNewSources = false;
var currentHeightMode = 1; // 0=25%, 1=45% (default), 2=72%
var heightValues = ['25%', '45%', '72%'];
// EXTERNAL RULES STATE
var wikiTypos = [];
var localTypos = [];
// LOADING FLAG
var isLoadingProject = false;
// NAMESPACE ALIASES
var nsIds = mw.config.get('wgNamespaceIds');
var catAliases = [],
fileAliases = [];
for (var key in nsIds) {
if (nsIds[key] === 14) catAliases.push(key.replace(/_/g, ' '));
if (nsIds[key] === 6) fileAliases.push(key.replace(/_/g, ' '));
}
catAliases.sort((a, b) => b.length - a.length);
fileAliases.sort((a, b) => b.length - a.length);
var REGEX_CAT_PFX = catAliases.map(mw.util.escapeRegExp).join('|');
var REGEX_FILE_PFX = fileAliases.map(mw.util.escapeRegExp).join('|');
// MASTER PROTECTION DEFINITIONS
var PROTECTION_DEFS = [{
id: 'nowiki',
isOn: true,
label: 'Nowiki: <nowiki>',
regex: /<nowiki>[\s\S]*?<\/nowiki>|<nowiki\s*\/>/gi
},
{
id: 'comments',
isOn: true,
label: 'Comments: <!' + '-- -->',
regex: new RegExp('<!' + '--[\\s\\S]*?--' + '>', 'g')
},
{
id: 'headers',
isOn: false,
label: 'Headers: == Title ==',
regex: /^==+[\s\S]+?==+\s*$/gm
},
{
id: 'templates',
isOn: false,
label: 'Templates: {{...}}',
open: '{{',
close: '}}',
species: null,
regex: null
},
{
id: 'tables',
isOn: false,
label: 'Tables: {|...|}',
open: '\n{|',
close: '\n|}',
regex: null
},
{
id: 'images',
isOn: false,
label: 'Images: [[File:...|...|...]]',
open: '[[',
close: ']]',
species: '(?:' + REGEX_FILE_PFX + ')\\s*:',
regex: null
},
{
id: 'refs',
isOn: true,
label: 'Refs: <ref...',
regex: /<ref[^>]*?\/>|<ref[^>]*?(?<!\/)>[\s\S]*?<\/ref>/gi
},
{
id: 'blocks',
isOn: false,
label: 'Blocks: math, gallery...',
regex: null
},
{
id: 'categories',
isOn: true,
label: 'Categories: [[Category:...]]',
regex: new RegExp('\\[\\[\\s*(' + REGEX_CAT_PFX + ')\\s*:[^\\]]+\\]\\]', 'giu')
},
{
id: 'files',
isOn: true,
label: 'File names: File:...',
regex: new RegExp('(?<=\\[\\[\\s*:?(:?' + REGEX_FILE_PFX + ')\\s*:)[^|\\]]+' + '|^\\s*(?:' + REGEX_FILE_PFX + ')\\s*:([^\\][}{|\\n]{1,150}\\.(?:svg|png|jpe?g|gif|tiff|webp|xcf|mp3|midi|ogg|webm|flac|wav|mpe?g|pdf|djv))', 'gmiu')
},
{
id: 'targets',
isOn: false,
label: 'Targets of [[...|',
regex: /(?<=\[\[:?)[^|\]]+?(?=\||\]\])/g
},
{
id: 'extlinks',
isOn: true,
label: 'External links: [...]',
regex: /(?<=\[)(https?:\/\/|ftps?:\/\/|mailto:)[^\]]+(?=\])/gi
},
{
id: 'urls',
isOn: true,
label: 'URLs: http...',
regex: /https?:\/\/[^\s<>[\]"'`()]+/gi
}
];
// =====================================================================
// 2. CSS STYLES
// =====================================================================
var styles = `
* { box-sizing: border-box; }
#wa-root { font-family: sans-serif; height: 100vh; width: 100vw; overflow: hidden; display: flex; font-size: 14px; }
#wa-left-panel { width: 400px; min-width: 400px; max-width: 400px; background: var(--background-color-base, #fff); border-right: 1px solid #c8ccd1; display: flex; flex-direction: column; z-index: 10; overflow-x: hidden; }
#wa-left-panel h3 { color: #3f6fcf; text-align: center; margin: 12px 0 0 0; }
#wa-username { color: #3f6fcf; text-align: center; margin: 2px 0; font-size: 92%; }
#wa-content-area { flex: 1; padding: 10px 10px 100px 10px; overflow-y: auto; overflow-x: hidden; }
#wa-right-panel { flex: 1; display: flex; flex-direction: column; height: 100%; background: var(--background-color-interactive, #eaecf0); overflow: hidden; }
#wa-visual-output { flex: 0 0 45%; min-height: 0; overflow-y: auto; background: var(--background-color-base, #fff); padding: 20px; border-bottom: 1px solid #c8ccd1; }
.wa-editor-header { flex: 0 0 40px; gap:4em; min-height: 40px; padding: 0 7px; background: var(--background-color-interactive-subtle, #f8f9fa); border-bottom: 1px solid #c8ccd1; color: var(--color-subtle, #54595d); display: flex; justify-content: space-between; align-items: center; white-space: nowrap; z-index: 10; }
.wa-editor-header.wa-dirty { background: var(--background-color-warning-subtle, #fdf2d5); border-bottom: 1px solid #e6a700; }
.wa-header-left { flex: 1; display: flex; align-items: center; overflow: hidden; }
.wa-title-link { font-weight: bold; font-size: 1.1em; color: var(--color-progressive--focus, #36c) !important; text-decoration: none; text-overflow: ellipsis; overflow: hidden; max-width: 300px; }
.wa-title-link:hover { text-decoration: underline; }
.wa-header-sep { margin: 0 10px; border-right: 1px solid #ccc; height: 16px; display: inline-block; }
.wa-unsaved-wrapper { display: none; align-items: center; }
.wa-dirty .wa-unsaved-wrapper { display: inline-flex; }
.wa-unsaved-dot { color: var(--color-destructive, #bf3c2c); font-size: 1.2em; margin-right: 5px; line-height: 1; }
.wa-unsaved-text { color: var(--color-placeholder, #72777d); font-size: 0.9em; font-weight: normal; }
.wa-header-center { flex: 0 0 auto; text-align: center; }
.wa-status-label { font-weight: bold; font-size: 0.85em; color: var(--color-base, #202122); text-transform: uppercase; letter-spacing: 0.5px; }
.wa-status-error { color: var(--color-error, #bf3c2c); }
.wa-status-working { color: var(--color-progressive--focus, #36c); }
.wa-header-right { flex: 1; text-align: right; font-size: 0.85em; color: var(--color-placeholder, #72777d); display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.wa-info-container { margin-right: 10px; }
.wa-tools-container { display: flex; align-items: center; gap: 2px; }
.wa-resize-container { display: flex; flex-direction: column; justify-content: center; height: 100%; margin-left: 10px; padding-left: 5px; border-left: 1px solid #ccc; }
.wa-resize-btn { cursor: pointer; color: #72777d; user-select: none; width: 20px; height: 14px; display: flex; align-items: center; justify-content: center; transition: color 0.1s ease-in-out; }
.wa-resize-btn:hover { color: #36c; }
.wa-resize-btn.wa-resize-disabled { color: #ccc; cursor: default; }
#wa-proc-header { margin-top: 15px !important; border-bottom: none !important; cursor: default; }
#wa-proc-title { font-weight: bold; padding: 10px; display: block; }
#wa-proc-content { padding: 0 10px 15px 10px; }
#wa-editor-area { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--background-color-base, #fff); position: relative; overflow: hidden; }
#wa-editor-textarea { flex: 1; height: 100%; font-family: monospace; font-size: 13px; border: none; outline: none; padding: 10px; resize: none; width: 100%; }
.cm-editor { height: 100% !important; flex: 1; }
.wa-section-header { margin-top: 12px; border-bottom: 1px solid #eee; width: 100%; display: block; margin-left: 0 !important; }
#wa-content-area .wa-section-header:first-child, #wa-content-area .wa-section-header.oo-ui-buttonElement-frameless:first-child { margin-top: 0; margin-left: 0 !important; }
.wa-section-header > .oo-ui-buttonElement-button { text-align: left; padding: 10px 10px !important; margin: 0 !important; display: block; width: 100%; position: relative; border-left: 3px solid #3f6fcf !important; border-radius: 3px !important; background-color: transparent !important; }
.wa-section-header > .oo-ui-buttonElement-button:focus { outline: none !important; }
.wa-section-header .oo-ui-labelElement-label { font-weight: bold; padding-left: 0 !important; margin-left: 0 !important; color: var(--color-base, #202122); }
.wa-section-header .oo-ui-indicatorElement-indicator { position: absolute; right: 10px !important; top: 50%; margin-top: -10px; left: auto !important; width: 20px; }
.wa-foldable-content { display: none; padding: 10px 0; }
.wa-source-options { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; border-top: none; padding: 8px; margin-bottom: 10px; font-size: 0.9em; }
.wa-opt-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 5px; }
.wa-opt-label { font-weight: bold; width: 100%; margin-bottom: 5px; color: var(--color-base, #202122); }
.wa-opt-row > div { margin-top: 8px !important; margin-bottom: 8px !important; }
.wa-rule-row { background: var(--background-color-interactive-subtle, #f8f9fa); border: 1px solid #c8ccd1; padding: 8px; margin-bottom: 8px; border-radius: 4px; display: flex; align-items: stretch; transition: background-color 0.3s; }
.wa-rule-row.wa-highlight { background-color: var(--background-color-interactive, #eaecf0); border-color: #36c; }
.wa-rule-controls { display: flex; flex-direction: column; justify-content: center; gap: 0px; padding-right: 4px; border-right: 1px solid #eee; margin-right: 8px; }
.wa-rule-btn { margin: 0 !important; margin-right: 0 !important; margin-left: 0 !important; }
.wa-rule-btn > .oo-ui-buttonElement-button { margin: 0 !important; }
.wa-rule-content { flex: 1; min-width: 0; }
.wa-rule-opt-row { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
#wa-ns-selector { width: 100%; margin-bottom: 10px; font-family: sans-serif; font-size: 0.9em; border: 1px solid #a2a9b1; }
.wa-lib-dialog > .oo-ui-window-frame { width: 80vw !important; max-width: none !important; height: 80vh !important; max-height: none !important; }
.wa-lib-editorwrapper { height: 100%; border: 1px solid #c8ccd1; position: relative; boxSizing: border-box; }
.wa-page-list-raw textarea { font-family: monospace; font-size: 0.9em; white-space: pre; overflow-x: auto; }
.wa-list-running textarea { background-color: var(--background-color-neutral-subtle, #f8f8f8) !important; color: var(--color-base, #202122) !important; }
.wa-grid-container { display: flex; gap: 6px; margin-bottom: 10px; }
.wa-grid-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.wa-grid-col .oo-ui-buttonWidget { width: 100%; }
.wa-grid-col .oo-ui-buttonWidget .oo-ui-buttonElement-button { width: 100%; text-align: center; justify-content: center; }
.wa-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; }
.wa-list-counter { margin-right: auto; font-weight: bold; color: var(--color-subtle, #54595d); font-size: 0.9em; padding-left: 5px; }
.wa-project-bar { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 10px; margin: 8px 0; justify-content: center; }
.wa-project-bar .oo-ui-buttonElement-button { padding-left: 36px !important; padding-right: 12px !important; font-size: 0.9em; }
.wa-project-bar .oo-ui-iconElement-icon { left: 10px !important; }
.wa-settings-header { font-weight: bold; color: var(--color-subtle, #54595d); margin-bottom: 8px; display: block; text-transform: uppercase; font-size: 0.85em; }
.wa-setting-row { display: flex; align-items: center; margin-bottom: 6px; }
.wa-bot-row { background: var(--background-color-success-subtle, #dff2eb); border: 1px solid #a5d6a7; padding: 8px; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; justify-content: flex-start; gap: 15px; }
table.diff { width: 100%; font-family: "Adwaita Mono", "Courier New", monospace }
table.diff td { vertical-align: top; }
table.diff tr:hover td { background-color: var(--background-color-progressive-subtle--hover, #d9e2ff); cursor: pointer; }
@keyframes wa-pulse-red { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); border-color: #ff0000; } 70% { box-shadow: 0 0 0 6px rgba(255, 0, 0, 0); border-color: #ff0000; } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); border-color: #ff0000; } }
.wa-summary-warning input { animation: wa-pulse-red 1s infinite; border-color: #ff0000 !important; }
`;
$('<style>').text(styles).appendTo('head');
$('body').empty();
// =====================================================================
// 3. HELPER FUNCTIONS
// =====================================================================
function checkPermissions() {
return new Promise(function(resolve) {
var api = new mw.Api();
var projectNs = mw.config.get('wgFormattedNamespaces')[4];
var checkTitles = {
'permissions': projectNs + ':AutoWikiBrowser/CheckPageJSON',
'tag': 'MediaWiki:Tag-wAwB'
};
api.get({
action: 'query',
prop: 'revisions',
titles: Object.values(checkTitles).join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(data) {
var pagePerms = data.query.pages.find(p => p.title === checkTitles['permissions']);
var pageTag = data.query.pages.find(p => p.title === checkTitles['tag']);
DO_TAG = pageTag.missing === undefined;
var userName = mw.config.get('wgUserName');
var userGroups = mw.config.get('wgUserGroups');
var isSysop = userGroups.includes('sysop');
if (!pagePerms.missing) {
try {
var content = pagePerms.revisions[0].slots.main.content;
var json = JSON.parse(content);
var inEnabledUsers = json.enabledusers && json.enabledusers.includes(userName);
var inEnabledBots = json.enabledbots && json.enabledbots.includes(userName);
var isBotGroup = userGroups.includes('bot');
var canSave = inEnabledUsers || inEnabledBots || isSysop;
var allowBot = inEnabledBots && isBotGroup;
resolve({
canSave: canSave,
allowBot: allowBot,
saveDelay: 0
});
} catch (e) {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
} else {
var editCount = mw.config.get('wgUserEditCount');
if (editCount > 500) resolve({
canSave: true,
allowBot: false,
saveDelay: 20000
});
else resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
}
}).catch(function() {
resolve({
canSave: false,
allowBot: false,
saveDelay: 0
});
});
});
}
function getUserCode(widget, globalName) {
var val = widget.getValue().trim();
if (!val || val.startsWith('// Enter')) {
if (window[globalName] && typeof window[globalName] === 'function') {
var s = window[globalName].toString();
return s.substring(s.indexOf('{') + 1, s.lastIndexOf('}'));
}
return "";
}
if (val.startsWith('function')) {
return val.substring(val.indexOf('{') + 1, val.lastIndexOf('}'));
}
return val;
}
function normalizeLine(line) {
if (!line) return null;
// Pass through comments/STOP commands (trimmed)
if (line.trim().startsWith('####')) return line.trim();
// Handle Title|Variables
var parts = line.split('|');
var title = parts[0].trim();
if (!title) return null; // Skip if title is empty
// Reassemble: Clean Title + Original Variables (preserving whitespace)
var rest = parts.length > 1 ? parts.slice(1).join('|') : null;
return title + (rest !== null ? '|' + rest : '');
}
function getNormalizedList(text) {
if (!text) return [];
return text.split('\n')
.map(normalizeLine)
.filter(function(l) {
return l !== null;
});
}
function getDeduplicatedList(text) {
if (!text) return [];
var seen = new Set();
var out = [];
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var clean = normalizeLine(lines[i]);
if (clean && !seen.has(clean)) {
seen.add(clean);
out.push(clean);
}
}
return out;
}
function parseTypoContent(content) {
if (!content) return [];
try {
var $wrapper = $('<body>').html(content);
var rules = [];
$wrapper.find('Typo:not([disabled])').each(function() {
var $t = $(this);
var find = $t.attr('find');
var replace = $t.attr('replace');
if (find && replace !== undefined) {
rules.push({
find: find,
replace: replace,
regex: true,
flags: 'gmu',
enabled: true,
isFunc: false
});
}
});
return rules;
} catch (e) {
return [];
}
}
// =====================================================================
// 4. UI CONSTRUCTION
// =====================================================================
checkPermissions().then(function(pState) {
PERMS = pState;
var $main = $('<div>').attr('id', 'wa-root').appendTo('body');
var $left = $('<div>').attr('id', 'wa-left-panel').appendTo($main);
$left.append($('<h3>').append($('<a>').attr('href', DOC_URL).attr('target', '_blank').text(APP_NAME).css({
'text-decoration': 'none',
'color': 'inherit'
})));
$left.append($('<div>').attr('id', 'wa-username').append($('<a>').attr('href', mw.util.getUrl('Special:Contributions/' + mw.config.get('wgUserName'))).attr('target', '_blank').text('User: ' + mw.config.get('wgUserName')).css({
'text-decoration': 'none',
'color': 'inherit'
})));
var btnSaveProj = new OO.ui.ButtonWidget({
icon: 'download',
label: 'Save project',
framed: false,
flags: 'progressive'
});
var btnLoadProj = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load project',
framed: false
});
var $projBar = $('<div>').addClass('wa-project-bar').append(btnSaveProj.$element, btnLoadProj.$element);
$left.append($projBar);
var $fileInput = $('<input type="file" accept=".json">').hide().appendTo('body');
var $content = $('<div>').attr('id', 'wa-content-area').appendTo($left);
var $right = $('<div>').attr('id', 'wa-right-panel').appendTo($main);
var $editorHeader = $('<div>').addClass('wa-editor-header').appendTo($right);
var $headerLeft = $('<div>').addClass('wa-header-left').appendTo($editorHeader);
var $titleLink = $('<a>').addClass('wa-title-link').text('Page content').attr('target', '_blank').appendTo($headerLeft);
$('<span>').addClass('wa-header-sep').appendTo($headerLeft);
var $unsavedWrapper = $('<span>').addClass('wa-unsaved-wrapper').appendTo($headerLeft);
$('<span>').addClass('wa-unsaved-dot').text('●').appendTo($unsavedWrapper);
$('<span>').addClass('wa-unsaved-text').text('Unsaved').appendTo($unsavedWrapper);
var $headerCenter = $('<div>').addClass('wa-header-center').appendTo($editorHeader);
var $statusLabel = $('<span>').addClass('wa-status-label').text('READY').appendTo($headerCenter);
var $headerRight = $('<div>').addClass('wa-header-right').appendTo($editorHeader);
var $infoContainer = $('<span>').addClass('wa-info-container').appendTo($headerRight);
var $toolsContainer = $('<div>').addClass('wa-tools-container').appendTo($headerRight);
var $resizeContainer = $('<div>').addClass('wa-resize-container').appendTo($headerRight);
var $adminTools = $('<div>').addClass('wa-admin-tools').hide().appendTo($toolsContainer);
// Wide chevron SVGs
var svgUp = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 10 L12 2 L22 10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var svgDown = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="8" viewBox="0 0 24 12"><path d="M2 2 L12 10 L22 2" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var $btnSizeUp = $('<div>').addClass('wa-resize-btn').html(svgUp).attr('title', 'Decrease view size');
var $btnSizeDown = $('<div>').addClass('wa-resize-btn').html(svgDown).attr('title', 'Increase view size');
$resizeContainer.append($btnSizeUp, $btnSizeDown);
function setPanelHeight(modeIndex) {
currentHeightMode = modeIndex;
if (currentHeightMode < 0) currentHeightMode = 0;
if (currentHeightMode > 2) currentHeightMode = 2;
$('#wa-visual-output').css('flex-basis', heightValues[currentHeightMode]);
$btnSizeUp.toggleClass('wa-resize-disabled', currentHeightMode === 0);
$btnSizeDown.toggleClass('wa-resize-disabled', currentHeightMode === 2);
}
$btnSizeUp.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode - 1);
});
$btnSizeDown.on('click', function() {
if (!$(this).hasClass('wa-resize-disabled')) setPanelHeight(currentHeightMode + 1);
});
setPanelHeight(1);
if (CAN_MOVE) {
var btnAdminMove = new OO.ui.ButtonWidget({
icon: 'move',
title: 'Move page to $xA',
disabled: true,
framed: false
});
$adminTools.append(btnAdminMove.$element).show();
}
if (IS_ADMIN) {
var btnAdminDel = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Delete page',
disabled: true,
framed: false
});
var btnAdminProt = new OO.ui.ButtonWidget({
icon: 'lock',
title: 'Protect page',
disabled: true,
framed: false
});
$adminTools.append(btnAdminDel.$element, btnAdminProt.$element).show();
}
var btnWatch = new OO.ui.ButtonWidget({
icon: 'star',
title: 'Watch this page',
framed: false,
disabled: true,
accessKey: 'w'
});
$toolsContainer.append(btnWatch.$element);
var $visualOut = $('<div>').attr('id', 'wa-visual-output').html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready to start...</div>').prependTo($right);
var $editorArea = $('<div>').attr('id', 'wa-editor-area').appendTo($right);
var $textArea = $('<textarea>').attr('id', 'wa-editor-textarea').attr('placeholder', 'Page text will appear here...').appendTo($editorArea);
function setStatus(msg, type) {
if (!msg) msg = "Ready";
$statusLabel.text(msg).removeClass('wa-status-error wa-status-working');
if (type === 'error') $statusLabel.addClass('wa-status-error');
if (type === 'working') $statusLabel.addClass('wa-status-working');
}
// EDITOR OBJECT
var Editor = {
mode: 'textarea',
cmInstance: null,
init: function() {
var self = this;
mw.loader.using(['ext.CodeMirror', 'ext.CodeMirror.mode.mediawiki']).then(function(require) {
try {
self.cmInstance = new(require('ext.CodeMirror'))($textArea[0], (require('ext.CodeMirror.mode.mediawiki')).mediawiki());
self.cmInstance.initialize();
self.mode = 'codemirror';
} catch (e) {
console.error("CM Error", e);
}
}).catch(function(err) {
console.error("CM Load Error:", err);
});
$textArea.on('input', updateDirtyState);
},
getValue: function() {
return (this.mode === 'codemirror' && this.cmInstance) ? this.cmInstance.view.state.doc.toString() : $textArea.val();
},
setValue: function(text) {
$textArea.val(text);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.dispatch({
changes: {
from: 0,
to: this.cmInstance.view.state.doc.length,
insert: text
}
});
} else {
$textArea[0].dispatchEvent(new Event('input'));
}
},
setDisabled: function(d) {
$textArea.prop('disabled', d);
if (this.mode === 'codemirror' && this.cmInstance) {
this.cmInstance.view.contentDOM.contentEditable = !d;
$($textArea).parent().find('.cm-editor').css('opacity', d ? 0.5 : 1);
}
},
scrollToLine: function(n) {
if (isNaN(n)) return;
if (this.mode === 'codemirror' && this.cmInstance) {
var v = this.cmInstance.view;
var l = v.state.doc.line(n);
v.dispatch({
effects: v.constructor.scrollIntoView(l.from, {
y: 'center'
}),
selection: {
anchor: l.from
}
});
v.focus();
}
}
};
var WorkerEngine = {
activeWorker: null,
workerURL: null,
currentLibCode: null,
timeoutTimer: null,
initWorker: function(libCode) {
this.destroy(); // Clean up existing if any
this.currentLibCode = libCode || "";
var scriptContent = this.currentLibCode + "\n\n" + `
self.onmessage = async function(e) {
try {
var data = e.data;
var inputs = data.texts || [data.text];
var vars = data.vars;
var outputs = [];
// Helper to construct async functions dynamically
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function inject(str) {
if (!str) return "";
return str.replace(/\\$x([A-Z]|x)/g, function(m) { return vars[m] || ""; });
}
// Returns a Promise and handles 'await' inside user code
async function execUserFunc(code, currentText, currentVars, sharedObj) {
if (!code || code.trim() === "") return currentText;
try {
var func = new AsyncFunction('text', 'vars', 'shared', code);
var res = await func(currentText, currentVars, sharedObj);
if (res && typeof res === 'object' && res.skip) {
return { _skipSignal: true, reason: res.reason || 'Script-requested skip' };
}
return (res !== undefined) ? res : currentText;
} catch (err) {
throw err; // or: return currentText
}
}
for (var i = 0; i < inputs.length; i++) {
var text = inputs[i];
var shared = {}; // Shared context for this page
// 1. Pre-Process
var preRes;
if (data.preCode && data.preCode.trim() !== "") {
preRes = await execUserFunc(data.preCode, text, vars, shared);
} else if (typeof wAwB_Pre === 'function') {
try {
preRes = await wAwB_Pre(text, vars, shared);
if (preRes && typeof preRes === 'object' && preRes.skip) {
preRes = { _skipSignal: true, reason: preRes.reason || 'Script-requested skip' };
}
} catch (err) { preRes = text; }
} else {
preRes = text;
}
if (preRes && preRes._skipSignal) {
self.postMessage({ skipped: true, reason: preRes.reason });
return;
}
text = (preRes !== undefined) ? preRes : text;
// 2. Rules Processing
if (data.rules && data.rules.length > 0) {
data.rules.forEach(function(rule) {
var findStr = inject(rule.find);
if (!findStr) return;
if (rule.isFunc) {
try {
var userFunc = new Function('match', 'groups', 'vars', 'shared', rule.replace);
text = text.replace(new RegExp(findStr, (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '')), function(...args) {
var match = args[0];
var groups = args.slice(1, -2);
try {
var res = userFunc(match, groups, vars, shared);
return res !== undefined ? res : match;
} catch (err) { return match; }
});
} catch (e) {}
} else {
var repStr = inject(rule.replace).replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
if (rule.regex) {
try {
var flags = (rule.flags || 'gmu').replace(/[^gimsuvy]/g, '');
text = text.replace(new RegExp(findStr, flags), repStr);
} catch (e) {}
} else {
var finalFind = findStr.replace(/\\\\n/g, "\\n").replace(/\\\\t/g, "\\t").replace(/\\\\r/g, "\\r");
text = text.split(finalFind).join(repStr);
}
}
});
}
// 3. Post-Process
var postRes;
if (data.postCode && data.postCode.trim() !== "") {
postRes = await execUserFunc(data.postCode, text, vars, shared);
} else if (typeof wAwB_Post === 'function') {
try {
postRes = await wAwB_Post(text, vars, shared);
if (postRes && typeof postRes === 'object' && postRes.skip) {
postRes = { _skipSignal: true, reason: postRes.reason || 'Script-requested skip' };
}
} catch (err) { postRes = text; }
} else {
postRes = text;
}
if (postRes && postRes._skipSignal) {
self.postMessage({ skipped: true, reason: postRes.reason });
return;
}
text = (postRes !== undefined) ? postRes : text;
outputs.push(text);
}
self.postMessage({ success: true, texts: outputs });
} catch (err) { self.postMessage({ success: false, error: err.toString() }); }
};
`;
var blob = new Blob([scriptContent], {
type: 'application/javascript'
});
this.workerURL = URL.createObjectURL(blob);
this.activeWorker = new Worker(this.workerURL);
},
run: function(payload) {
var self = this;
return new Promise(function(resolve, reject) {
// Re-init if no worker exists, or if the user changed the library code
if (!self.activeWorker || self.currentLibCode !== (payload.libraryCode || "")) {
self.initWorker(payload.libraryCode);
}
if (self.timeoutTimer) clearTimeout(self.timeoutTimer);
self.timeoutTimer = setTimeout(function() {
self.destroy(); // Assassinate the stuck worker
reject("Script timed out (" + SCRIPT_TIMEOUT_MS + "ms).");
}, SCRIPT_TIMEOUT_MS);
self.activeWorker.onmessage = function(e) {
clearTimeout(self.timeoutTimer);
if (e.data.skipped) resolve({
skipped: true,
reason: e.data.reason
});
else if (e.data.success) resolve({
success: true,
texts: e.data.texts
});
else reject(e.data.error);
};
self.activeWorker.postMessage(payload);
});
},
destroy: function() {
if (this.activeWorker) {
this.activeWorker.terminate();
this.activeWorker = null;
}
if (this.workerURL) {
URL.revokeObjectURL(this.workerURL);
this.workerURL = null;
}
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
}
};
var PageProtector = {
store: [],
getKey: function() {
var id = this.store.length.toString();
var p = "";
for (var i = 0; i < id.length; i++) {
p += String.fromCharCode(0xE010 + parseInt(id[i]));
}
return '\uE000' + p + '\uE001';
},
protect: function(text, mode, config, templateSpecies = null) {
this.store = [];
var self = this;
var safeRep = function(t, r) {
return t.replace(r, function(m) {
if (!m) return m;
var key = self.getKey();
self.store.push(m);
return key;
});
};
var shouldProcess = function(id) {
if (mode === 'target') return config === id;
return config[id] === true;
};
var matchedBrackets = function(text, op, cl, species = '') {
var newText = "",
depth = 0,
start = 0,
cursor = 0;
var speciesRegex = species ? new RegExp(species, 'iu') : null;
for (var i = 0; i < text.length; i++) {
if (text[i] === op[0] && text.slice(i, i + op.length) === op) {
if (depth === 0) start = i;
depth++;
i += op.length - 1;
} else if (text[i] === cl[0] && text.slice(i, i + cl.length) === cl) {
if (depth > 0) {
depth--;
if (depth === 0) {
var chunk = text.substring(start, i + cl.length);
if (!speciesRegex || speciesRegex.test(chunk)) {
var key = self.getKey();
self.store.push(chunk);
newText += text.substring(cursor, start) + key;
} else {
newText += text.substring(cursor, i + cl.length);
}
cursor = i + cl.length;
}
i += cl.length - 1;
}
}
}
newText += text.substring(cursor);
return newText;
};
PROTECTION_DEFS.forEach(function(def) {
if (shouldProcess(def.id)) {
if (def.id === 'blocks') {
['math', 'pre', 'source', 'syntaxhighlight', 'code', 'gallery'].forEach(t => text = safeRep(text, new RegExp('<' + t + '[^>]*?>[\\s\\S]*?<\\/' + t + '>|<' + t + '[^>]*?/>', 'gi')));
} else if (['templates', 'tables', 'images'].includes(def.id)) {
var activeSpecies = (def.id === 'templates') ? templateSpecies : def.species;
text = matchedBrackets(text, def.open, def.close, activeSpecies || '');
} else if (def.regex) {
text = safeRep(text, def.regex);
}
}
});
return text;
},
restore: function(text) {
var self = this;
var loop = 100;
while (/(\uE000[\uE010-\uE019]+\uE001)/.test(text) && loop > 0) {
text = text.replace(/\uE000([\uE010-\uE019]+)\uE001/g, function(m, d) {
var id = "";
for (var i = 0; i < d.length; i++) id += (d.charCodeAt(i) - 0xE010).toString();
return self.store[parseInt(id, 10)] || m;
});
loop--;
}
return text;
}
};
var accordionRegistry = [];
function addSection(title, $inner) {
var btn = new OO.ui.ButtonWidget({
label: title,
indicator: 'down',
framed: false,
classes: ['wa-section-header']
});
var box = $('<div>').addClass('wa-foldable-content').append($inner);
var sectionObj = {
btn: btn,
box: box,
label: title
};
accordionRegistry.push(sectionObj);
btn.on('click', function() {
var isOpening = !box.is(':visible');
if (isOpening) {
accordionRegistry.forEach(function(sec) {
if (sec !== sectionObj) {
sec.box.hide();
sec.btn.setIndicator('down');
}
});
}
box.toggle();
btn.setIndicator(box.is(':visible') ? 'up' : 'down');
});
$content.append(btn.$element, box);
return sectionObj;
}
// WIDGETS
var srcSelect = new OO.ui.DropdownInputWidget({
options: [{
data: 'cat',
label: 'Category'
}, {
data: 'linksto',
label: 'Pages linking to...'
}, {
data: 'linkson',
label: 'Links on page...'
}, {
data: 'prefix',
label: 'Pages with prefix...'
}, {
data: 'watchlist',
label: 'Watchlist'
}, {
data: 'search',
label: 'Wiki search'
}, {
data: 'usercontribs',
label: 'User contributions'
}, {
data: 'pageswithprop',
label: 'Pages with property'
}]
});
var srcInput = new OO.ui.TextInputWidget({
placeholder: 'Category...'
});
var now = new Date();
var today = now.toISOString().split('T')[0];
var srcInputUser = new OO.ui.TextInputWidget({
placeholder: 'Username'
});
var srcInputStartDate = new OO.ui.TextInputWidget({
value: today + 'T00:00:00',
placeholder: 'ISO start date'
});
var srcInputEndDate = new OO.ui.TextInputWidget({
value: today + 'T23:59:59',
placeholder: 'ISO end date'
});
var srcDropProp = new OO.ui.DropdownInputWidget({
options: []
});
var $optContainer = $('<div>').addClass('wa-source-options').hide();
var $optCat = $('<div>').hide();
var $optUser = $('<div>').hide();
var $optProp = $('<div>').hide();
var chkCatPages = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkCatSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkCatFile = new OO.ui.CheckboxInputWidget({
selected: false
});
$optCat.append($('<div>').addClass('wa-opt-label').text('Include:'), new OO.ui.FieldLayout(chkCatPages, {
label: 'Pages',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatSub, {
label: 'Subcats',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkCatFile, {
label: 'Files',
align: 'inline'
}).$element);
$optUser.append(new OO.ui.FieldLayout(srcInputUser, {
label: 'User',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputStartDate, {
label: 'Start (Older)',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInputEndDate, {
label: 'End (Newer)',
align: 'top'
}).$element);
$optProp.append(new OO.ui.FieldLayout(srcDropProp, {
label: 'Property',
align: 'top'
}).$element);
var $optLinks = $('<div>').hide();
var chkLinkWiki = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkLinkTrans = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkLinkImg = new OO.ui.CheckboxInputWidget({
selected: false
});
var dropLinkRedir = new OO.ui.DropdownInputWidget({
options: [{
data: 'nonredirects',
label: 'No redirects'
}, {
data: 'all',
label: 'Both'
}, {
data: 'redirects',
label: 'Redirects only'
}]
});
var chkLinkToRedir = new OO.ui.CheckboxInputWidget({
selected: false
});
$optLinks.append($('<div>').addClass('wa-opt-label').text('What to include:'), $('<div>').addClass('wa-opt-row').append(new OO.ui.FieldLayout(chkLinkWiki, {
label: 'Wikilinks',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkTrans, {
label: 'Transclusions',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkLinkImg, {
label: 'File usage',
align: 'inline'
}).$element), $('<div>').addClass('wa-opt-label').text('Redirects:'), dropLinkRedir.$element, new OO.ui.FieldLayout(chkLinkToRedir, {
label: 'Include links to redirects',
align: 'inline'
}).$element);
$optContainer.append($optCat, $optLinks, $optUser, $optProp);
var queryCache = {};
var lastMode = 'cat';
srcSelect.on('change', function(newMode) {
if (!isLoadingProject) {
if (lastMode !== 'watchlist' && lastMode !== 'usercontribs' && lastMode !== 'pageswithprop') {
queryCache[lastMode] = srcInput.getValue();
}
}
$optContainer.hide();
$optCat.hide();
$optLinks.hide();
$optUser.hide();
$optProp.hide();
srcInput.setDisabled(false).$element.show();
if (newMode === 'cat') {
$optContainer.show();
$optCat.show();
} else if (newMode === 'linksto') {
$optContainer.show();
$optLinks.show();
} else if (newMode === 'usercontribs') {
$optContainer.show();
$optUser.show();
srcInput.setDisabled(true).$element.hide();
} else if (newMode === 'pageswithprop') {
$optContainer.show();
$optProp.show();
srcInput.setDisabled(true).$element.hide();
if (!propNamesLoaded) {
new mw.Api().get({
action: 'query',
list: 'pagepropnames',
ppnlimit: 'max'
}).then(function(d) {
if (d.query && d.query.pagepropnames) {
srcDropProp.setOptions(d.query.pagepropnames.map(p => ({
data: p.propname,
label: p.propname
})));
propNamesLoaded = true;
}
});
}
}
if (newMode === 'watchlist') {
srcInput.setValue('');
srcInput.setDisabled(true);
srcInput.$input.attr('placeholder', '(No query needed)');
} else if (newMode !== 'usercontribs' && newMode !== 'pageswithprop') {
srcInput.setValue(queryCache[newMode] || '');
var ph = 'Query...';
if (newMode === 'cat') ph = 'Category name';
if (newMode === 'search') ph = 'Search query...';
if (newMode === 'prefix') ph = 'Page prefix...';
if (newMode === 'linksto') ph = 'Pages linking to this title...';
if (newMode === 'linkson') ph = 'Get links from this page...';
srcInput.$input.attr('placeholder', ph);
}
lastMode = newMode;
});
srcSelect.emit('change', srcSelect.getValue());
var $nsSelect = $('<select>').attr('id', 'wa-ns-selector').attr('multiple', 'multiple').attr('size', '8');
var nsMap = mw.config.get('wgFormattedNamespaces');
for (var id in nsMap) {
if (parseInt(id) >= 0) $nsSelect.append($('<option>').val(id).text(id + ': ' + (nsMap[id] || '(Main)')));
}
$nsSelect.val(['0']);
var btnAdd = new OO.ui.ButtonWidget({
label: 'Add to list',
icon: 'add',
flags: ['primary', 'progressive']
});
var $btnRow = $('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-top': '10px'
});
var $fetchStatus = $('<span>').css({
'margin-right': '10px',
'color': '#888',
'font-size': '0.85em',
'align-self': 'center'
}).hide();
$btnRow.append($fetchStatus, btnAdd.$element);
addSection('Source', $('<div>').append(new OO.ui.FieldLayout(srcSelect, {
label: 'Mode',
align: 'top'
}).$element, new OO.ui.FieldLayout(srcInput, {
label: 'Query',
align: 'top'
}).$element, $optContainer, $('<div>').text('Namespaces:').css({
'font-weight': 'bold',
'margin-top': '5px'
}), $nsSelect, $btnRow));
var redirMode = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'edit',
label: 'Edit the redirect page (Default)'
}), new OO.ui.RadioOptionWidget({
data: 'follow',
label: 'Follow redirect (Edit target)'
}), new OO.ui.RadioOptionWidget({
data: 'skip',
label: 'Skip redirects'
})]
});
redirMode.selectItemByData('edit');
var radSkipExist = new OO.ui.RadioSelectWidget({
items: [new OO.ui.RadioOptionWidget({
data: 'none',
label: 'Process all'
}), new OO.ui.RadioOptionWidget({
data: 'missing',
label: 'Skip if page does not exist'
}), new OO.ui.RadioOptionWidget({
data: 'exists',
label: 'Skip if page exists'
})]
});
radSkipExist.selectItemByData('none');
var chkSkipNoChange = new OO.ui.CheckboxInputWidget({
selected: false
});
var inpSkipContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if FOUND'
});
var togSkipContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipNotContains = new OO.ui.TextInputWidget({
placeholder: 'Text/Regex for Skip if MISSING'
});
var togSkipNotContainsRegex = new OO.ui.ToggleSwitchWidget({
value: false,
title: 'Use regex'
});
var inpSkipCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if in: Category1|Category2'
});
var inpSkipNotCategories = new OO.ui.TextInputWidget({
placeholder: 'Skip if NOT in: Category1|Category2'
});
var $settingsPanel = $('<div>')
.append($('<span>').addClass('wa-settings-header').text('Redirects'))
.append(redirMode.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Skip logic'))
.append(new OO.ui.FieldLayout(chkSkipNoChange, {
label: 'Skip if no changes made',
align: 'inline'
}).$element.css('margin-bottom', '8px'))
.append(radSkipExist.$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Content filters'))
.append($('<div>').addClass('wa-setting-row').append(inpSkipContains.$element.css('flex', 1), togSkipContainsRegex.$element.css('margin-left', '5px')))
.append($('<div>').addClass('wa-setting-row').append(inpSkipNotContains.$element.css('flex', 1), togSkipNotContainsRegex.$element.css('margin-left', '5px')))
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($('<span>').addClass('wa-settings-header').text('Category filters'))
.append(new OO.ui.FieldLayout(inpSkipCategories, {
label: 'Blacklist',
align: 'top'
}).$element)
.append(new OO.ui.FieldLayout(inpSkipNotCategories, {
label: 'Whitelist',
align: 'top'
}).$element);
addSection('Skip', $settingsPanel);
var dropProtMode = new OO.ui.DropdownInputWidget({
options: [{
data: 'protect',
label: 'Protect (Exclude)'
}, {
data: 'target',
label: 'Target (Edit Matches Only)'
}]
});
var inpTemplateFilter = new OO.ui.TextInputWidget({
placeholder: 'Regex: infobox rail line|railway'
});
var $templateFilterLayout = new OO.ui.FieldLayout(inpTemplateFilter, {
label: 'Template filter',
align: 'top'
});
var $protList = $('<div>');
var protCheckboxes = {};
PROTECTION_DEFS.forEach(function(def) {
var chk = new OO.ui.CheckboxInputWidget({
selected: def.isOn
});
protCheckboxes[def.id] = chk;
$protList.append(new OO.ui.FieldLayout(chk, {
label: def.label,
align: 'inline'
}).$element);
});
var targetRadioItems = PROTECTION_DEFS.map(function(def) {
return new OO.ui.RadioOptionWidget({
data: def.id,
label: def.label
});
});
var radTargetSet = new OO.ui.RadioSelectWidget({
items: targetRadioItems
});
var $targetList = $('<div>').hide().append(radTargetSet.$element);
dropProtMode.on('change', function(mode) {
if (mode === 'protect') {
$protList.show();
$targetList.hide();
} else {
$protList.hide();
$targetList.show();
}
});
addSection('Protection', $('<div>').addClass('wa-source-options')
.append(new OO.ui.FieldLayout(dropProtMode, {
label: 'Mode',
align: 'top'
}).$element)
.append($('<hr>').css('border-top', '1px solid #eee'))
.append($protList).append($targetList)
.append($('<div style="margin-top:10px;">').append($templateFilterLayout.$element))
);
var $rulesList = $('<div>');
var btnAddRule = new OO.ui.ButtonWidget({
label: 'Add rule',
icon: 'add'
});
var rulesRegistry = [];
addSection('Rules', $('<div>').append($rulesList, btnAddRule.$element));
var togWikiTypos = new OO.ui.ToggleSwitchWidget({
value: false
});
var lblWikiStatus = $('<div>').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var btnLoadLocal = new OO.ui.ButtonWidget({
icon: 'upload',
label: 'Load file',
framed: false
});
var btnClearLocal = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Clear local',
framed: false,
flags: 'destructive',
disabled: true
});
var lblLocalStatus = $('<div>').text('No local rules').css({
'font-size': '0.85em',
'color': '#888',
'margin-top': '2px'
});
var $typoInput = $('<input type="file">').hide().appendTo('body');
var $extRulesPanel = $('<div>').addClass('wa-source-options');
$extRulesPanel.append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between'
}).append($('<span>').text('Project:AutoWikiBrowser/Typos').css('font-weight', 'bold'), togWikiTypos.$element),
$('<div>').css('margin-bottom', '10px').append(lblWikiStatus),
$('<hr>').css('border-top', '1px solid #eee'),
$('<div>').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Local rules (session only)').css({
'font-weight': 'bold'
}), $('<div>').css('flex', '1'), btnLoadLocal.$element, btnClearLocal.$element), lblLocalStatus)
);
addSection('External rules', $extRulesPanel);
var txtPreScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var txtPostScript = new OO.ui.MultilineTextInputWidget({
rows: 6,
value: '',
placeholder: '// Enter JavaScript function body here.\n// Available variables: text, vars, shared\nreturn text;'
});
var btnLoadLib = new OO.ui.ButtonWidget({
icon: 'upload',
title: 'Load library (.js)',
framed: false
});
var btnRemoveLib = new OO.ui.ButtonWidget({
icon: 'trash',
title: 'Remove library',
framed: false,
flags: 'destructive'
});
var txtLibStatus = new OO.ui.TextInputWidget({
value: '(No library loaded)',
readOnly: true
});
var $libInput = $('<input type="file" accept=".js">').hide().appendTo('body');
var btnEditLib = new OO.ui.ButtonWidget({
icon: 'edit',
label: 'Edit project library',
framed: false
});
var $scriptPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '5px',
'margin-bottom': '10px'
}).append($('<span>').text('JS library:').css({
'font-weight': 'bold',
'white-space': 'nowrap'
}), txtLibStatus.$element.css('flex', '1'), btnLoadLib.$element, btnRemoveLib.$element),
$('<div>').css({
'display': 'flex',
'justify-content': 'flex-end',
'margin-bottom': '10px'
}).append(btnEditLib.$element),
new OO.ui.FieldLayout(txtPreScript, {
label: 'Pre-Process',
align: 'top'
}).$element,
new OO.ui.FieldLayout(txtPostScript, {
label: 'Post-Process',
align: 'top'
}).$element
);
addSection('Scripts', $scriptPanel);
function updateLibUI() {
if (currentLibrary.code) {
txtLibStatus.setValue(currentLibrary.name);
btnRemoveLib.setDisabled(false);
} else {
txtLibStatus.setValue('(No library loaded)');
btnRemoveLib.setDisabled(true);
}
}
updateLibUI();
function LibraryEditorDialog(config) {
LibraryEditorDialog.super.call(this, config);
}
OO.inheritClass(LibraryEditorDialog, OO.ui.ProcessDialog);
LibraryEditorDialog.static.name = 'libraryEditor';
LibraryEditorDialog.static.title = 'Edit project library';
LibraryEditorDialog.static.actions = [{
action: 'save',
label: 'Save',
flags: ['primary', 'progressive']
},
{
label: 'Cancel',
flags: 'safe'
}
];
LibraryEditorDialog.prototype.initialize = function() {
LibraryEditorDialog.super.prototype.initialize.call(this);
this.$element.addClass('wa-lib-dialog'); // Attach our custom CSS override class
this.panel = new OO.ui.PanelLayout({
padded: true,
expanded: true
});
this.$editorWrapper = $('<div>').addClass('wa-lib-editorwrapper');
this.panel.$element.append(this.$editorWrapper);
this.$body.append(this.panel.$element);
};
LibraryEditorDialog.prototype.getSetupProcess = function(data) {
data = data || {};
return LibraryEditorDialog.super.prototype.getSetupProcess.call(this, data)
.next(function() {
var self = this;
self.$editorWrapper.empty();
// Create a textarea for the MediaWiki CM wrapper to properly bind to
var $libTextArea = $('<textarea>').appendTo(self.$editorWrapper);
var initCode = currentLibrary.code || "// All custom library functions defined here will be passed to the worker.\n// Special functions:\n// function wAwB_Pre(text, vars, shared) { return text; }\n// function wAwB_Post(text, vars, shared) { return text; }\n";
return mw.loader.using(['ext.CodeMirror', 'ext.CodeMirror.modes']).then(function(require) {
var CM = require('ext.CodeMirror');
var modes = require('ext.CodeMirror.modes');
self.cmInstance = new CM($libTextArea[0], modes.javascript());
self.cmInstance.initialize();
self.cmInstance.view.dispatch({
changes: {
from: 0,
insert: initCode
}
});
// Force CodeMirror to fill the wrapper
self.$editorWrapper.find('.cm-editor').css({
height: '100%'
});
}).catch(function(err) {
console.error("wAwB CM Init Error:", err);
});
}, this);
};
LibraryEditorDialog.prototype.getActionProcess = function(action) {
var dialog = this;
if (action === 'save') {
return new OO.ui.Process(function() {
var newCode = "";
if (dialog.cmInstance) {
newCode = dialog.cmInstance.view.state.doc.toString();
}
if (newCode.trim() === "") {
currentLibrary = {
name: null,
code: null
};
} else {
currentLibrary.code = newCode;
currentLibrary.name = "custom code";
}
updateLibUI();
dialog.close({
action: action
});
});
}
if (action === 'cancel' || !action) {
return new OO.ui.Process(function() {
dialog.close({
action: action
});
});
}
return LibraryEditorDialog.super.prototype.getActionProcess.call(this, action);
};
LibraryEditorDialog.prototype.getTeardownProcess = function(data) {
return LibraryEditorDialog.super.prototype.getTeardownProcess.call(this, data)
.next(function() {
if (this.cmInstance) {
try {
this.cmInstance.view.destroy();
} catch (e) {}
this.cmInstance = null;
}
}, this);
};
var windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
var libDialog = new LibraryEditorDialog();
windowManager.addWindows([libDialog]);
btnEditLib.on('click', function() {
windowManager.openWindow(libDialog);
});
var togAdminEnable = new OO.ui.ToggleSwitchWidget({
value: false
});
var chkMovRedirect = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkMovTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var chkMovSub = new OO.ui.CheckboxInputWidget({
selected: false
});
var chkDelTalk = new OO.ui.CheckboxInputWidget({
selected: true
});
var dropProtEdit = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var dropProtMove = new OO.ui.DropdownInputWidget({
options: [{
data: '',
label: '(No Change)'
}, {
data: 'all',
label: 'All'
}, {
data: 'autoconfirmed',
label: 'Autoconfirmed'
}, {
data: 'sysop',
label: 'Sysop'
}]
});
var inpProtExpiry = new OO.ui.TextInputWidget({
placeholder: 'infinite / 2 days / 12 hours'
});
if (CAN_MOVE || IS_ADMIN) {
var $adminPanel = $('<div>').append(
$('<div>').css({
'display': 'flex',
'align-items': 'center',
'justify-content': 'flex-start',
'gap': '10px'
}).append($('<span>').text('Enable page actions').css('font-weight', 'bold'), togAdminEnable.$element),
$('<hr>')
);
if (CAN_MOVE) {
$adminPanel.append(
$('<strong>').text('Move options:'), new OO.ui.FieldLayout(chkMovRedirect, {
label: 'Do not create redirect',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovTalk, {
label: 'Move talk page',
align: 'inline'
}).$element, new OO.ui.FieldLayout(chkMovSub, {
label: 'Move subpages',
align: 'inline'
}).$element, $('<br>')
);
}
if (IS_ADMIN) {
$adminPanel.append(
$('<strong>').text('Delete options:'), new OO.ui.FieldLayout(chkDelTalk, {
label: 'Delete talk page',
align: 'inline'
}).$element, $('<br>'),
$('<strong>').text('Protect options:'), new OO.ui.FieldLayout(dropProtEdit, {
label: 'Edit level',
align: 'top'
}).$element, new OO.ui.FieldLayout(dropProtMove, {
label: 'Move level',
align: 'top'
}).$element, new OO.ui.FieldLayout(inpProtExpiry, {
label: 'Expiry',
align: 'top'
}).$element
);
}
addSection('Page actions', $adminPanel);
}
var btnPower = new OO.ui.ButtonWidget({
label: 'Start',
icon: 'power',
flags: ['primary', 'progressive'],
title: 'Start editing',
accessKey: 'a'
});
var btnDiff = new OO.ui.ButtonWidget({
label: 'Diff',
icon: 'update',
title: 'Show diff',
accessKey: 'd'
});
var btnSkip = new OO.ui.ButtonWidget({
label: 'Next',
icon: 'next',
title: 'Skip to next page',
accessKey: 'n',
disabled: true
});
var btnPreview = new OO.ui.ButtonWidget({
label: 'Preview',
icon: 'article',
title: 'Preview page',
accessKey: 'p'
});
var btnSave = new OO.ui.ButtonWidget({
label: 'Save',
icon: 'upload',
flags: 'progressive',
title: 'Save edit',
accessKey: 's',
disabled: true
});
var inputSummary = new OO.ui.TextInputWidget({
placeholder: '',
value: '',
title: 'Enter edit summary',
accessKey: 'b'
});
var $sumLayout = new OO.ui.FieldLayout(inputSummary, {
label: 'Edit summary',
align: 'top'
}).$element;
$sumLayout.css('margin-bottom', '6px');
var listTextarea = new OO.ui.MultilineTextInputWidget({
rows: 15,
classes: ['wa-page-list-raw']
});
var btnSort = new OO.ui.ButtonWidget({
icon: 'sortVertical',
framed: false
});
var btnDedup = new OO.ui.ButtonWidget({
icon: 'funnel',
framed: false
});
var btnClear = new OO.ui.ButtonWidget({
icon: 'trash',
framed: false
});
var btnPreParse = new OO.ui.ButtonWidget({
label: 'Pre-parse',
title: 'Process list in background',
icon: 'robot',
framed: false
});
var $listCounter = $('<span>').addClass('wa-list-counter').text('0 pages');
var togAutoSave = new OO.ui.ToggleSwitchWidget({
value: false
});
var txtAutoDelay = new OO.ui.TextInputWidget({
value: '10'
});
var $botRow = $('<div>').addClass('wa-bot-row').hide();
if (PERMS.allowBot) {
$botRow.show().append($('<span>').css('font-weight', 'bold').text('Bot mode: '), togAutoSave.$element, $('<span>').text('Delay (s):'), txtAutoDelay.$element.css('max-width', '40px'));
togAutoSave.on('change', function(v) {
if (v) txtAutoDelay.setValue('10');
});
}
var sortAsc = true;
var $procHeader = $('<div>').addClass('wa-section-header').attr('id', 'wa-proc-header').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center'
});
var $procTitle = $('<span>').attr('id', 'wa-proc-title').text('Processing');
var chkMinor = new OO.ui.CheckboxInputWidget({
selected: true,
title: 'Minor edit'
});
var $minorLayout = new OO.ui.FieldLayout(chkMinor, {
label: 'm',
align: 'inline',
title: 'Minor edit'
});
$minorLayout.$element.css({
'margin-right': '15px',
'font-weight': 'normal'
});
$procHeader.append($procTitle, $minorLayout.$element);
var $procContent = $('<div>').attr('id', 'wa-proc-content').append(
$sumLayout, $botRow,
$('<div>').addClass('wa-grid-container').append(
$('<div>').addClass('wa-grid-col').append(btnPower.$element),
$('<div>').addClass('wa-grid-col').append(btnDiff.$element, btnSkip.$element),
$('<div>').addClass('wa-grid-col').append(btnPreview.$element, btnSave.$element)
),
$('<div>').addClass('wa-toolbar').append($listCounter, btnSort.$element, btnDedup.$element, btnClear.$element),
listTextarea.$element,
$('<div>').css({
'margin-top': '5px'
}).append(btnPreParse.$element)
);
$content.append($procHeader, $procContent);
var configWidgets = [
srcSelect, srcInput, srcInputUser, srcInputStartDate, srcInputEndDate, srcDropProp,
chkCatPages, chkCatSub, chkCatFile, chkLinkWiki, chkLinkTrans, chkLinkImg, dropLinkRedir, chkLinkToRedir,
btnAdd, redirMode, chkSkipNoChange, radSkipExist,
inpSkipContains, togSkipContainsRegex, inpSkipNotContains, togSkipNotContainsRegex, inpSkipCategories, inpSkipNotCategories,
dropProtMode, radTargetSet, inpTemplateFilter, btnAddRule,
txtPreScript, txtPostScript, chkMovRedirect, chkMovTalk, chkMovSub, chkDelTalk, dropProtEdit, dropProtMove, inpProtExpiry,
togWikiTypos, btnLoadLocal, btnClearLocal, btnPreParse
];
// =====================================================================
// 5. FUNCTION DEFINITIONS (Core Logic)
// =====================================================================
function checkSummaryWarning() {
var val = inputSummary.getValue();
var isBlank = !val || val.trim() === "";
if (isBlank || hasNewSources) inputSummary.$element.addClass('wa-summary-warning');
else inputSummary.$element.removeClass('wa-summary-warning');
}
function renderCurrentView() {
if (currentViewMode === 'preview') renderPreview();
else renderDiff();
}
function toggleConfig(isLocked) {
configWidgets.forEach(function(w) {
if (w instanceof OO.ui.TextInputWidget || w instanceof OO.ui.MultilineTextInputWidget) {
w.setReadOnly(isLocked);
w.$element.css('opacity', isLocked ? 0.8 : 1);
} else {
w.setDisabled(isLocked);
}
});
$nsSelect.prop('disabled', isLocked);
for (var key in protCheckboxes) protCheckboxes[key].setDisabled(isLocked);
rulesRegistry.forEach(function(r) {
r.find.setReadOnly(isLocked);
r.rep.setReadOnly(isLocked);
r.regex.setDisabled(isLocked);
r.flags.setReadOnly(isLocked);
r.enable.setDisabled(isLocked);
r.del.setDisabled(isLocked);
r.btnFunc.setDisabled(isLocked || !r.regex.getValue());
r.btnUp.setDisabled(isLocked || rulesRegistry.indexOf(r) === 0);
r.btnDown.setDisabled(isLocked || rulesRegistry.indexOf(r) === rulesRegistry.length - 1);
});
if (CAN_MOVE || IS_ADMIN) togAdminEnable.setDisabled(isLocked);
btnLoadLib.setDisabled(isLocked);
btnRemoveLib.setDisabled(isLocked || !currentLibrary.code);
btnEditLib.setDisabled(isLocked);
btnLoadLocal.setDisabled(isLocked);
btnClearLocal.setDisabled(isLocked || localTypos.length === 0);
}
function updateListCount() {
var val = listTextarea.getValue();
var count = val.trim() ? val.split('\n').filter(function(l) {
var line = l.trim();
return line !== "" && !line.startsWith("####");
}).length : 0;
$listCounter.text(count + ' pages');
}
listTextarea.on('change', updateListCount);
function updateDirtyState() {
if (isRunning && currentTitle && Editor.getValue() !== originalWikitext) $editorHeader.addClass('wa-dirty');
else $editorHeader.removeClass('wa-dirty');
}
function removeTopLine() {
var l = listTextarea.getValue().split('\n');
l.shift();
listTextarea.setValue(l.join('\n'));
updateListCount();
}
function updateInterfaceMode() {
var isAdminMode = togAdminEnable.getValue();
var pageLoaded = !!currentTitle;
btnSave.setDisabled(isAdminMode || !pageLoaded || !PERMS.canSave);
btnSkip.setDisabled(!pageLoaded);
btnPreview.setDisabled(!pageLoaded);
btnDiff.setDisabled(isAdminMode || !pageLoaded);
Editor.setDisabled(isAdminMode || !pageLoaded);
if (CAN_MOVE) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminMove.setDisabled(!(allowAdmin && currentVars['$xA']));
if (currentVars['$xA']) btnAdminMove.setTitle('Move page to ' + currentVars['$xA']);
else btnAdminMove.setTitle('Move page to $xA (Variable not set)');
}
if (IS_ADMIN) {
var allowAdmin = isAdminMode && currentPageExists;
btnAdminDel.setDisabled(!allowAdmin);
btnAdminProt.setDisabled(!allowAdmin);
}
}
function renderDiff() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Diff...</div>');
var currentText = Editor.getValue();
new mw.Api().post({
'action': 'compare',
fromtitle: currentTitle,
toslots: 'main',
'totext-main': currentText,
slots: 'main',
prop: 'diff',
formatversion: 2
}).then(function(data) {
var diffBody = data.compare && data.compare.bodies && data.compare.bodies.main;
if (diffBody) {
$visualOut.html('<h4>Diff: ' + currentTitle + '</h4><table class="diff"><colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup><tbody>' + diffBody + '</tbody></table>');
processDiffTable();
} else {
$visualOut.html('<div style="color:green; text-align:center; padding-top:20px;">No Changes detected</div>');
}
});
}
function processDiffTable() {
var rightLineNum = 0;
$visualOut.find('table.diff tr').each(function() {
var $tr = $(this);
var $linenos = $tr.find('td.diff-lineno');
if ($linenos.length > 0) {
var txt = $linenos.last().text();
var m = txt.match(/(\d+)/);
if (m) rightLineNum = parseInt(m[1]);
return;
}
if ($tr.find('.diff-addedline').length > 0 || $tr.find('.diff-context').length > 0) {
$tr.attr('data-line', rightLineNum);
$tr.css('cursor', 'pointer').attr('title', 'Jump to line ' + rightLineNum);
rightLineNum++;
}
});
// Attach a single delegated click listener to the table instead of every row
$visualOut.find('table.diff').on('click', 'tr[data-line]', function() {
Editor.scrollToLine(parseInt($(this).attr('data-line')));
});
}
function renderPreview() {
$visualOut.html('<div style="color:#888; text-align:center;">Generating Preview...</div>');
new mw.Api().post({
action: 'parse',
title: currentTitle,
text: Editor.getValue(),
prop: 'text|categorieshtml|modules|jsconfigvars',
useskin: mw.config.get('skin'),
disablelimitreport: true,
pst: true,
contentmodel: 'wikitext'
}).then(function(data) {
if (data.parse && data.parse.text) {
var $prev = $('<div>').html(data.parse.text['*']);
if (data.parse.categorieshtml) $prev.append(data.parse.categorieshtml['*']);
$prev.find('a').attr('target', '_blank');
$visualOut.empty().append($prev);
mw.loader.using(data.parse.modules.concat(data.parse.modulestyles, data.parse.modulescripts), function() {
mw.hook('wikipage.content').fire($('.wa-visual-output .mw-parser-output'));
});
}
}).catch(function(err) {
$visualOut.html('Error generating preview.');
alert("Preview failed: " + err);
});
}
async function transformPageText(rawText, title, config) {
var filters = config.filters;
if (filters) {
var check = function(text, rule) {
if (!rule || !rule.val) return false;
if (rule.regex) {
try {
return new RegExp(rule.val, 'mu').test(text);
} catch (e) {
return false;
}
}
return text.indexOf(rule.val) !== -1;
};
if (filters.skipContains && filters.skipContains.val && check(rawText, filters.skipContains)) {
return {
skipped: true,
reason: 'Contains: ' + filters.skipContains.val
};
}
if (filters.skipNotContains && filters.skipNotContains.val && !check(rawText, filters.skipNotContains)) {
return {
skipped: true,
reason: 'Missing: ' + filters.skipNotContains.val
};
}
}
var mode = config.mode;
var inputs = [];
var compiledSpecies = null;
if (config.templateFilter) {
var tFilter = config.templateFilter;
if (tFilter[0] === "^") tFilter = "^\\{\\{\\s*" + tFilter.slice(1);
else tFilter = "\\{\\{\\s*" + tFilter;
compiledSpecies = tFilter + "(?=\\s*[|}\\n])";
}
var skeleton = PageProtector.protect(rawText, mode, config.excludes, compiledSpecies);
if (mode === 'target') inputs = PageProtector.store;
else inputs = [skeleton];
var combinedRules = rulesRegistry.filter(r => r.isActive()).map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
}));
if (togWikiTypos.getValue()) combinedRules = combinedRules.concat(wikiTypos);
if (localTypos.length > 0) combinedRules = combinedRules.concat(localTypos);
var payload = {
texts: inputs,
vars: config.vars,
preCode: getUserCode(txtPreScript, 'wAwB_Pre'),
libraryCode: currentLibrary.code,
rules: combinedRules,
postCode: getUserCode(txtPostScript, 'wAwB_Post')
};
var result = await WorkerEngine.run(payload);
if (result.skipped) return {
skipped: true,
reason: result.reason
};
var finalText = "";
if (mode === 'target') {
PageProtector.store = result.texts;
finalText = PageProtector.restore(skeleton);
} else {
finalText = PageProtector.restore(result.texts[0]);
}
return {
skipped: false,
text: finalText
};
}
async function processPageContent() {
try {
setStatus('Processing...', 'working');
var mode = dropProtMode.getValue();
var activeConfig = {
mode: mode,
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: currentVars,
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
var res = await transformPageText(originalWikitext, currentTitle, activeConfig);
if (res.skipped) {
removeTopLine();
loadNextPage();
return;
}
if (chkSkipNoChange.isSelected() && res.text === originalWikitext) {
removeTopLine();
loadNextPage();
return;
}
setStatus('Ready');
Editor.setValue(res.text);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
else {
Editor.setDisabled(false);
btnSave.setDisabled(!PERMS.canSave);
btnSkip.setDisabled(false);
btnPreview.setDisabled(false);
btnDiff.setDisabled(false);
}
updateDirtyState();
renderCurrentView();
if (PERMS.allowBot && togAutoSave.getValue()) {
var delay = Math.max(0, parseInt(txtAutoDelay.getValue(), 10) || 0) * 1000;
setStatus('Auto-save in ' + (delay / 1000) + 's...', 'working');
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
if (isRunning && PERMS.canSave) {
btnSave.emit('click');
}
}, delay);
}
} catch (e) {
setStatus('Error', 'error');
alert(e);
btnPower.emit('click');
}
}
async function runPreParseBatch() {
// 1. Toggle / Stop Logic
if (isRunning) {
isRunning = false;
setStatus('Stopping...', 'working');
btnPreParse.setLabel('Pre-parse');
return;
}
// 2. Start & Deduplicate
var currentVal = listTextarea.getValue();
var cleanVal = getDeduplicatedList(currentVal).join('\n');
listTextarea.setValue(cleanVal);
updateListCount();
isRunning = true;
toggleUI(true);
// 3. Lock UI
toggleUI(true);
btnSkip.setDisabled(true);
btnDiff.setDisabled(true);
btnPreview.setDisabled(true);
btnSave.setDisabled(true);
Editor.setDisabled(true);
btnPreParse.setLabel('Stop pre-parse');
// Inject STOP marker if not present
var currentList = listTextarea.getValue().split('\n');
if (!currentList.includes('####STOP')) {
currentList.push('####STOP');
listTextarea.setValue(currentList.join('\n'));
}
// Gather Config
var activeConfig = {
mode: dropProtMode.getValue(),
excludes: {},
templateFilter: inpTemplateFilter.getValue().trim(),
vars: {},
filters: {
skipContains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
skipNotContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
}
}
};
if (activeConfig.mode === 'protect') {
for (var k in protCheckboxes) activeConfig.excludes[k] = protCheckboxes[k].isSelected();
} else {
var sel = radTargetSet.findSelectedItem();
activeConfig.excludes = sel ? sel.getData() : null;
}
setStatus('Pre-parsing...', 'working');
while (isRunning) {
var lines = listTextarea.getValue().split('\n');
var batchTitles = [];
var stopFound = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line === '####STOP') {
stopFound = true;
break;
}
if (line && !line.startsWith('####')) {
var parts = line.split('|');
batchTitles.push({
fullLine: line,
title: parts[0],
vars: parts.slice(1)
});
}
if (batchTitles.length >= 50) break;
}
if (batchTitles.length === 0) {
if (stopFound) setStatus('Pre-parse complete');
else setStatus('List empty');
break;
}
$listCounter.text('Fetching ' + batchTitles.length + '...');
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
try {
var data = await api.get({
action: 'query',
prop: 'revisions' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: batchTitles.map(t => t.title).join('|'),
rvprop: 'content',
rvslots: 'main',
redirects: 1,
cllimit: 'max'
});
var pageMap = {};
if (data.query && data.query.pages) Object.values(data.query.pages).forEach(p => pageMap[p.title] = p);
var redirMap = {};
if (data.query && data.query.redirects) data.query.redirects.forEach(r => redirMap[r.from] = r.to);
var keptLines = [];
for (var k = 0; k < batchTitles.length; k++) {
var item = batchTitles[k];
var lookupTitle = redirMap[item.title] || item.title;
var page = pageMap[lookupTitle];
if (!page || page.missing || page.invalid || !page.revisions || !page.revisions[0]) {
console.warn("Skipping invalid/missing page:", item.title);
continue;
}
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) continue; // Skip
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) continue; // Skip
var rawText = page.revisions[0].slots.main['*'];
activeConfig.vars = {
'$xx': item.title
};
item.vars.forEach((v, idx) => activeConfig.vars['$x' + String.fromCharCode(65 + idx)] = v);
var res = await transformPageText(rawText, item.title, activeConfig);
// UPDATED LOGIC: Respect "Skip if no change" checkbox
if (!res.skipped && (!chkSkipNoChange.isSelected() || res.text !== rawText)) {
keptLines.push(item.fullLine);
}
}
var freshLines = listTextarea.getValue().split('\n');
var stopIndex = -1;
for (var x = 0; x < freshLines.length; x++) {
if (freshLines[x] === '####STOP') {
stopIndex = x;
break;
}
}
if (stopIndex > -1) {
var topChunk = freshLines.slice(0, stopIndex);
var botChunk = freshLines.slice(stopIndex + 1);
var processedSet = new Set(batchTitles.map(t => t.fullLine));
var newTop = topChunk.filter(l => !processedSet.has(l));
var newList = newTop.concat(['####STOP']).concat(botChunk).concat(keptLines);
listTextarea.setValue(newList.join('\n'));
updateListCount();
}
} catch (e) {
console.error(e);
setStatus('Batch error: ' + e, 'error');
break;
}
}
isRunning = false;
toggleUI(false);
WorkerEngine.destroy();
btnPreParse.setLabel('Pre-parse');
if (listTextarea.getValue().startsWith('####STOP')) setStatus('Pre-parse done!');
else setStatus('Stopped');
}
btnPreParse.on('click', runPreParseBatch);
function loadNextPage() {
if (!isRunning) return;
var allLines = listTextarea.getValue().split('\n');
var listChanged = false;
var stopCommand = false;
while (allLines.length > 0) {
var line = allLines[0];
if (line === '####STOP') {
stopCommand = true;
break;
}
if (line.startsWith('####') || line === "") {
allLines.shift();
listChanged = true;
} else {
break;
}
}
if (listChanged) {
listTextarea.setValue(allLines.join('\n'));
updateListCount();
}
if (stopCommand) {
btnPower.emit('click');
setStatus("Stopped by ####STOP");
return;
}
if (allLines.length === 0) {
btnPower.emit('click');
setStatus("Done!");
return;
}
var raw = allLines[0];
var parts = raw.split('|');
currentTitle = parts[0].trim();
baseRevId = 0;
originalWikitext = "";
if (!currentTitle) {
removeTopLine();
loadNextPage();
return;
}
currentVars = {};
currentVars['$xx'] = currentTitle;
for (var i = 1; i < parts.length; i++) currentVars['$x' + String.fromCharCode(64 + i)] = parts[i];
setStatus('Loading...', 'working');
btnSave.setDisabled(true);
btnPreview.setDisabled(true);
btnDiff.setDisabled(true);
btnSkip.setDisabled(true);
Editor.setDisabled(true);
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
$editorHeader.removeClass('wa-dirty');
$visualOut.empty();
Editor.setValue('Loading...');
$infoContainer.empty();
currentPageExists = false;
btnWatch.setDisabled(true);
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
var badCats = inpSkipCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var reqCats = inpSkipNotCategories.getValue().split('|').map(s => s.trim()).filter(s => s);
var api = new mw.Api();
var params = {
action: 'query',
prop: 'revisions|info' + (badCats.length + reqCats.length > 0 ? '|categories' : ''),
titles: currentTitle,
rvprop: 'content|timestamp|ids',
rvslots: 'main',
inprop: 'watched',
cllimit: 'max'
};
var rMode = redirMode.findSelectedItem().getData();
if (rMode === 'follow') params.redirects = 1;
return api.get(params).then(async function(data) {
var pid = Object.keys(data.query.pages)[0];
var page = data.query.pages[pid];
currentPageExists = !page.missing && !page.invalid;
var pageCats = new Set((page.categories || []).map(c => c.title.replace(/^[^:]+:/, '').trim()));
if (badCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat blacklist');
removeTopLine();
loadNextPage();
return;
}
if (reqCats.length > 0 && !reqCats.some(c => pageCats.has(c))) {
setStatus('Skip: cat whitelist');
removeTopLine();
loadNextPage();
return;
}
if (rMode === 'follow' && data.query.redirects) {
currentTitle = page.title;
$titleLink.attr('href', mw.util.getUrl(currentTitle)).text(currentTitle);
mw.notify('Redirect followed to: ' + currentTitle);
}
if (rMode === 'skip' && page.redirect !== undefined) {
removeTopLine();
loadNextPage();
return;
}
var skipMode = radSkipExist.findSelectedItem().getData();
if (pid === "-1") {
if (skipMode === 'missing') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = "";
baseRevId = 0;
} else {
if (skipMode === 'exists') {
removeTopLine();
loadNextPage();
return;
}
originalWikitext = page.revisions[0].slots.main['*'];
baseRevId = page.revisions[0].revid;
}
if (page.revisions && page.revisions.length > 0) {
var rev = page.revisions[0];
var ts = new Date(rev.timestamp).toISOString().replace('T', ' ').substring(0, 16);
$infoContainer.empty().append('Last edit: ' + ts + ' | ', $('<a>').attr('href', mw.util.getUrl(currentTitle, {
action: 'history'
})).attr('target', '_blank').text('history'));
}
btnWatch.setDisabled(!currentPageExists);
if (page.watched !== undefined) btnWatch.setIcon('unStar');
else btnWatch.setIcon('star');
if (CAN_MOVE || IS_ADMIN) {
updateInterfaceMode();
if (togAdminEnable.getValue()) {
Editor.setValue(originalWikitext);
renderCurrentView();
setStatus('Ready (Page actions)');
return;
}
}
processPageContent();
}).catch(function(e) {
setStatus('API error', 'error');
alert('Load error: ' + e);
btnPower.emit('click');
});
}
async function fetchWithContinue(api, params) {
var allTitles = new Set();
var continueToken = {};
var safetyLimit = FETCH_SAFETY_LIMIT;
var count = 0;
isFetching = true;
btnAdd.setLabel('Cancel fetch');
$fetchStatus.text('Fetching...').show();
try {
while (isFetching && count < safetyLimit) {
var merged = Object.assign({}, params, continueToken);
var data = await api.get(merged);
var batch = [];
if (data.watchlistraw) batch = data.watchlistraw;
else if (data.query) {
if (data.query.pages) batch = Object.values(data.query.pages);
else if (data.query.categorymembers) batch = data.query.categorymembers;
else if (data.query.backlinks) batch = data.query.backlinks;
else if (data.query.embeddedin) batch = data.query.embeddedin;
else if (data.query.imageusage) batch = data.query.imageusage;
else if (data.query.search) batch = data.query.search;
else if (data.query.allpages) batch = data.query.allpages;
else if (data.query.usercontribs) batch = data.query.usercontribs;
else if (data.query.pageswithprop) batch = data.query.pageswithprop;
}
if (batch.length > 0) {
batch.forEach(item => {
if (item.title) allTitles.add(item.title);
});
count = allTitles.size;
$fetchStatus.text('Fetched ' + count + '...');
}
if (data.continue) continueToken = data.continue;
else break;
}
} catch (e) {
alert("Fetch interrupted: " + e);
}
isFetching = false;
btnAdd.setLabel('Add to list').setDisabled(false);
$fetchStatus.text('Added ' + allTitles.size + ' pages').delay(3000).fadeOut();
if (allTitles.size > 0) {
hasNewSources = true;
checkSummaryWarning();
}
return Array.from(allTitles);
}
function toggleUI(d) {
if (d) {
btnPower.setLabel('Stop').setIcon('power').setFlags(['destructive']);
} else {
btnPower.setLabel('Start').setIcon('power').clearFlags().setFlags(['primary', 'progressive']);
if (PERMS.allowBot) togAutoSave.setValue(false);
}
toggleConfig(d);
btnSort.setDisabled(d);
btnDedup.setDisabled(d);
btnClear.setDisabled(d);
btnSaveProj.setDisabled(d);
btnLoadProj.setDisabled(d);
btnSkip.setDisabled(!d);
btnSave.setDisabled(true);
listTextarea.setReadOnly(d);
if (d) listTextarea.$element.addClass('wa-list-running');
else listTextarea.$element.removeClass('wa-list-running');
}
function resetPanels() {
Editor.setValue('');
$titleLink.text('Page content').removeAttr('href');
$editorHeader.removeClass('wa-dirty');
setStatus('Ready');
currentTitle = null;
$visualOut.html('<div style="color:#aaa; text-align:center; margin-top:50px;">Ready...</div>');
$infoContainer.empty();
btnWatch.setDisabled(true);
Editor.setDisabled(true);
currentPageExists = false;
if (CAN_MOVE || IS_ADMIN) updateInterfaceMode();
toggleUI(false);
updateListCount();
if (autoSaveTimer) clearTimeout(autoSaveTimer);
}
function arrayMove(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) arr.push(undefined);
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}
function updateRuleButtons() {
rulesRegistry.forEach(function(item, idx) {
item.btnUp.setDisabled(idx === 0);
item.btnDown.setDisabled(idx === rulesRegistry.length - 1);
});
}
function addRule() {
var row = $('<div>').addClass('wa-rule-row');
var controls = $('<div>').addClass('wa-rule-controls');
var btnUp = new OO.ui.ButtonWidget({
icon: 'collapse',
framed: false,
title: 'Move up',
classes: ['wa-rule-btn']
});
var btnDown = new OO.ui.ButtonWidget({
icon: 'expand',
framed: false,
title: 'Move down',
classes: ['wa-rule-btn']
});
controls.append(btnUp.$element, btnDown.$element);
var contentDiv = $('<div>').addClass('wa-rule-content');
var f = new OO.ui.TextInputWidget({
placeholder: 'Find'
});
var r = new OO.ui.TextInputWidget({
placeholder: 'Replace'
});
var reg = new OO.ui.ToggleSwitchWidget();
var fl = new OO.ui.TextInputWidget({
value: 'gmu',
disabled: true
}).toggle(false);
var btnEnable = new OO.ui.ButtonWidget({
icon: 'power',
framed: false,
title: 'Toggle rule',
flags: ['progressive']
});
var isRuleActive = true;
var btnFunc = new OO.ui.ButtonWidget({
icon: 'code',
framed: false,
title: 'Toggle JS mode',
disabled: true
});
var isRuleFunc = false;
var toggleRule = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleActive;
isRuleActive = val;
row.css('opacity', isRuleActive ? 1 : 0.5);
if (isRuleActive) btnEnable.setFlags(['progressive']);
else btnEnable.clearFlags();
};
btnEnable.on('click', function() {
toggleRule();
});
var toggleFunc = function(forceVal) {
var val = (forceVal !== undefined) ? forceVal : !isRuleFunc;
isRuleFunc = val;
if (isRuleFunc) {
btnFunc.setFlags(['progressive']);
r.$input.attr('placeholder', 'return match.toUpperCase();');
} else {
btnFunc.clearFlags();
r.$input.attr('placeholder', 'Replace');
}
};
btnFunc.on('click', function() {
toggleFunc();
});
btnFunc.toggle(false);
reg.on('change', function(v) {
fl.setDisabled(!v);
fl.toggle(v);
btnFunc.setDisabled(!v);
if (!v) {
btnFunc.toggle(false);
if (isRuleFunc) toggleFunc(false);
} else btnFunc.toggle(true);
});
var del = new OO.ui.ButtonWidget({
icon: 'trash',
flags: 'destructive',
framed: false
});
del.on('click', function() {
row.fadeOut(200, function() {
row.remove();
rulesRegistry = rulesRegistry.filter(x => x.row !== row);
updateRuleButtons();
});
});
contentDiv.append(f.$element, $('<div>').css('margin-top', '3px').append(r.$element), $('<div>').addClass('wa-rule-opt-row').append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<div>').css({
'display': 'flex',
'align-items': 'center'
}).append($('<span>').text('Regex: ').css({
'font-size': '0.8em',
'margin-right': '4px'
}), reg.$element, fl.$element.css({
'width': '50px',
'margin-left': '5px'
})), btnFunc.$element.css('margin-left', '10px')), $('<div>').css('display', 'flex').append(btnEnable.$element, del.$element)));
row.append(controls, contentDiv);
$rulesList.append(row);
var ruleItem = {
row: row,
find: f,
rep: r,
regex: reg,
flags: fl,
btnUp: btnUp,
btnDown: btnDown,
enable: btnEnable,
del: del,
btnFunc: btnFunc,
isActive: function() {
return isRuleActive;
},
setActive: toggleRule,
isFunc: function() {
return isRuleFunc;
},
setFunc: toggleFunc
};
rulesRegistry.push(ruleItem);
btnUp.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx > 0) {
var prevRow = rulesRegistry[idx - 1].row;
row.fadeOut(150, function() {
row.insertBefore(prevRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx - 1);
updateRuleButtons();
}
});
btnDown.on('click', function() {
var idx = rulesRegistry.indexOf(ruleItem);
if (idx < rulesRegistry.length - 1) {
var nextRow = rulesRegistry[idx + 1].row;
row.fadeOut(150, function() {
row.insertAfter(nextRow).fadeIn(150).addClass('wa-highlight');
setTimeout(function() {
row.removeClass('wa-highlight');
}, 500);
});
arrayMove(rulesRegistry, idx, idx + 1);
updateRuleButtons();
}
});
updateRuleButtons();
}
btnAddRule.on('click', addRule);
addRule();
togWikiTypos.on('change', function(v) {
if (v) {
if (wikiTypos.length > 0) lblWikiStatus.text(wikiTypos.length + ' rules loaded (Cached)');
else {
lblWikiStatus.text('Fetching...');
togWikiTypos.setDisabled(true);
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgFormattedNamespaces')[4] + ':AutoWikiBrowser/Typos',
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function(d) {
var page = d.query.pages[0];
if (!page.missing) {
wikiTypos = parseTypoContent(page.revisions[0].slots.main.content);
lblWikiStatus.text(wikiTypos.length + ' rules loaded');
} else {
lblWikiStatus.text('Page not found');
togWikiTypos.setValue(false);
}
}).catch(function() {
lblWikiStatus.text('Error');
togWikiTypos.setValue(false);
}).always(function() {
togWikiTypos.setDisabled(false);
});
}
} else lblWikiStatus.text('Rules inactive');
});
btnLoadLocal.on('click', function() {
$typoInput.click();
});
$typoInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
localTypos = parseTypoContent(evt.target.result);
lblLocalStatus.text(localTypos.length + ' local rules loaded');
btnClearLocal.setDisabled(false);
};
reader.readAsText(file);
$typoInput.val('');
});
btnClearLocal.on('click', function() {
localTypos = [];
lblLocalStatus.text('No local rules');
btnClearLocal.setDisabled(true);
});
btnLoadLib.on('click', function() {
$libInput.click();
});
btnRemoveLib.on('click', function() {
currentLibrary = {
name: null,
code: null
};
updateLibUI();
});
$libInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
currentLibrary = {
name: file.name,
code: evt.target.result
};
updateLibUI();
};
reader.readAsText(file);
$libInput.val('');
});
btnPower.on('click', function() {
hasNewSources = false;
checkSummaryWarning();
if (!isRunning) {
if (SAVED_SESSION === 0) mw.track('stats.mediawiki_gadget_wAwB_total');
isRunning = true;
toggleUI(true);
loadNextPage();
} else {
if (SAVED_RUN > 0) {
mw.track('stats.mediawiki_gadget_wAwB_saved_total', SAVED_RUN, {
wiki: WIKI
});
SAVED_SESSION += SAVED_RUN;
SAVED_RUN = 0;
}
isRunning = false;
toggleUI(false);
WorkerEngine.destroy();
resetPanels();
}
});
inputSummary.on('change', function() {
checkSummaryWarning();
});
btnSkip.on('click', function() {
if (Editor.getValue() === 'Loading...') return;
removeTopLine();
loadNextPage();
});
btnDiff.on('click', function() {
currentViewMode = 'diff';
if (currentTitle) renderDiff();
});
btnPreview.on('click', function() {
currentViewMode = 'preview';
if (currentTitle) renderPreview();
});
btnSave.on('click', function() {
if (Editor.getValue() === 'Loading...' || !currentTitle) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
btnSave.setDisabled(true);
setStatus('Saving...', 'working');
var effectiveDelay = PERMS.saveDelay || 0;
if (effectiveDelay > 0) setStatus('Throttling (' + (effectiveDelay / 1000) + 's)...', 'working');
setTimeout(function() {
if (effectiveDelay > 0) setStatus('Saving...', 'working');
var summary = inputSummary.getValue() + SUMMARY_SUFFIX;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
text: Editor.getValue(),
summary: summary,
minor: chkMinor.isSelected(),
baserevid: baseRevId,
bot: PERMS.allowBot,
tags: DO_TAG ? APP_NAME : undefined
}).then(function() {
SAVED_RUN += 1;
removeTopLine();
loadNextPage();
}).catch(function(c) {
btnSave.setDisabled(false);
setStatus('Save error', 'error');
alert('Save failed: ' + c);
});
}, effectiveDelay);
});
btnWatch.on('click', function() {
var isWatched = btnWatch.getIcon() === 'unStar';
new mw.Api()[isWatched ? 'unwatch' : 'watch'](currentTitle).then(function() {
btnWatch.setIcon(isWatched ? 'star' : 'unStar');
mw.notify(isWatched ? 'Unwatched' : 'Watched');
});
});
btnAdd.on('click', function() {
if (isFetching) {
isFetching = false;
btnAdd.setDisabled(true).setLabel('Cancelling...');
return;
}
try {
var mode = srcSelect.getValue(),
q = srcInput.getValue().trim();
if (mode !== 'watchlist' && mode !== 'usercontribs' && mode !== 'pageswithprop' && !q) {
alert('Query empty');
return;
}
var nsIds = ($nsSelect.val() || []).map(v => parseInt(v));
var nsStr = nsIds.join('|');
var api = new mw.Api(),
promises = [];
if (mode === 'cat') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'categorymembers',
cmtitle: mw.Title.newFromText(q, 14) ? mw.Title.newFromText(q, 14).getPrefixedText() : 'Category:' + q,
cmnamespace: nsStr,
cmtype: (chkCatPages.isSelected() ? 'page|' : '') + (chkCatSub.isSelected() ? 'subcat|' : '') + (chkCatFile.isSelected() ? 'file' : ''),
cmlimit: 'max'
}));
else if (mode === 'linksto') {
if (chkLinkWiki.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'backlinks',
bltitle: q,
blnamespace: nsStr,
bllimit: 'max',
blfilterredir: dropLinkRedir.getValue(),
blredirect: chkLinkToRedir.isSelected()
}));
if (chkLinkTrans.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'embeddedin',
eititle: q,
einamespace: nsStr,
eilimit: 'max',
eifilterredir: dropLinkRedir.getValue()
}));
if (chkLinkImg.isSelected()) promises.push(fetchWithContinue(api, {
action: 'query',
list: 'imageusage',
iutitle: q,
iunamespace: nsStr,
iulimit: 'max',
iufilterredir: dropLinkRedir.getValue()
}));
} else if (mode === 'linkson') promises.push(fetchWithContinue(api, {
action: 'query',
generator: 'links',
titles: q,
gplnamespace: nsStr,
gpllimit: 'max',
prop: 'info'
}));
else if (mode === 'prefix') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'allpages',
apprefix: q,
apnamespace: nsIds[0] || 0,
aplimit: 'max'
}));
else if (mode === 'watchlist') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'watchlistraw',
wrnamespace: nsStr,
wrlimit: 'max'
}));
else if (mode === 'search') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'search',
srsearch: q,
srnamespace: nsStr,
srlimit: 'max'
}));
else if (mode === 'usercontribs') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'usercontribs',
ucuser: srcInputUser.getValue(),
ucstart: srcInputStartDate.getValue(),
ucend: srcInputEndDate.getValue(),
ucdir: 'newer',
uclimit: 'max',
ucnamespace: nsStr,
ucprop: 'title'
}));
else if (mode === 'pageswithprop') promises.push(fetchWithContinue(api, {
action: 'query',
list: 'pageswithprop',
pwppropname: srcDropProp.getValue(),
pwplimit: 'max'
}));
Promise.all(promises).then(function(res) {
var list = new Set();
res.forEach(titles => titles.forEach(t => list.add(t)));
var currentVal = listTextarea.getValue();
var newVal = Array.from(list).join('\n');
listTextarea.setValue(currentVal ? currentVal + '\n' + newVal : newVal);
mw.notify('Added ' + list.size + ' pages');
}).catch(e => alert('Error: ' + e));
} catch (e) {
alert("Fetch error: " + e);
}
});
btnSort.on('click', function() {
var v = listTextarea.getValue();
if (v) {
var lines = getNormalizedList(v);
lines.sort((a, b) => sortAsc ? a.localeCompare(b) : b.localeCompare(a));
listTextarea.setValue(lines.join('\n'));
sortAsc = !sortAsc;
}
});
btnDedup.on('click', function() {
var v = listTextarea.getValue();
if (v) listTextarea.setValue(getDeduplicatedList(v).join('\n'));
});
btnClear.on('click', function() {
listTextarea.setValue('');
});
btnSaveProj.on('click', function() {
try {
var currentMode = srcSelect.getValue();
if (['watchlist', 'usercontribs', 'pageswithprop'].indexOf(currentMode) === -1) queryCache[currentMode] = srcInput.getValue();
var saveExcludes = {};
for (var k in protCheckboxes) saveExcludes[k] = protCheckboxes[k].isSelected();
var data = {
version: APP_VERSION,
library: currentLibrary,
source: {
activeMode: currentMode,
namespaces: ($nsSelect.val() || []).map(v => parseInt(v)),
modes: {
cat: {
query: queryCache['cat'] || '',
options: {
pages: chkCatPages.isSelected(),
sub: chkCatSub.isSelected(),
file: chkCatFile.isSelected()
}
},
linksto: {
query: queryCache['linksto'] || '',
options: {
wiki: chkLinkWiki.isSelected(),
trans: chkLinkTrans.isSelected(),
img: chkLinkImg.isSelected(),
redir: dropLinkRedir.getValue(),
toRedir: chkLinkToRedir.isSelected()
}
},
linkson: {
query: queryCache['linkson'] || ''
},
prefix: {
query: queryCache['prefix'] || ''
},
watchlist: {
query: ''
},
search: {
query: queryCache['search'] || ''
},
usercontribs: {
options: {
user: srcInputUser.getValue(),
start: srcInputStartDate.getValue(),
end: srcInputEndDate.getValue()
}
},
pageswithprop: {
options: {
prop: srcDropProp.getValue()
}
}
}
},
settings: {
redir: redirMode.findSelectedItem().getData(),
skipLogic: radSkipExist.findSelectedItem().getData(),
skipNoChange: chkSkipNoChange.isSelected(),
minor: chkMinor.isSelected()
},
filters: {
contains: {
val: inpSkipContains.getValue(),
regex: togSkipContainsRegex.getValue()
},
notContains: {
val: inpSkipNotContains.getValue(),
regex: togSkipNotContainsRegex.getValue()
},
categories: {
skip: inpSkipCategories.getValue(),
require: inpSkipNotCategories.getValue()
}
},
rules: rulesRegistry.map(r => ({
find: r.find.getValue(),
replace: r.rep.getValue(),
regex: r.regex.getValue(),
flags: r.flags.getValue(),
enabled: r.isActive(),
isFunc: r.isFunc()
})),
scripts: {
pre: txtPreScript.getValue(),
post: txtPostScript.getValue()
},
processing: {
summary: inputSummary.getValue(),
list: listTextarea.getValue()
},
protection: {
mode: dropProtMode.getValue(),
excludes: saveExcludes,
target: (radTargetSet.findSelectedItem() || {
getData: () => null
}).getData(),
templateFilter: inpTemplateFilter.getValue()
}
};
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 1)], {
type: "application/json"
}));
a.download = "wawb_project.json";
a.click();
} catch (e) {
alert("Save error: " + e);
}
});
btnLoadProj.on('click', function() {
$fileInput.click();
});
$fileInput.on('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
isLoadingProject = true;
if (data.source) {
if (data.source.namespaces) $nsSelect.val(data.source.namespaces.map(String));
if (data.source.modes) {
var m = data.source.modes;
queryCache = {};
for (var key in m)
if (m[key].query !== undefined) queryCache[key] = m[key].query;
if (m.cat && m.cat.options) {
chkCatPages.setSelected(m.cat.options.pages);
chkCatSub.setSelected(m.cat.options.sub);
chkCatFile.setSelected(m.cat.options.file);
}
if (m.linksto && m.linksto.options) {
chkLinkWiki.setSelected(m.linksto.options.wiki);
chkLinkTrans.setSelected(m.linksto.options.trans);
chkLinkImg.setSelected(m.linksto.options.img);
dropLinkRedir.setValue(m.linksto.options.redir);
chkLinkToRedir.setSelected(m.linksto.options.toRedir);
}
if (m.usercontribs && m.usercontribs.options) {
srcInputUser.setValue(m.usercontribs.options.user);
srcInputStartDate.setValue(m.usercontribs.options.start);
srcInputEndDate.setValue(m.usercontribs.options.end);
}
if (m.pageswithprop && m.pageswithprop.options) srcDropProp.setValue(m.pageswithprop.options.prop);
}
if (data.source.activeMode) {
srcSelect.setValue(data.source.activeMode);
isLoadingProject = false;
srcSelect.emit('change', data.source.activeMode);
isLoadingProject = true;
}
}
if (data.settings) {
redirMode.selectItemByData(data.settings.redir);
chkSkipNoChange.setSelected(data.settings.skipNoChange);
radSkipExist.selectItemByData(data.settings.skipLogic);
if (data.settings.minor !== undefined) chkMinor.setSelected(data.settings.minor);
}
if (data.protection) {
dropProtMode.setValue(data.protection.mode);
for (var k in data.protection.excludes)
if (protCheckboxes[k]) protCheckboxes[k].setSelected(data.protection.excludes[k]);
if (data.protection.target) radTargetSet.selectItemByData(data.protection.target);
if (data.protection.templateFilter) inpTemplateFilter.setValue(data.protection.templateFilter);
}
if (data.library) {
currentLibrary = data.library;
updateLibUI();
}
if (data.filters) {
inpSkipContains.setValue(data.filters.contains.val);
togSkipContainsRegex.setValue(data.filters.contains.regex);
inpSkipNotContains.setValue(data.filters.notContains.val);
togSkipNotContainsRegex.setValue(data.filters.notContains.regex);
}
if (data.filters.categories) {
inpSkipCategories.setValue(data.filters.categories.skip);
inpSkipNotCategories.setValue(data.filters.categories.require);
}
rulesRegistry.forEach(r => r.row.remove());
rulesRegistry = [];
$rulesList.empty();
if (data.rules) data.rules.forEach(r => {
addRule();
var last = rulesRegistry[rulesRegistry.length - 1];
last.find.setValue(r.find);
last.rep.setValue(r.replace);
last.regex.setValue(r.regex);
last.flags.setValue(r.flags);
last.flags.setDisabled(!r.regex);
last.setActive(r.enabled);
if (r.isFunc) last.setFunc(true);
});
if (rulesRegistry.length === 0) addRule();
if (data.scripts) {
txtPreScript.setValue(data.scripts.pre);
txtPostScript.setValue(data.scripts.post);
}
if (data.processing) {
inputSummary.setValue(data.processing.summary);
listTextarea.setValue(data.processing.list);
}
isLoadingProject = false;
setStatus('Project loaded');
} catch (err) {
alert("Error: " + err);
}
$fileInput.val('');
};
reader.readAsText(file);
});
if (CAN_MOVE || IS_ADMIN) {
togAdminEnable.on('change', function(val) {
if (!currentTitle) {
updateInterfaceMode();
return;
}
if (val) {
Editor.setValue(originalWikitext);
updateInterfaceMode();
renderDiff();
setStatus('Ready (Page actions)');
} else processPageContent();
});
}
if (CAN_MOVE) {
btnAdminMove.on('click', function() {
if (!currentVars['$xA']) {
mw.notify('Variable $xA not set', {
type: 'error'
});
return;
}
new mw.Api().postWithToken('csrf', {
action: 'move',
from: currentTitle,
to: currentVars['$xA'],
reason: inputSummary.getValue() + SUMMARY_SUFFIX,
movetalk: chkMovTalk.isSelected(),
movesubpages: chkMovSub.isSelected(),
noredirect: chkMovRedirect.isSelected()
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Move failed: ' + e));
});
}
if (IS_ADMIN) {
btnAdminDel.on('click', function() {
new mw.Api().postWithToken('csrf', {
action: 'delete',
title: currentTitle,
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
if (chkDelTalk.isSelected()) new mw.Api().postWithToken('csrf', {
action: 'delete',
title: mw.Title.newFromText(currentTitle).getTalkPage().getPrefixedText(),
reason: 'Talk page of deleted page'
});
removeTopLine();
loadNextPage();
}).catch(e => alert('Delete failed: ' + e));
});
btnAdminProt.on('click', function() {
var protections = [];
if (dropProtEdit.getValue()) protections.push('edit=' + dropProtEdit.getValue());
if (dropProtMove.getValue()) protections.push('move=' + dropProtMove.getValue());
new mw.Api().postWithToken('csrf', {
action: 'protect',
title: currentTitle,
protections: protections.join('|'),
expiry: inpProtExpiry.getValue() || 'infinite',
reason: inputSummary.getValue() + SUMMARY_SUFFIX
}).then(function() {
removeTopLine();
loadNextPage();
}).catch(e => alert('Protect failed: ' + e));
});
}
Editor.init();
resetPanels();
});
$(window).on('beforeunload', function() {
return "You have unsaved work.";
});
}).catch(e => console.error("wAwB Loader Error:", e));
//</nowiki>
k97y7ukggj94tywlzrkicd84935nc0x
User:Laerdon/ConvoWizard.js
2
172222
740018
732256
2026-05-01T15:29:29Z
Laerdon
70887
740018
javascript
text/javascript
//<nowiki>
/**
* Filename: wiki-talk-page-script.js
* ConvoWizard – Real-time Reply Feedback (Wikipedia userscript)
* Runs on Talk pages inside DiscussionTools reply widgets.
* Prompts for a token on first run; persists after authorization.
*/
(async function () {
'use strict';
// Load core util module via ResourceLoader
await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api']);
// === CONFIG ===
const SERVER = 'https://convocompass.toolforge.org/api/';
// ---- UI strings and thresholds ----
const NAME = 'ConvoWizard';
const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search);
const OPTION_KEY = 'userjs-convowizard-token';
const SCORE_CHANGE_THRESH = 0.08;
const MID_TENSION_THRESH = 0.50;
const HIGH_TENSION_THRESH = 0.75;
const CONTEXT_LOW = `${NAME} will notify you here if it detects anything in the preceding conversation.`;
const CONTEXT_MID = `${NAME} thinks this discussion is getting somewhat tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_HIGH = `${NAME} thinks this discussion is getting tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_VERY_HIGH = `${NAME} thinks this discussion is getting very tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const REPLY_LOW = `${NAME} thinks this comment might decrease tension in this discussion.`;
const REPLY_MID = `${NAME} will notify you here if it detects anything in your comment draft.`;
const REPLY_HIGH = `${NAME} thinks this comment might increase the tension in this discussion. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const TENSION_COLORS = {
mid: { border: '#e8825c', bg: 'linear-gradient(135deg, #fff0eb 0%, #fff8f5 100%)', icon: '#e8825c', inputBg: 'rgba(232,130,92,0.05)' },
high: { border: '#d73027', bg: 'linear-gradient(135deg, #ffeaea 0%, #fff5f5 100%)', icon: '#d73027', inputBg: 'rgba(215,48,39,0.05)' },
veryHigh: { border: '#a50f15', bg: 'linear-gradient(135deg, #fdd 0%, #fee 100%)', icon: '#a50f15', inputBg: 'rgba(165,15,21,0.07)' },
};
// Keep ConvoWizard logs quiet by default to reduce console noise on Wikimedia pages.
const nativeConsole = window.console;
const console = {
log: (...args) => {
if (!DEBUG && typeof args[0] === 'string' && args[0].startsWith(`[${NAME}]`)) {
return;
}
nativeConsole.log(...args);
},
warn: (...args) => nativeConsole.warn(...args),
error: (...args) => nativeConsole.error(...args)
};
const isWikipediaHost = /(^|\.)wikipedia\.org$/.test(location.hostname);
const namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
const isTalkNamespace = Number.isFinite(namespaceNumber) && namespaceNumber % 2 === 1;
const isViewAction = mw.config.get('wgAction') === 'view';
// Gadget scope guard: only run on Wikipedia talk pages in normal view mode.
if (!isWikipediaHost || !isTalkNamespace || !isViewAction) {
console.log(
`[${NAME}] Skipping page (host=${location.hostname}, ns=${namespaceNumber}, action=${mw.config.get('wgAction')})`
);
return;
}
// ---------- helpers ----------
function throttle(fn, waitMs) {
let last = 0, timer = 0, pendingArgs = null;
return (...args) => {
const now = Date.now();
const remaining = waitMs - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
} else {
pendingArgs = args;
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn(...pendingArgs); }, remaining);
}
};
}
// Rewrite Wikipedia URL so the reddit-style backend can extract a post_id.
// Backend does: url.split("comments/")[1][:6]
// We inject "comments/<hash>" where hash is derived from the wiki page path.
function wikiUrlToPostId(href) {
try {
const pagePath = href.split('/wiki/')[1] || href;
// Simple hash to produce a stable 6-char hex id per page
let h = 0;
for (let i = 0; i < pagePath.length; i++) {
h = ((h << 5) - h) + pagePath.charCodeAt(i);
h = h & h;
}
const hex = Math.abs(h).toString(16).padStart(6, '0').slice(0, 6);
return href.split('/wiki/')[0] + '/comments/' + hex;
} catch {
return href;
}
}
async function postJson(route, body) {
try {
const res = await fetch(SERVER + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
try { return JSON.parse(text); } catch { return {}; }
} catch {
return {};
}
}
function getTokenFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(OPTION_KEY);
if (typeof raw === 'string') {
const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
}
}
} catch (err) {
console.warn(`[${NAME}] Failed to read token from user options`, err);
}
return null;
}
async function persistToken(storageKey, token) {
if (!token) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, token);
}
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(OPTION_KEY, token || '');
await new mw.Api().saveOption(OPTION_KEY, token || '');
console.log(`[${NAME}] ${token ? 'Saved' : 'Cleared'} token in user options (notepad)`);
} catch (err) {
console.warn(`[${NAME}] Could not persist token to user options`, err);
}
}
async function validateToken(token, username) {
try {
const response = await postJson('claim_token', { token, username });
return response.valid === true;
} catch {
return false;
}
}
// ========== CREDENTIAL SETUP ==========
const USERNAME = mw.config.get('wgUserName');
if (!USERNAME) {
console.log(`[${NAME}] Not logged in, skipping`);
return;
}
let TOKEN = null;
const STORAGE_KEY = `ConvoWizard:token:${USERNAME}`;
// Load persisted token (options → localStorage), validate, sync stores
async function loadPersistedToken() {
let token = getTokenFromOptions();
let source = 'options';
if (!token) {
token = localStorage.getItem(STORAGE_KEY);
source = 'localStorage';
}
if (token && typeof token === 'string') {
token = token.trim();
}
if (!token) return;
const valid = await validateToken(token, USERNAME);
if (valid) {
TOKEN = token;
// Sync both stores
if (source === 'options') {
localStorage.setItem(STORAGE_KEY, token);
} else {
await persistToken(STORAGE_KEY, token);
}
console.log(`[${NAME}] Token loaded from ${source}`);
} else {
console.log(`[${NAME}] Persisted token invalid, clearing`);
await persistToken(STORAGE_KEY, null);
}
}
await loadPersistedToken();
console.log(`[${NAME}] === SCRIPT LOADED SUCCESSFULLY ===`);
console.log(`[${NAME}] Server: ${SERVER}`);
console.log(`[${NAME}] User: ${USERNAME}`);
console.log(`[${NAME}] Token: ${TOKEN ? 'present' : 'none (will prompt)'}`);
console.log(`[${NAME}] Page URL: ${location.href}`);
console.log(`[${NAME}] Installing click and mutation observers...`);
// ========== TOKEN INPUT FORM ==========
function showTokenForm(widgetEl) {
// Don't add duplicate forms
if (widgetEl.querySelector('.convowizard-auth-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-auth-form';
form.style.cssText = `
margin:8px 0;padding:16px 20px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
const title = document.createElement('div');
title.style.cssText = 'font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;';
title.textContent = `${NAME}: Authorization Required`;
const desc = document.createElement('div');
desc.style.cssText = 'font-size:13px;color:#333;margin-bottom:12px;line-height:1.4;';
desc.textContent = 'Please paste your ConvoWizard token below to activate the extension.';
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;align-items:center;';
const input = document.createElement('input');
input.type = 'password';
input.placeholder = 'Paste token here';
input.autocomplete = 'off';
input.spellcheck = false;
input.style.cssText = `
flex:1;padding:8px 10px;font-size:13px;border:1px solid #a2a9b1;border-radius:4px;
font-family:monospace;outline:none;transition:border-color 0.2s ease;
`;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Authorize';
btn.style.cssText = `
padding:8px 16px;font-size:13px;font-weight:600;border:none;border-radius:4px;
background:#0645ad;color:#fff;cursor:pointer;transition:background 0.2s ease;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
btn.addEventListener('mouseenter', () => { btn.style.background = '#0b5cce'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#0645ad'; });
const errDiv = document.createElement('div');
errDiv.style.cssText = 'font-size:12px;color:#d73027;margin-top:8px;display:none;';
async function onAuthorize() {
const raw = input.value.trim();
// Input validation: 33 hex chars
if (!/^[0-9a-f]{33}$/i.test(raw)) {
errDiv.textContent = 'Invalid token format. A token is 33 hexadecimal characters.';
errDiv.style.display = 'block';
input.value = '';
input.focus();
return;
}
btn.disabled = true;
btn.textContent = 'Validating...';
errDiv.style.display = 'none';
const valid = await validateToken(raw, USERNAME);
// Clear input immediately after use
input.value = '';
if (valid) {
TOKEN = raw;
await persistToken(STORAGE_KEY, TOKEN);
// Remove all auth forms on page
document.querySelectorAll('.convowizard-auth-form').forEach(f => f.remove());
// Activate the widget
startForWidget(widgetEl);
} else {
errDiv.textContent = 'Token is invalid or expired. Please check and try again.';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Authorize';
input.focus();
}
}
btn.addEventListener('click', onAuthorize);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); onAuthorize(); }
});
row.appendChild(input);
row.appendChild(btn);
form.appendChild(title);
form.appendChild(desc);
form.appendChild(row);
form.appendChild(errDiv);
widgetEl.prepend(form);
input.focus();
}
// ---- state + logs ----
const widgetState = new WeakMap();
function cleanupDuplicatePanels() {
const allPanels = document.querySelectorAll('.craftDisplay, .passiveModeDisplay');
const seen = new Set();
allPanels.forEach(panel => {
const pid = panel.id;
if (pid && pid.includes('_')) {
const parts = pid.split('_');
const type = parts[0];
const wid = parts.slice(1).join('_').replace('_d', '');
const key = `${type}|${wid}`;
if (seen.has(key)) panel.remove(); else seen.add(key);
}
});
}
// ========== MUTE THREAD ==========
/**
* Generates a unique thread identifier by combining URL section, DOM position, and content hash.
* Ensures each thread on a page gets a distinct ID for per-thread mute functionality.
*/
function getThreadIdForWidget(widgetEl) {
try {
const urlHash = location.hash ? location.hash.replace('#', '') : 'root';
// Generate DOM path for widget position
const widgetPath = [];
let el = widgetEl;
for (let i = 0; i < 8 && el; i++) {
const parent = el.parentElement;
if (!parent || parent === document.body) break;
const siblings = Array.from(parent.children);
const index = siblings.indexOf(el);
widgetPath.unshift(`${parent.tagName}_${index}`);
el = parent;
}
const domPath = widgetPath.length > 0 ? widgetPath.join('_') : 'unknown';
// Get content-based hash from first comment
const context = getContextStringFor(widgetEl);
let contentHash = '';
if (context && context.length > 0) {
const firstComment = context.split('\n\n')[0] || context.substring(0, 150);
let hash = 0;
for (let i = 0; i < Math.min(firstComment.length, 150); i++) {
const char = firstComment.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
const parentText = widgetEl.parentElement ?
(widgetEl.parentElement.textContent || '').substring(0, 50).trim() : '';
if (parentText) {
let hash = 0;
for (let i = 0; i < parentText.length; i++) {
hash = ((hash << 5) - hash) + parentText.charCodeAt(i);
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
contentHash = Math.random().toString(36).slice(2, 8);
}
}
return `thread_${urlHash}_${domPath.substring(0, 50)}_${contentHash}`;
} catch (err) {
console.warn(`[${NAME}] Error generating thread ID:`, err);
return `thread_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}
}
function getMutedThreads() {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const stored = localStorage.getItem(storageKey);
if (!stored) return new Set();
const threads = JSON.parse(stored);
return new Set(Array.isArray(threads) ? threads : []);
} catch {
return new Set();
}
}
function saveMutedThreads(threadSet) {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const threads = Array.from(threadSet);
localStorage.setItem(storageKey, JSON.stringify(threads));
} catch (err) {
console.warn(`[${NAME}] Error saving muted threads:`, err);
}
}
function isThreadMuted(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
return muted.has(threadId);
}
function toggleThreadMute(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
const wasMuted = muted.has(threadId);
if (wasMuted) {
muted.delete(threadId);
} else {
muted.add(threadId);
}
saveMutedThreads(muted);
return !wasMuted;
}
/**
* Creates an unmute indicator button that appears when a thread is muted
* Allows users to restore ConvoWizard for a muted thread
*/
function ensureUnmuteIndicator(widgetEl, threadId, widgetId) {
const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator');
if (existingIndicator) return;
const indicator = document.createElement('div');
indicator.id = `convowizard-unmute-${widgetId}`;
indicator.className = 'convowizard-unmute-indicator';
indicator.style.cssText = `
margin: 4px 0;
padding: 6px 10px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: #f8f9fa;
color: #54595d;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s ease;
`;
const icon = document.createElement('span');
icon.textContent = '🔊';
icon.style.fontSize = '12px';
const text = document.createElement('span');
text.textContent = `${NAME} is muted for this thread. Click to unmute.`;
text.style.flex = '1';
indicator.appendChild(icon);
indicator.appendChild(text);
indicator.addEventListener('mouseenter', () => {
indicator.style.background = '#e9ecef';
indicator.style.borderColor = '#0645ad';
});
indicator.addEventListener('mouseleave', () => {
indicator.style.background = '#f8f9fa';
indicator.style.borderColor = '#a2a9b1';
});
indicator.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Show panels and remove indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
if (contextPanel) contextPanel.style.display = '';
if (replyPanel) replyPanel.style.display = '';
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
// Update mute buttons
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(btn => {
btn.textContent = '🔇 Mute thread';
btn.title = 'Mute ConvoWizard for this thread';
btn.style.background = '#ffffff';
});
// Re-trigger analysis to refresh display
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
// Re-run the continue request to refresh the display
const inputText = st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '';
if (inputText.trim().length > 0) {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('continue', {
existing: existingForContinue,
new: inputText,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
if (data && !data.error) {
handleIntervention(widgetEl, st.inputEl, data);
}
} else {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('start', {
existing,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
if (data && !data.error && data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
handleIntervention(widgetEl, st.inputEl, data);
}
}
} else {
startForWidget(widgetEl);
}
});
// Insert at the beginning of the widget
widgetEl.insertBefore(indicator, widgetEl.firstChild);
}
function createMuteButton(threadId, widgetId, isMuted) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-mute-btn';
btn.setAttribute('data-thread-id', threadId);
btn.setAttribute('data-widget-id', widgetId);
btn.style.cssText = `
margin-left: auto;
padding: 4px 8px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: ${isMuted ? '#f8f9fa' : '#ffffff'};
color: #54595d;
cursor: pointer;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
btn.textContent = isMuted
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = isMuted
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.addEventListener('mouseenter', () => {
btn.style.background = isMuted ? '#e9ecef' : '#f8f9fa';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = isMuted ? '#f8f9fa' : '#ffffff';
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Update button state
btn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
// Hide/show panels and unmute indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
const widgetEl = contextPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
contextPanel?.closest('.ext-discussiontools-ui-newTopicWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-newTopicWidget');
if (contextPanel) contextPanel.style.display = newMuteState ? 'none' : '';
if (replyPanel) replyPanel.style.display = newMuteState ? 'none' : '';
// Clear textbox background when muting
if (newMuteState) {
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
}
if (newMuteState && widgetEl) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (widgetEl) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Update all mute buttons for this thread
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(otherBtn => {
if (otherBtn !== btn) {
otherBtn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
otherBtn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
otherBtn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
}
});
});
return btn;
}
function ensurePanels(widgetEl, threadId = null) {
cleanupDuplicatePanels();
// Clean up duplicate unmute indicators
const unmuteIndicators = widgetEl.querySelectorAll('.convowizard-unmute-indicator');
if (unmuteIndicators.length > 1) {
for (let i = 1; i < unmuteIndicators.length; i++) {
unmuteIndicators[i].remove();
}
}
const widgetId = widgetEl.id || `widget_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
if (!widgetEl.id) widgetEl.id = widgetId;
const contextId = `context_${widgetId}`;
const replyId = `reply_${widgetId}`;
const isMuted = threadId ? isThreadMuted(threadId) : false;
// Ensure Context panel
if (!document.getElementById(`${contextId}_d`)) {
const contextBox = document.createElement('div');
contextBox.id = `${contextId}_d`;
contextBox.className = 'craftDisplay';
contextBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const cheader = document.createElement('div');
cheader.id = `${contextId}_h`;
cheader.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Context Summary`;
cheader.appendChild(iconSvg);
cheader.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
cheader.appendChild(muteBtn);
}
const ccontent = document.createElement('div');
ccontent.id = `${contextId}_p`;
ccontent.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
ccontent.textContent = `When you type a reply ${NAME} will give you some feedback on the discussion context.`;
contextBox.appendChild(cheader);
contextBox.appendChild(ccontent);
if (isMuted) contextBox.style.display = 'none';
widgetEl.prepend(contextBox);
} else if (threadId) {
const existingPanel = document.getElementById(`${contextId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
// Show unmute indicator when muted (only if panels don't exist yet)
if (threadId && isMuted && !document.getElementById(`${contextId}_d`) && !document.getElementById(`${replyId}_d`)) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (threadId && !isMuted) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Ensure Reply panel
if (!document.getElementById(`${replyId}_d`)) {
const replyBox = document.createElement('div');
replyBox.id = `${replyId}_d`;
replyBox.className = 'craftDisplay';
replyBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const header = document.createElement('div');
header.id = `${replyId}_h`;
header.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Reply Summary`;
header.appendChild(iconSvg);
header.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
header.appendChild(muteBtn);
}
const content = document.createElement('div');
content.id = `${replyId}_p`;
content.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
content.textContent = `When you type a reply ${NAME} will give you some feedback on your comment.`;
replyBox.appendChild(header);
replyBox.appendChild(content);
if (isMuted) replyBox.style.display = 'none';
const preview = widgetEl.querySelector('.ext-discussiontools-ui-replyWidget-preview')
|| document.querySelector('.ext-discussiontools-ui-replyWidget-preview');
if (preview && preview.parentElement) {
preview.parentElement.insertBefore(replyBox, preview.nextSibling);
} else {
widgetEl.append(replyBox);
}
} else if (threadId) {
const existingPanel = document.getElementById(`${replyId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
return { replyId, contextId };
}
function updateContextPanel(contextId, score, showScores) {
console.log(`[${NAME}] updateContextPanel called - contextId: ${contextId}, score: ${score}, showScores: ${showScores}`);
const hasScore = typeof score === 'number';
let text = showScores && hasScore ? `Context craft score: ${score.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
console.log(`[${NAME}] hasScore: ${hasScore}, score value: ${score}`);
console.log(`[${NAME}] Thresholds - MID: ${MID_TENSION_THRESH}, HIGH: ${HIGH_TENSION_THRESH}`);
if (hasScore && score > MID_TENSION_THRESH) {
let level;
if (score > HIGH_TENSION_THRESH) {
console.log(`[${NAME}] Very high tension (score > ${HIGH_TENSION_THRESH})`);
text += `${CONTEXT_VERY_HIGH}`;
level = 'veryHigh';
} else if (score > 0.60) {
console.log(`[${NAME}] High tension (score > 0.60)`);
text += `${CONTEXT_HIGH}`;
level = 'high';
} else {
console.log(`[${NAME}] Mid tension (score > ${MID_TENSION_THRESH})`);
text += `${CONTEXT_MID}`;
level = 'mid';
}
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else {
console.log(`[${NAME}] Low tension or no score`);
text += `${CONTEXT_LOW}`;
}
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
console.log(`[${NAME}] DOM elements - box: ${!!box}, p: ${!!p}, h: ${!!h}`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) {
p.textContent = text;
console.log(`[${NAME}] Updated text content (length: ${text.length})`);
}
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Context Summary`;
}
console.log(`[${NAME}] Updated header color: ${iconColor}`);
}
}
function updateReplyPanel(replyId, scoreChange, rawScore, showScores, inputEl) {
let text = showScores ? `Change in craft score after your comment: ${scoreChange.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
text += `${REPLY_HIGH}`;
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else if (rawScore - scoreChange > 0.55 && scoreChange < -SCORE_CHANGE_THRESH) {
text += `${REPLY_LOW}`;
borderColor = '#4caf50';
bgGradient = 'linear-gradient(135deg, #f1f8e9 0%, #f9fbe7 100%)';
iconColor = '#4caf50';
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 6L6.5 11 4 8.5l1-1 1.5 1.5L10.5 5l1 1z"/>
</svg>`;
} else {
text += `${REPLY_MID}`;
}
if (inputEl) {
let threadId = null;
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) {
const muteBtn = replyBox.querySelector('.convowizard-mute-btn');
if (muteBtn) {
threadId = muteBtn.getAttribute('data-thread-id');
}
}
// If no thread ID from panel, try to get from widget
if (!threadId) {
const widgetEl = inputEl.closest('.ext-discussiontools-ui-replyWidget') ||
inputEl.closest('.ext-discussiontools-ui-newTopicWidget');
if (widgetEl) {
threadId = getThreadIdForWidget(widgetEl);
}
}
// If muted, clear background and return
if (threadId && isThreadMuted(threadId)) {
inputEl.style.setProperty('background-color', 'transparent', 'important');
return;
}
let inputBg = 'transparent';
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
inputBg = TENSION_COLORS[level].inputBg;
} else if (scoreChange < -SCORE_CHANGE_THRESH) {
inputBg = 'rgba(76,175,80,0.05)';
}
inputEl.style.setProperty('background-color', inputBg, 'important');
inputEl.style.setProperty('transition', 'background-color 0.3s ease', 'important');
}
const box = document.getElementById(`${replyId}_d`);
const p = document.getElementById(`${replyId}_p`);
const h = document.getElementById(`${replyId}_h`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) p.textContent = text;
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary`;
}
}
}
function getContextStringFor(widgetEl) {
try {
console.log(`[${NAME}] === Extracting context for widget ===`);
// Helper to check if element is a comment container
const isCommentNode = (el) => !!el && (el.tagName === 'DD' || el.tagName === 'LI' || el.tagName === 'P');
// Extract clean comment text, removing nested replies and UI elements
// IMPORTANT: Keep timestamps (UTC) because backend splits on them!
const extractCommentText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
// Remove nested discussion lists, widgets, and UI elements
// NOTE: DO NOT remove .ext-discussiontools-init-timestamplink - backend needs timestamps to split utterances!
clone.querySelectorAll('dl, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, .ext-discussiontools-init-replylink-buttons, [role="button"]').forEach(n => n.remove());
const text = (clone.innerText || '').trim();
return text;
};
// Wikipedia Talk pages have structure: <dd>comment text<dl><dd>reply<dl><dd>widget
// When widget appears, it's nested inside a new <dd> inside a new <dl>
// We need to find the parent <dd> that contains actual comment text
let commentDD = null;
// Strategy 1: Widget is inside <dd> → <dl> → <dd> (the target comment)
// Find the <dd> containing the widget
const widgetDD = widgetEl.closest('dd, li');
console.log(`[${NAME}] Widget container DD:`, widgetDD ? widgetDD.tagName : 'not found');
if (widgetDD) {
// Check if this DD has meaningful text (not just the widget)
const widgetDDText = extractCommentText(widgetDD);
console.log(`[${NAME}] Widget DD text length:`, widgetDDText.length);
if (widgetDDText.length > 20) {
// This DD has actual comment text
commentDD = widgetDD;
} else {
// This DD is empty/minimal - the widget was just inserted
// Go up: DD → DL → parent DD (which should have the comment)
const parentDL = widgetDD.parentElement;
console.log(`[${NAME}] Parent DL:`, parentDL ? parentDL.tagName : 'not found');
if (parentDL && (parentDL.tagName === 'DL' || parentDL.tagName === 'UL' || parentDL.tagName === 'OL')) {
// Find the parent DD/LI/P that contains this DL
commentDD = parentDL.closest('dd, li, p');
console.log(`[${NAME}] Found parent comment node:`, commentDD ? commentDD.tagName : 'not found');
}
}
}
// Strategy 2: Look for preceding paragraph with comment markers (flat structure)
if (!commentDD) {
console.log(`[${NAME}] Trying flat structure search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 10) {
probe = probe.previousElementSibling || (probe.parentElement ? probe.parentElement.previousElementSibling : null);
if (probe && probe.tagName === 'P' && probe.querySelector('[data-mw-comment-start], [data-mw-comment-end]')) {
commentDD = probe;
console.log(`[${NAME}] Found comment via flat search at hop ${hops}`);
break;
}
}
}
// Strategy 3: Last resort - traverse DOM looking for any comment node
if (!commentDD) {
console.log(`[${NAME}] Trying fallback search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 20) {
if (isCommentNode(probe) && extractCommentText(probe).length > 20) {
commentDD = probe;
console.log(`[${NAME}] Found comment via fallback at hop ${hops}`);
break;
}
probe = probe.previousElementSibling ||
(probe.parentElement ? probe.parentElement.previousElementSibling : null) ||
(probe.parentElement ? probe.parentElement.parentElement : null);
}
}
if (!commentDD) {
console.warn(`[${NAME}] Could not find any comment node!`);
return '';
}
// Now traverse up the comment chain collecting all parent comments
const texts = [];
const seen = new Set();
let current = commentDD;
let iterations = 0;
console.log(`[${NAME}] Starting comment chain traversal from:`, current.tagName);
while (current && iterations++ < 20) {
if (seen.has(current)) {
console.log(`[${NAME}] Already seen this node, breaking`);
break;
}
seen.add(current);
const text = extractCommentText(current);
if (text && text.length > 0) {
console.log(`[${NAME}] Extracted comment (${text.length} chars):`, text.substring(0, 100) + '...');
texts.unshift(text); // Add to beginning to maintain chronological order
}
// Navigate up the discussion tree
// Structure: <p>text</p><dl><dd>reply<dl><dd>reply</dd></dl></dd></dl>
// From current <dd>, go to parent element (should be <dl>)
const parentList = current.parentElement;
if (!parentList) {
console.log(`[${NAME}] No parent element, stopping`);
break;
}
console.log(`[${NAME}] Parent list:`, parentList.tagName);
// From <dl>, find the parent <dd> or <li> or <p>
if (parentList.tagName === 'DL' || parentList.tagName === 'UL' || parentList.tagName === 'OL') {
// First try to find an ancestor comment node
current = parentList.closest('dd, li, p');
if (current) {
console.log(`[${NAME}] Found ancestor comment:`, current.tagName);
} else {
// No ancestor found - the <p> might be a SIBLING of this <dl>
// This happens in flat structure: <p>comment</p><dl><dd>reply...
console.log(`[${NAME}] No ancestor, searching for previous sibling of DL...`);
let sibling = parentList.previousElementSibling;
let siblingHops = 0;
while (sibling && siblingHops++ < 10) {
console.log(`[${NAME}] Checking sibling:`, sibling.tagName);
if (isCommentNode(sibling) && !seen.has(sibling)) {
const siblingText = extractCommentText(sibling);
if (siblingText.length > 20) {
current = sibling;
console.log(`[${NAME}] Found sibling comment with text (${siblingText.length} chars)`);
break;
}
}
sibling = sibling.previousElementSibling;
}
if (!current || current === parentList.closest('dd, li, p')) {
console.log(`[${NAME}] No more comments found, stopping`);
break;
}
}
} else if (isCommentNode(parentList)) {
// Parent is itself a comment node
current = parentList;
} else {
// Try to find previous sibling comment
let sibling = current.previousElementSibling;
while (sibling && !isCommentNode(sibling)) {
sibling = sibling.previousElementSibling;
}
if (sibling && !seen.has(sibling)) {
current = sibling;
console.log(`[${NAME}] Moving to sibling:`, current.tagName);
} else {
console.log(`[${NAME}] No more siblings, stopping`);
break;
}
}
}
console.log(`[${NAME}] Extracted ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
// Additional pass: Look for any <p> elements with comment markers that might have been missed
// This catches the case where the first comment in a thread is a <p> that's a sibling of the DL
if (commentDD && commentDD.tagName === 'DD') {
const topDL = commentDD.closest('dl');
if (topDL && topDL.previousElementSibling) {
let probe = topDL.previousElementSibling;
let probeHops = 0;
console.log(`[${NAME}] Scanning backwards from top DL for more context...`);
while (probe && probeHops++ < 5) {
if (probe.tagName === 'P' && !seen.has(probe)) {
const probeText = extractCommentText(probe);
if (probeText.length > 20 && (probe.querySelector('[data-mw-comment-start]') || probe.querySelector('[data-mw-comment-end]'))) {
console.log(`[${NAME}] Found additional preceding comment (${probeText.length} chars):`, probeText.substring(0, 100) + '...');
texts.unshift(probeText);
seen.add(probe);
}
}
probe = probe.previousElementSibling;
}
}
}
console.log(`[${NAME}] Final extraction: ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
if (texts.length > 0) {
const fullContext = texts.join('\n\n');
console.log(`[${NAME}] Full context:`, fullContext);
return fullContext;
}
console.warn(`[${NAME}] No context found, returning empty string`);
return '';
} catch (err) {
console.error(`[${NAME}] Error extracting context:`, err);
return '';
}
}
async function startForWidget(widgetEl) {
if (!TOKEN) { showTokenForm(widgetEl); return; }
if (widgetState.has(widgetEl) || widgetEl.getAttribute('data-convowizard-processing') === 'true') return;
widgetEl.setAttribute('data-convowizard-processing', 'true');
console.log(`[${NAME}] Starting widget processing:`, widgetEl.className);
function findInput(scope) {
// Prefer DT textbox role, then VE contenteditable surface, then textarea
return (
scope.querySelector('div[role="textbox"]') ||
scope.querySelector('[contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface [contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface') ||
scope.querySelector('textarea')
);
}
async function waitForInput(scope, timeoutMs = 8000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tick = () => {
const el = findInput(scope) || findInput(document);
if (el) return resolve(el);
if (Date.now() > deadline) return resolve(null);
setTimeout(tick, 100);
};
tick();
});
}
const inputEl = await waitForInput(widgetEl);
if (!inputEl) {
console.warn(`[${NAME}] No input element found in widget (after waiting)`);
return;
}
console.log(`[${NAME}] Input element found:`, inputEl.tagName, inputEl.getAttribute('role') || (inputEl.getAttribute('contenteditable') ? 'contenteditable' : ''));
const context = getContextStringFor(widgetEl);
console.log(`[${NAME}] Context extracted, length: ${context.length} chars`);
console.log(`[${NAME}] Context preview:`, context.substring(0, 200));
// Split context on (UTC) timestamps to create separate utterances like Reddit does
// This prevents backend caching bug where all Wikipedia convos share id=None
const utterances = context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = utterances.map((text, index) => ({
id: `wiki_${Date.now()}_${index}`, // Unique ID prevents cache collision
text: text.trim()
}));
console.log(`[${NAME}] Created ${existing.length} utterances with unique IDs`);
existing.forEach((utt, i) => {
console.log(`[${NAME}] ${i}: ID=${utt.id}, text=${utt.text.substring(0, 60)}...`);
});
const data = await postJson('start', {
existing, // Send as array like Reddit, not context string!
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
console.log(`[${NAME}] Received response from /start:`, data);
// Handle invalid token - clear and show auth form
if (data && data.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (!data || !data.interaction_id) return;
widgetState.set(widgetEl, { interactionId: data.interaction_id, inputEl, context });
handleIntervention(widgetEl, inputEl, data);
const sendContinue = throttle(async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
// Check if thread is muted
const threadId = getThreadIdForWidget(widgetEl);
if (threadId && isThreadMuted(threadId)) {
// Clear input background if muted
if (st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
return;
}
const widgetId = widgetEl.id;
const h = document.getElementById(`reply_${widgetId}_h`);
if (h) {
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>
`;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span with processing indicator
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary (Processing...)`;
}
}
// Recreate existing array from cached context for continue requests
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`, // Use interaction_id for consistency
text: text.trim()
}));
const data2 = await postJson('continue', {
existing: existingForContinue,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
// Handle invalid token on continue
if (data2 && data2.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
handleIntervention(widgetEl, st.inputEl, data2);
}, 3000);
inputEl.addEventListener('input', sendContinue);
inputEl.addEventListener('keyup', sendContinue);
inputEl.addEventListener('paste', sendContinue);
let postBtn = widgetEl.querySelector(".oo-ui-buttonElement-button[title*='Reply']");
if (!postBtn) {
postBtn = Array.from(widgetEl.querySelectorAll('.oo-ui-buttonElement-button'))
.find(b => /reply/i.test((b.title || b.innerText || '').trim()));
}
if (postBtn) {
postBtn.addEventListener('click', async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
try {
// Recreate existing array from cached context for submit
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForSubmit = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
await postJson('submit', {
existing: existingForSubmit,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
submitted_id: null,
token: TOKEN,
username: USERNAME
});
} finally {
widgetState.delete(widgetEl);
}
});
}
}
function handleIntervention(widgetEl, inputEl, data) {
if (!data) {
console.warn(`[${NAME}] handleIntervention called with no data`);
return;
}
console.log(`[${NAME}] handleIntervention called with data:`, data);
if (data.error) {
return;
}
const interactionId = data.interaction_id;
if (!interactionId) {
console.warn(`[${NAME}] No interaction_id in data`);
return;
}
const threadId = getThreadIdForWidget(widgetEl);
// Skip display if thread is muted
if (threadId && isThreadMuted(threadId)) {
const widgetId = widgetEl.id || `widget_${Date.now()}`;
if (!widgetEl.id) widgetEl.id = widgetId;
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
return;
}
const { replyId, contextId } = ensurePanels(widgetEl, threadId);
const showScores = Object.prototype.hasOwnProperty.call(data, 'show_scores');
console.log(`[${NAME}] Panel IDs - context: ${contextId}, reply: ${replyId}`);
console.log(`[${NAME}] Show scores: ${showScores}`);
const which = (data.which || '').substring(0, 5);
console.log(`[${NAME}] Response type (which): ${which}`);
if (which === 'craft') {
console.log(`[${NAME}] CRAFT mode - craft_ctx_score:`, data.craft_ctx_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_score:`, data.craft_reply_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_change:`, data.craft_reply_change);
if (data.craft_ctx_score != null) {
console.log(`[${NAME}] Updating context panel with score: ${data.craft_ctx_score}`);
updateContextPanel(contextId, data.craft_ctx_score, showScores);
} else {
console.log(`[${NAME}] No context score, updating context panel with null`);
updateContextPanel(contextId, null, showScores);
}
if (data.craft_reply_change != null && data.craft_reply_score != null) {
console.log(`[${NAME}] Updating reply panel with change: ${data.craft_reply_change}, score: ${data.craft_reply_score}`);
updateReplyPanel(replyId, data.craft_reply_change, data.craft_reply_score, showScores, inputEl);
} else {
console.log(`[${NAME}] No reply scores, updating reply panel with zeros`);
updateReplyPanel(replyId, 0, 0, showScores, inputEl);
}
} else {
const noteId = `passivenote${interactionId}_d`;
if (!data.message && !document.getElementById(noteId)) {
const box = document.createElement('div');
box.id = noteId;
box.className = 'passiveModeDisplay';
box.style.cssText = `
margin:8px 0;padding:8px 12px;border-radius:6px;border:1px solid #a2a9b1;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;opacity:.8;
`;
const content = document.createElement('div');
content.id = `passivenote${interactionId}_p`;
content.style.cssText = `font-size:12px;color:#54595d;margin:0;font-style:italic;`;
content.textContent = `${NAME} is currently not active on this thread.`;
box.appendChild(content);
widgetEl.prepend(box);
}
}
}
function installClickDelegation() {
document.addEventListener('click', (e) => {
console.log(`[${NAME}] Click detected on:`, e.target.tagName, e.target.className);
// Match reply buttons and links (both old and new DiscussionTools formats)
// Need to check the actual clicked element AND traverse up
const link = e.target.closest && (
e.target.closest('.ext-discussiontools-init-replybutton') ||
e.target.closest('.ext-discussiontools-init-replylink-reply') ||
e.target.closest('a.ext-discussiontools-init-replylink') ||
e.target.closest('.ext-discussiontools-init-replylink-buttons') ||
e.target.closest('[class*="ext-discussiontools-init-"][class*="reply"]')
);
if (!link) return;
console.log(`[${NAME}] Reply link clicked:`, link.className);
// For flat discussion structures (like Research talk pages), the widget may be inserted
// outside the immediate parent. Search more broadly.
const anchorScope = link.closest('dd, li, p, div, section') || document.body;
const broadScope = anchorScope.closest('#mw-content-text, .mw-parser-output, #content') || document.body;
console.log(`[${NAME}] Anchor scope:`, anchorScope.tagName, anchorScope.className);
console.log(`[${NAME}] Broad scope:`, broadScope.tagName, broadScope.className);
const timeoutAt = Date.now() + 8000;
let attempts = 0;
const poll = setInterval(() => {
attempts++;
// Try multiple search strategies:
// 1. In immediate anchor scope (for nested structures like dd/li)
// 2. As sibling to anchor scope (for flat structures like p)
// 3. In broader content scope
// 4. Document-wide fallback
const widget = anchorScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| anchorScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| (anchorScope.nextElementSibling && (
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-replyWidget') ||
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-newTopicWidget')
) ? anchorScope.nextElementSibling : null)
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-replyWidget:last-child'))
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-newTopicWidget:last-child'))
|| broadScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| broadScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| document.querySelector('.ext-discussiontools-ui-replyWidget')
|| document.querySelector('.ext-discussiontools-ui-newTopicWidget');
if (widget) {
console.log(`[${NAME}] Widget found after ${attempts} attempts:`, widget.className);
clearInterval(poll);
startForWidget(widget);
}
else if (Date.now() > timeoutAt) {
console.log(`[${NAME}] Widget timeout after ${attempts} attempts - no widget found`);
console.log(`[${NAME}] Searched in anchorScope:`, anchorScope);
console.log(`[${NAME}] Searched in broadScope:`, broadScope);
clearInterval(poll);
} else if (attempts % 10 === 0) {
console.log(`[${NAME}] Still searching for widget, attempt ${attempts}...`);
}
}, 100);
});
}
function installWidgetObserver() {
const root = document.getElementById('content') || document.body;
console.log(`[${NAME}] Setting up mutation observer on:`, root.id || root.tagName);
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof HTMLElement)) continue;
// Log all significant additions for debugging
if (n.className && typeof n.className === 'string' && n.className.includes('ext-discussiontools')) {
console.log(`[${NAME}] DOM addition detected:`, n.tagName, n.className);
}
// Match specific DiscussionTools widgets
if (n.matches('.ext-discussiontools-ui-replyWidget') || n.matches('.ext-discussiontools-ui-newTopicWidget')) {
console.log(`[${NAME}] *** Widget detected via observer ***:`, n.className);
startForWidget(n);
} else {
const nested = n.querySelector && (
n.querySelector('.ext-discussiontools-ui-replyWidget') ||
n.querySelector('.ext-discussiontools-ui-newTopicWidget')
);
if (nested) {
console.log(`[${NAME}] *** Nested widget detected ***:`, nested.className);
startForWidget(nested);
}
}
}
}
});
obs.observe(root, { childList: true, subtree: true });
console.log(`[${NAME}] Mutation observer is now watching for widget creation`);
// Check for existing widgets on page load
const existingWidgets = document.querySelectorAll('.ext-discussiontools-ui-replyWidget, .ext-discussiontools-ui-newTopicWidget');
if (existingWidgets.length > 0) {
console.log(`[${NAME}] Found ${existingWidgets.length} existing widget(s) on page load`);
existingWidgets.forEach(w => {
console.log(`[${NAME}] Processing existing widget:`, w.className);
startForWidget(w);
});
} else {
console.log(`[${NAME}] No existing widgets found on page load`);
}
}
// kick off
installClickDelegation();
installWidgetObserver();
console.log(`[${NAME}] === INITIALIZATION COMPLETE ===`);
console.log(`[${NAME}] Click delegation: ACTIVE`);
console.log(`[${NAME}] Mutation observer: ACTIVE`);
console.log(`[${NAME}] Waiting for user interaction...`);
})();
//</nowiki>
7okx55pztcah4ihmthez32j4psjj61s
740022
740018
2026-05-01T15:57:09Z
Laerdon
70887
740022
javascript
text/javascript
//<nowiki>
/**
* Filename: wiki-talk-page-script.js
* ConvoWizard – Real-time Reply Feedback (Wikipedia userscript)
* Runs on Talk pages inside DiscussionTools reply widgets.
* Prompts for a token on first run; persists after authorization.
*/
(async function () {
'use strict';
// Load core util module via ResourceLoader
await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api']);
// === CONFIG ===
const SERVER = 'https://convocompass.toolforge.org/api/';
// ---- UI strings and thresholds ----
const NAME = 'ConvoWizard';
const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search);
const OPTION_KEY = 'userjs-convowizard-token';
const SCORE_CHANGE_THRESH = 0.08;
const MID_TENSION_THRESH = 0.50;
const HIGH_TENSION_THRESH = 0.75;
const CONTEXT_LOW = `${NAME} will notify you here if it detects anything in the preceding conversation.`;
const CONTEXT_MID = `${NAME} thinks this discussion is getting somewhat tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_HIGH = `${NAME} thinks this discussion is getting tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_VERY_HIGH = `${NAME} thinks this discussion is getting very tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const REPLY_LOW = `${NAME} thinks this comment might decrease tension in this discussion.`;
const REPLY_MID = `${NAME} will notify you here if it detects anything in your comment draft.`;
const REPLY_HIGH = `${NAME} thinks this comment might increase the tension in this discussion. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const TENSION_COLORS = {
mid: { border: '#e8825c', bg: 'linear-gradient(135deg, #fff0eb 0%, #fff8f5 100%)', icon: '#e8825c', inputBg: 'rgba(232,130,92,0.05)' },
high: { border: '#d73027', bg: 'linear-gradient(135deg, #ffeaea 0%, #fff5f5 100%)', icon: '#d73027', inputBg: 'rgba(215,48,39,0.05)' },
veryHigh: { border: '#a50f15', bg: 'linear-gradient(135deg, #fdd 0%, #fee 100%)', icon: '#a50f15', inputBg: 'rgba(165,15,21,0.07)' },
};
// Keep ConvoWizard logs quiet by default to reduce console noise on Wikimedia pages.
const nativeConsole = window.console;
const console = {
log: (...args) => {
if (!DEBUG && typeof args[0] === 'string' && args[0].startsWith(`[${NAME}]`)) {
return;
}
nativeConsole.log(...args);
},
warn: (...args) => nativeConsole.warn(...args),
error: (...args) => nativeConsole.error(...args)
};
const isWikipediaHost = /(^|\.)wikipedia\.org$/.test(location.hostname);
const namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
const isTalkNamespace = Number.isFinite(namespaceNumber) && namespaceNumber % 2 === 1;
const isViewAction = mw.config.get('wgAction') === 'view';
// Gadget scope guard: only run on Wikipedia talk pages in normal view mode.
if (!isWikipediaHost || !isTalkNamespace || !isViewAction) {
console.log(
`[${NAME}] Skipping page (host=${location.hostname}, ns=${namespaceNumber}, action=${mw.config.get('wgAction')})`
);
return;
}
// ---------- helpers ----------
function throttle(fn, waitMs) {
let last = 0, timer = 0, pendingArgs = null;
return (...args) => {
const now = Date.now();
const remaining = waitMs - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
} else {
pendingArgs = args;
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn(...pendingArgs); }, remaining);
}
};
}
// Rewrite Wikipedia URL so the reddit-style backend can extract a post_id.
// Backend does: url.split("comments/")[1][:6]
// We inject "comments/<hash>" where hash is derived from the wiki page path.
function wikiUrlToPostId(href) {
try {
const pagePath = href.split('/wiki/')[1] || href;
// Simple hash to produce a stable 6-char hex id per page
let h = 0;
for (let i = 0; i < pagePath.length; i++) {
h = ((h << 5) - h) + pagePath.charCodeAt(i);
h = h & h;
}
const hex = Math.abs(h).toString(16).padStart(6, '0').slice(0, 6);
return href.split('/wiki/')[0] + '/comments/' + hex;
} catch {
return href;
}
}
async function postJson(route, body) {
try {
const res = await fetch(SERVER + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
console.log(`[${NAME}] response from ${route}: status=${res.status}`, text);
try {
const data = JSON.parse(text);
console.log(`[${NAME}] json from ${route}:`, data);
return data;
} catch (err) {
console.warn(`[${NAME}] non-json response from ${route}:`, text);
return {};
}
}
function getTokenFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(OPTION_KEY);
if (typeof raw === 'string') {
const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
}
}
} catch (err) {
console.warn(`[${NAME}] Failed to read token from user options`, err);
}
return null;
}
async function persistToken(storageKey, token) {
if (!token) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, token);
}
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(OPTION_KEY, token || '');
await new mw.Api().saveOption(OPTION_KEY, token || '');
console.log(`[${NAME}] ${token ? 'Saved' : 'Cleared'} token in user options (notepad)`);
} catch (err) {
console.warn(`[${NAME}] Could not persist token to user options`, err);
}
}
async function validateToken(token, username) {
try {
const response = await postJson('claim_token', { token, username });
return response.valid === true;
} catch {
return false;
}
}
// ========== CREDENTIAL SETUP ==========
const USERNAME = mw.config.get('wgUserName');
if (!USERNAME) {
console.log(`[${NAME}] Not logged in, skipping`);
return;
}
let TOKEN = null;
const STORAGE_KEY = `ConvoWizard:token:${USERNAME}`;
// Load persisted token (options → localStorage), validate, sync stores
async function loadPersistedToken() {
let token = getTokenFromOptions();
let source = 'options';
if (!token) {
token = localStorage.getItem(STORAGE_KEY);
source = 'localStorage';
}
if (token && typeof token === 'string') {
token = token.trim();
}
if (!token) return;
const valid = await validateToken(token, USERNAME);
if (valid) {
TOKEN = token;
// Sync both stores
if (source === 'options') {
localStorage.setItem(STORAGE_KEY, token);
} else {
await persistToken(STORAGE_KEY, token);
}
console.log(`[${NAME}] Token loaded from ${source}`);
} else {
console.log(`[${NAME}] Persisted token invalid, clearing`);
await persistToken(STORAGE_KEY, null);
}
}
await loadPersistedToken();
console.log(`[${NAME}] === SCRIPT LOADED SUCCESSFULLY ===`);
console.log(`[${NAME}] Server: ${SERVER}`);
console.log(`[${NAME}] User: ${USERNAME}`);
console.log(`[${NAME}] Token: ${TOKEN ? 'present' : 'none (will prompt)'}`);
console.log(`[${NAME}] Page URL: ${location.href}`);
console.log(`[${NAME}] Installing click and mutation observers...`);
// ========== TOKEN INPUT FORM ==========
function showTokenForm(widgetEl) {
// Don't add duplicate forms
if (widgetEl.querySelector('.convowizard-auth-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-auth-form';
form.style.cssText = `
margin:8px 0;padding:16px 20px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
const title = document.createElement('div');
title.style.cssText = 'font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;';
title.textContent = `${NAME}: Authorization Required`;
const desc = document.createElement('div');
desc.style.cssText = 'font-size:13px;color:#333;margin-bottom:12px;line-height:1.4;';
desc.textContent = 'Please paste your ConvoWizard token below to activate the extension.';
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;align-items:center;';
const input = document.createElement('input');
input.type = 'password';
input.placeholder = 'Paste token here';
input.autocomplete = 'off';
input.spellcheck = false;
input.style.cssText = `
flex:1;padding:8px 10px;font-size:13px;border:1px solid #a2a9b1;border-radius:4px;
font-family:monospace;outline:none;transition:border-color 0.2s ease;
`;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Authorize';
btn.style.cssText = `
padding:8px 16px;font-size:13px;font-weight:600;border:none;border-radius:4px;
background:#0645ad;color:#fff;cursor:pointer;transition:background 0.2s ease;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
btn.addEventListener('mouseenter', () => { btn.style.background = '#0b5cce'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#0645ad'; });
const errDiv = document.createElement('div');
errDiv.style.cssText = 'font-size:12px;color:#d73027;margin-top:8px;display:none;';
async function onAuthorize() {
const raw = input.value.trim();
// Input validation: 33 hex chars
if (!/^[0-9a-f]{33}$/i.test(raw)) {
errDiv.textContent = 'Invalid token format. A token is 33 hexadecimal characters.';
errDiv.style.display = 'block';
input.value = '';
input.focus();
return;
}
btn.disabled = true;
btn.textContent = 'Validating...';
errDiv.style.display = 'none';
const valid = await validateToken(raw, USERNAME);
// Clear input immediately after use
input.value = '';
if (valid) {
TOKEN = raw;
await persistToken(STORAGE_KEY, TOKEN);
// Remove all auth forms on page
document.querySelectorAll('.convowizard-auth-form').forEach(f => f.remove());
// Activate the widget
startForWidget(widgetEl);
} else {
errDiv.textContent = 'Token is invalid or expired. Please check and try again.';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Authorize';
input.focus();
}
}
btn.addEventListener('click', onAuthorize);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); onAuthorize(); }
});
row.appendChild(input);
row.appendChild(btn);
form.appendChild(title);
form.appendChild(desc);
form.appendChild(row);
form.appendChild(errDiv);
widgetEl.prepend(form);
input.focus();
}
// ---- state + logs ----
const widgetState = new WeakMap();
function cleanupDuplicatePanels() {
const allPanels = document.querySelectorAll('.craftDisplay, .passiveModeDisplay');
const seen = new Set();
allPanels.forEach(panel => {
const pid = panel.id;
if (pid && pid.includes('_')) {
const parts = pid.split('_');
const type = parts[0];
const wid = parts.slice(1).join('_').replace('_d', '');
const key = `${type}|${wid}`;
if (seen.has(key)) panel.remove(); else seen.add(key);
}
});
}
// ========== MUTE THREAD ==========
/**
* Generates a unique thread identifier by combining URL section, DOM position, and content hash.
* Ensures each thread on a page gets a distinct ID for per-thread mute functionality.
*/
function getThreadIdForWidget(widgetEl) {
try {
const urlHash = location.hash ? location.hash.replace('#', '') : 'root';
// Generate DOM path for widget position
const widgetPath = [];
let el = widgetEl;
for (let i = 0; i < 8 && el; i++) {
const parent = el.parentElement;
if (!parent || parent === document.body) break;
const siblings = Array.from(parent.children);
const index = siblings.indexOf(el);
widgetPath.unshift(`${parent.tagName}_${index}`);
el = parent;
}
const domPath = widgetPath.length > 0 ? widgetPath.join('_') : 'unknown';
// Get content-based hash from first comment
const context = getContextStringFor(widgetEl);
let contentHash = '';
if (context && context.length > 0) {
const firstComment = context.split('\n\n')[0] || context.substring(0, 150);
let hash = 0;
for (let i = 0; i < Math.min(firstComment.length, 150); i++) {
const char = firstComment.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
const parentText = widgetEl.parentElement ?
(widgetEl.parentElement.textContent || '').substring(0, 50).trim() : '';
if (parentText) {
let hash = 0;
for (let i = 0; i < parentText.length; i++) {
hash = ((hash << 5) - hash) + parentText.charCodeAt(i);
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
contentHash = Math.random().toString(36).slice(2, 8);
}
}
return `thread_${urlHash}_${domPath.substring(0, 50)}_${contentHash}`;
} catch (err) {
console.warn(`[${NAME}] Error generating thread ID:`, err);
return `thread_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}
}
function getMutedThreads() {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const stored = localStorage.getItem(storageKey);
if (!stored) return new Set();
const threads = JSON.parse(stored);
return new Set(Array.isArray(threads) ? threads : []);
} catch {
return new Set();
}
}
function saveMutedThreads(threadSet) {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const threads = Array.from(threadSet);
localStorage.setItem(storageKey, JSON.stringify(threads));
} catch (err) {
console.warn(`[${NAME}] Error saving muted threads:`, err);
}
}
function isThreadMuted(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
return muted.has(threadId);
}
function toggleThreadMute(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
const wasMuted = muted.has(threadId);
if (wasMuted) {
muted.delete(threadId);
} else {
muted.add(threadId);
}
saveMutedThreads(muted);
return !wasMuted;
}
/**
* Creates an unmute indicator button that appears when a thread is muted
* Allows users to restore ConvoWizard for a muted thread
*/
function ensureUnmuteIndicator(widgetEl, threadId, widgetId) {
const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator');
if (existingIndicator) return;
const indicator = document.createElement('div');
indicator.id = `convowizard-unmute-${widgetId}`;
indicator.className = 'convowizard-unmute-indicator';
indicator.style.cssText = `
margin: 4px 0;
padding: 6px 10px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: #f8f9fa;
color: #54595d;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s ease;
`;
const icon = document.createElement('span');
icon.textContent = '🔊';
icon.style.fontSize = '12px';
const text = document.createElement('span');
text.textContent = `${NAME} is muted for this thread. Click to unmute.`;
text.style.flex = '1';
indicator.appendChild(icon);
indicator.appendChild(text);
indicator.addEventListener('mouseenter', () => {
indicator.style.background = '#e9ecef';
indicator.style.borderColor = '#0645ad';
});
indicator.addEventListener('mouseleave', () => {
indicator.style.background = '#f8f9fa';
indicator.style.borderColor = '#a2a9b1';
});
indicator.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Show panels and remove indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
if (contextPanel) contextPanel.style.display = '';
if (replyPanel) replyPanel.style.display = '';
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
// Update mute buttons
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(btn => {
btn.textContent = '🔇 Mute thread';
btn.title = 'Mute ConvoWizard for this thread';
btn.style.background = '#ffffff';
});
// Re-trigger analysis to refresh display
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
// Re-run the continue request to refresh the display
const inputText = st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '';
if (inputText.trim().length > 0) {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('continue', {
existing: existingForContinue,
new: inputText,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
if (data && !data.error) {
handleIntervention(widgetEl, st.inputEl, data);
}
} else {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('start', {
existing,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
if (data && !data.error && data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
handleIntervention(widgetEl, st.inputEl, data);
}
}
} else {
startForWidget(widgetEl);
}
});
// Insert at the beginning of the widget
widgetEl.insertBefore(indicator, widgetEl.firstChild);
}
function createMuteButton(threadId, widgetId, isMuted) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-mute-btn';
btn.setAttribute('data-thread-id', threadId);
btn.setAttribute('data-widget-id', widgetId);
btn.style.cssText = `
margin-left: auto;
padding: 4px 8px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: ${isMuted ? '#f8f9fa' : '#ffffff'};
color: #54595d;
cursor: pointer;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
btn.textContent = isMuted
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = isMuted
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.addEventListener('mouseenter', () => {
btn.style.background = isMuted ? '#e9ecef' : '#f8f9fa';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = isMuted ? '#f8f9fa' : '#ffffff';
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Update button state
btn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
// Hide/show panels and unmute indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
const widgetEl = contextPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
contextPanel?.closest('.ext-discussiontools-ui-newTopicWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-newTopicWidget');
if (contextPanel) contextPanel.style.display = newMuteState ? 'none' : '';
if (replyPanel) replyPanel.style.display = newMuteState ? 'none' : '';
// Clear textbox background when muting
if (newMuteState) {
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
}
if (newMuteState && widgetEl) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (widgetEl) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Update all mute buttons for this thread
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(otherBtn => {
if (otherBtn !== btn) {
otherBtn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
otherBtn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
otherBtn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
}
});
});
return btn;
}
function ensurePanels(widgetEl, threadId = null) {
cleanupDuplicatePanels();
// Clean up duplicate unmute indicators
const unmuteIndicators = widgetEl.querySelectorAll('.convowizard-unmute-indicator');
if (unmuteIndicators.length > 1) {
for (let i = 1; i < unmuteIndicators.length; i++) {
unmuteIndicators[i].remove();
}
}
const widgetId = widgetEl.id || `widget_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
if (!widgetEl.id) widgetEl.id = widgetId;
const contextId = `context_${widgetId}`;
const replyId = `reply_${widgetId}`;
const isMuted = threadId ? isThreadMuted(threadId) : false;
// Ensure Context panel
if (!document.getElementById(`${contextId}_d`)) {
const contextBox = document.createElement('div');
contextBox.id = `${contextId}_d`;
contextBox.className = 'craftDisplay';
contextBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const cheader = document.createElement('div');
cheader.id = `${contextId}_h`;
cheader.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Context Summary`;
cheader.appendChild(iconSvg);
cheader.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
cheader.appendChild(muteBtn);
}
const ccontent = document.createElement('div');
ccontent.id = `${contextId}_p`;
ccontent.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
ccontent.textContent = `When you type a reply ${NAME} will give you some feedback on the discussion context.`;
contextBox.appendChild(cheader);
contextBox.appendChild(ccontent);
if (isMuted) contextBox.style.display = 'none';
widgetEl.prepend(contextBox);
} else if (threadId) {
const existingPanel = document.getElementById(`${contextId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
// Show unmute indicator when muted (only if panels don't exist yet)
if (threadId && isMuted && !document.getElementById(`${contextId}_d`) && !document.getElementById(`${replyId}_d`)) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (threadId && !isMuted) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Ensure Reply panel
if (!document.getElementById(`${replyId}_d`)) {
const replyBox = document.createElement('div');
replyBox.id = `${replyId}_d`;
replyBox.className = 'craftDisplay';
replyBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const header = document.createElement('div');
header.id = `${replyId}_h`;
header.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Reply Summary`;
header.appendChild(iconSvg);
header.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
header.appendChild(muteBtn);
}
const content = document.createElement('div');
content.id = `${replyId}_p`;
content.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
content.textContent = `When you type a reply ${NAME} will give you some feedback on your comment.`;
replyBox.appendChild(header);
replyBox.appendChild(content);
if (isMuted) replyBox.style.display = 'none';
const preview = widgetEl.querySelector('.ext-discussiontools-ui-replyWidget-preview')
|| document.querySelector('.ext-discussiontools-ui-replyWidget-preview');
if (preview && preview.parentElement) {
preview.parentElement.insertBefore(replyBox, preview.nextSibling);
} else {
widgetEl.append(replyBox);
}
} else if (threadId) {
const existingPanel = document.getElementById(`${replyId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
return { replyId, contextId };
}
function updateContextPanel(contextId, score, showScores) {
console.log(`[${NAME}] updateContextPanel called - contextId: ${contextId}, score: ${score}, showScores: ${showScores}`);
const hasScore = typeof score === 'number';
let text = showScores && hasScore ? `Context craft score: ${score.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
console.log(`[${NAME}] hasScore: ${hasScore}, score value: ${score}`);
console.log(`[${NAME}] Thresholds - MID: ${MID_TENSION_THRESH}, HIGH: ${HIGH_TENSION_THRESH}`);
if (hasScore && score > MID_TENSION_THRESH) {
let level;
if (score > HIGH_TENSION_THRESH) {
console.log(`[${NAME}] Very high tension (score > ${HIGH_TENSION_THRESH})`);
text += `${CONTEXT_VERY_HIGH}`;
level = 'veryHigh';
} else if (score > 0.60) {
console.log(`[${NAME}] High tension (score > 0.60)`);
text += `${CONTEXT_HIGH}`;
level = 'high';
} else {
console.log(`[${NAME}] Mid tension (score > ${MID_TENSION_THRESH})`);
text += `${CONTEXT_MID}`;
level = 'mid';
}
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else {
console.log(`[${NAME}] Low tension or no score`);
text += `${CONTEXT_LOW}`;
}
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
console.log(`[${NAME}] DOM elements - box: ${!!box}, p: ${!!p}, h: ${!!h}`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) {
p.textContent = text;
console.log(`[${NAME}] Updated text content (length: ${text.length})`);
}
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Context Summary`;
}
console.log(`[${NAME}] Updated header color: ${iconColor}`);
}
}
function updateReplyPanel(replyId, scoreChange, rawScore, showScores, inputEl) {
let text = showScores ? `Change in craft score after your comment: ${scoreChange.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
text += `${REPLY_HIGH}`;
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else if (rawScore - scoreChange > 0.55 && scoreChange < -SCORE_CHANGE_THRESH) {
text += `${REPLY_LOW}`;
borderColor = '#4caf50';
bgGradient = 'linear-gradient(135deg, #f1f8e9 0%, #f9fbe7 100%)';
iconColor = '#4caf50';
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 6L6.5 11 4 8.5l1-1 1.5 1.5L10.5 5l1 1z"/>
</svg>`;
} else {
text += `${REPLY_MID}`;
}
if (inputEl) {
let threadId = null;
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) {
const muteBtn = replyBox.querySelector('.convowizard-mute-btn');
if (muteBtn) {
threadId = muteBtn.getAttribute('data-thread-id');
}
}
// If no thread ID from panel, try to get from widget
if (!threadId) {
const widgetEl = inputEl.closest('.ext-discussiontools-ui-replyWidget') ||
inputEl.closest('.ext-discussiontools-ui-newTopicWidget');
if (widgetEl) {
threadId = getThreadIdForWidget(widgetEl);
}
}
// If muted, clear background and return
if (threadId && isThreadMuted(threadId)) {
inputEl.style.setProperty('background-color', 'transparent', 'important');
return;
}
let inputBg = 'transparent';
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
inputBg = TENSION_COLORS[level].inputBg;
} else if (scoreChange < -SCORE_CHANGE_THRESH) {
inputBg = 'rgba(76,175,80,0.05)';
}
inputEl.style.setProperty('background-color', inputBg, 'important');
inputEl.style.setProperty('transition', 'background-color 0.3s ease', 'important');
}
const box = document.getElementById(`${replyId}_d`);
const p = document.getElementById(`${replyId}_p`);
const h = document.getElementById(`${replyId}_h`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) p.textContent = text;
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary`;
}
}
}
function getContextStringFor(widgetEl) {
try {
console.log(`[${NAME}] === Extracting context for widget ===`);
// Helper to check if element is a comment container
const isCommentNode = (el) => !!el && (el.tagName === 'DD' || el.tagName === 'LI' || el.tagName === 'P');
// Extract clean comment text, removing nested replies and UI elements
// IMPORTANT: Keep timestamps (UTC) because backend splits on them!
const extractCommentText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
// Remove nested discussion lists, widgets, and UI elements
// NOTE: DO NOT remove .ext-discussiontools-init-timestamplink - backend needs timestamps to split utterances!
clone.querySelectorAll('dl, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, .ext-discussiontools-init-replylink-buttons, [role="button"]').forEach(n => n.remove());
const text = (clone.innerText || '').trim();
return text;
};
// Wikipedia Talk pages have structure: <dd>comment text<dl><dd>reply<dl><dd>widget
// When widget appears, it's nested inside a new <dd> inside a new <dl>
// We need to find the parent <dd> that contains actual comment text
let commentDD = null;
// Strategy 1: Widget is inside <dd> → <dl> → <dd> (the target comment)
// Find the <dd> containing the widget
const widgetDD = widgetEl.closest('dd, li');
console.log(`[${NAME}] Widget container DD:`, widgetDD ? widgetDD.tagName : 'not found');
if (widgetDD) {
// Check if this DD has meaningful text (not just the widget)
const widgetDDText = extractCommentText(widgetDD);
console.log(`[${NAME}] Widget DD text length:`, widgetDDText.length);
if (widgetDDText.length > 20) {
// This DD has actual comment text
commentDD = widgetDD;
} else {
// This DD is empty/minimal - the widget was just inserted
// Go up: DD → DL → parent DD (which should have the comment)
const parentDL = widgetDD.parentElement;
console.log(`[${NAME}] Parent DL:`, parentDL ? parentDL.tagName : 'not found');
if (parentDL && (parentDL.tagName === 'DL' || parentDL.tagName === 'UL' || parentDL.tagName === 'OL')) {
// Find the parent DD/LI/P that contains this DL
commentDD = parentDL.closest('dd, li, p');
console.log(`[${NAME}] Found parent comment node:`, commentDD ? commentDD.tagName : 'not found');
}
}
}
// Strategy 2: Look for preceding paragraph with comment markers (flat structure)
if (!commentDD) {
console.log(`[${NAME}] Trying flat structure search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 10) {
probe = probe.previousElementSibling || (probe.parentElement ? probe.parentElement.previousElementSibling : null);
if (probe && probe.tagName === 'P' && probe.querySelector('[data-mw-comment-start], [data-mw-comment-end]')) {
commentDD = probe;
console.log(`[${NAME}] Found comment via flat search at hop ${hops}`);
break;
}
}
}
// Strategy 3: Last resort - traverse DOM looking for any comment node
if (!commentDD) {
console.log(`[${NAME}] Trying fallback search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 20) {
if (isCommentNode(probe) && extractCommentText(probe).length > 20) {
commentDD = probe;
console.log(`[${NAME}] Found comment via fallback at hop ${hops}`);
break;
}
probe = probe.previousElementSibling ||
(probe.parentElement ? probe.parentElement.previousElementSibling : null) ||
(probe.parentElement ? probe.parentElement.parentElement : null);
}
}
if (!commentDD) {
console.warn(`[${NAME}] Could not find any comment node!`);
return '';
}
// Now traverse up the comment chain collecting all parent comments
const texts = [];
const seen = new Set();
let current = commentDD;
let iterations = 0;
console.log(`[${NAME}] Starting comment chain traversal from:`, current.tagName);
while (current && iterations++ < 20) {
if (seen.has(current)) {
console.log(`[${NAME}] Already seen this node, breaking`);
break;
}
seen.add(current);
const text = extractCommentText(current);
if (text && text.length > 0) {
console.log(`[${NAME}] Extracted comment (${text.length} chars):`, text.substring(0, 100) + '...');
texts.unshift(text); // Add to beginning to maintain chronological order
}
// Navigate up the discussion tree
// Structure: <p>text</p><dl><dd>reply<dl><dd>reply</dd></dl></dd></dl>
// From current <dd>, go to parent element (should be <dl>)
const parentList = current.parentElement;
if (!parentList) {
console.log(`[${NAME}] No parent element, stopping`);
break;
}
console.log(`[${NAME}] Parent list:`, parentList.tagName);
// From <dl>, find the parent <dd> or <li> or <p>
if (parentList.tagName === 'DL' || parentList.tagName === 'UL' || parentList.tagName === 'OL') {
// First try to find an ancestor comment node
current = parentList.closest('dd, li, p');
if (current) {
console.log(`[${NAME}] Found ancestor comment:`, current.tagName);
} else {
// No ancestor found - the <p> might be a SIBLING of this <dl>
// This happens in flat structure: <p>comment</p><dl><dd>reply...
console.log(`[${NAME}] No ancestor, searching for previous sibling of DL...`);
let sibling = parentList.previousElementSibling;
let siblingHops = 0;
while (sibling && siblingHops++ < 10) {
console.log(`[${NAME}] Checking sibling:`, sibling.tagName);
if (isCommentNode(sibling) && !seen.has(sibling)) {
const siblingText = extractCommentText(sibling);
if (siblingText.length > 20) {
current = sibling;
console.log(`[${NAME}] Found sibling comment with text (${siblingText.length} chars)`);
break;
}
}
sibling = sibling.previousElementSibling;
}
if (!current || current === parentList.closest('dd, li, p')) {
console.log(`[${NAME}] No more comments found, stopping`);
break;
}
}
} else if (isCommentNode(parentList)) {
// Parent is itself a comment node
current = parentList;
} else {
// Try to find previous sibling comment
let sibling = current.previousElementSibling;
while (sibling && !isCommentNode(sibling)) {
sibling = sibling.previousElementSibling;
}
if (sibling && !seen.has(sibling)) {
current = sibling;
console.log(`[${NAME}] Moving to sibling:`, current.tagName);
} else {
console.log(`[${NAME}] No more siblings, stopping`);
break;
}
}
}
console.log(`[${NAME}] Extracted ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
// Additional pass: Look for any <p> elements with comment markers that might have been missed
// This catches the case where the first comment in a thread is a <p> that's a sibling of the DL
if (commentDD && commentDD.tagName === 'DD') {
const topDL = commentDD.closest('dl');
if (topDL && topDL.previousElementSibling) {
let probe = topDL.previousElementSibling;
let probeHops = 0;
console.log(`[${NAME}] Scanning backwards from top DL for more context...`);
while (probe && probeHops++ < 5) {
if (probe.tagName === 'P' && !seen.has(probe)) {
const probeText = extractCommentText(probe);
if (probeText.length > 20 && (probe.querySelector('[data-mw-comment-start]') || probe.querySelector('[data-mw-comment-end]'))) {
console.log(`[${NAME}] Found additional preceding comment (${probeText.length} chars):`, probeText.substring(0, 100) + '...');
texts.unshift(probeText);
seen.add(probe);
}
}
probe = probe.previousElementSibling;
}
}
}
console.log(`[${NAME}] Final extraction: ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
if (texts.length > 0) {
const fullContext = texts.join('\n\n');
console.log(`[${NAME}] Full context:`, fullContext);
return fullContext;
}
console.warn(`[${NAME}] No context found, returning empty string`);
return '';
} catch (err) {
console.error(`[${NAME}] Error extracting context:`, err);
return '';
}
}
async function startForWidget(widgetEl) {
if (!TOKEN) { showTokenForm(widgetEl); return; }
if (widgetState.has(widgetEl) || widgetEl.getAttribute('data-convowizard-processing') === 'true') return;
widgetEl.setAttribute('data-convowizard-processing', 'true');
console.log(`[${NAME}] Starting widget processing:`, widgetEl.className);
function findInput(scope) {
// Prefer DT textbox role, then VE contenteditable surface, then textarea
return (
scope.querySelector('div[role="textbox"]') ||
scope.querySelector('[contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface [contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface') ||
scope.querySelector('textarea')
);
}
async function waitForInput(scope, timeoutMs = 8000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tick = () => {
const el = findInput(scope) || findInput(document);
if (el) return resolve(el);
if (Date.now() > deadline) return resolve(null);
setTimeout(tick, 100);
};
tick();
});
}
const inputEl = await waitForInput(widgetEl);
if (!inputEl) {
console.warn(`[${NAME}] No input element found in widget (after waiting)`);
return;
}
console.log(`[${NAME}] Input element found:`, inputEl.tagName, inputEl.getAttribute('role') || (inputEl.getAttribute('contenteditable') ? 'contenteditable' : ''));
const context = getContextStringFor(widgetEl);
console.log(`[${NAME}] Context extracted, length: ${context.length} chars`);
console.log(`[${NAME}] Context preview:`, context.substring(0, 200));
// Split context on (UTC) timestamps to create separate utterances like Reddit does
// This prevents backend caching bug where all Wikipedia convos share id=None
const utterances = context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = utterances.map((text, index) => ({
id: `wiki_${Date.now()}_${index}`, // Unique ID prevents cache collision
text: text.trim()
}));
console.log(`[${NAME}] Created ${existing.length} utterances with unique IDs`);
existing.forEach((utt, i) => {
console.log(`[${NAME}] ${i}: ID=${utt.id}, text=${utt.text.substring(0, 60)}...`);
});
const data = await postJson('start', {
existing, // Send as array like Reddit, not context string!
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
console.log(`[${NAME}] Received response from /start:`, data);
// Handle invalid token - clear and show auth form
if (data && data.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (!data || !data.interaction_id) return;
widgetState.set(widgetEl, { interactionId: data.interaction_id, inputEl, context });
handleIntervention(widgetEl, inputEl, data);
const sendContinue = throttle(async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
// Check if thread is muted
const threadId = getThreadIdForWidget(widgetEl);
if (threadId && isThreadMuted(threadId)) {
// Clear input background if muted
if (st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
return;
}
const widgetId = widgetEl.id;
const h = document.getElementById(`reply_${widgetId}_h`);
if (h) {
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>
`;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span with processing indicator
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary (Processing...)`;
}
}
// Recreate existing array from cached context for continue requests
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`, // Use interaction_id for consistency
text: text.trim()
}));
const data2 = await postJson('continue', {
existing: existingForContinue,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
// Handle invalid token on continue
if (data2 && data2.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
handleIntervention(widgetEl, st.inputEl, data2);
}, 3000);
inputEl.addEventListener('input', sendContinue);
inputEl.addEventListener('keyup', sendContinue);
inputEl.addEventListener('paste', sendContinue);
let postBtn = widgetEl.querySelector(".oo-ui-buttonElement-button[title*='Reply']");
if (!postBtn) {
postBtn = Array.from(widgetEl.querySelectorAll('.oo-ui-buttonElement-button'))
.find(b => /reply/i.test((b.title || b.innerText || '').trim()));
}
if (postBtn) {
postBtn.addEventListener('click', async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
try {
// Recreate existing array from cached context for submit
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForSubmit = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
await postJson('submit', {
existing: existingForSubmit,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
submitted_id: null,
token: TOKEN,
username: USERNAME
});
} finally {
widgetState.delete(widgetEl);
}
});
}
}
function handleIntervention(widgetEl, inputEl, data) {
if (!data) {
console.warn(`[${NAME}] handleIntervention called with no data`);
return;
}
console.log(`[${NAME}] handleIntervention called with data:`, data);
if (data.error) {
return;
}
const interactionId = data.interaction_id;
if (!interactionId) {
console.warn(`[${NAME}] No interaction_id in data`);
return;
}
const threadId = getThreadIdForWidget(widgetEl);
// Skip display if thread is muted
if (threadId && isThreadMuted(threadId)) {
const widgetId = widgetEl.id || `widget_${Date.now()}`;
if (!widgetEl.id) widgetEl.id = widgetId;
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
return;
}
const { replyId, contextId } = ensurePanels(widgetEl, threadId);
const showScores = Object.prototype.hasOwnProperty.call(data, 'show_scores');
console.log(`[${NAME}] Panel IDs - context: ${contextId}, reply: ${replyId}`);
console.log(`[${NAME}] Show scores: ${showScores}`);
const which = (data.which || '').substring(0, 5);
console.log(`[${NAME}] Response type (which): ${which}`);
if (which === 'craft') {
console.log(`[${NAME}] CRAFT mode - craft_ctx_score:`, data.craft_ctx_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_score:`, data.craft_reply_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_change:`, data.craft_reply_change);
if (data.craft_ctx_score != null) {
console.log(`[${NAME}] Updating context panel with score: ${data.craft_ctx_score}`);
updateContextPanel(contextId, data.craft_ctx_score, showScores);
} else {
console.log(`[${NAME}] No context score, updating context panel with null`);
updateContextPanel(contextId, null, showScores);
}
if (data.craft_reply_change != null && data.craft_reply_score != null) {
console.log(`[${NAME}] Updating reply panel with change: ${data.craft_reply_change}, score: ${data.craft_reply_score}`);
updateReplyPanel(replyId, data.craft_reply_change, data.craft_reply_score, showScores, inputEl);
} else {
console.log(`[${NAME}] No reply scores, updating reply panel with zeros`);
updateReplyPanel(replyId, 0, 0, showScores, inputEl);
}
} else {
const noteId = `passivenote${interactionId}_d`;
if (!data.message && !document.getElementById(noteId)) {
const box = document.createElement('div');
box.id = noteId;
box.className = 'passiveModeDisplay';
box.style.cssText = `
margin:8px 0;padding:8px 12px;border-radius:6px;border:1px solid #a2a9b1;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;opacity:.8;
`;
const content = document.createElement('div');
content.id = `passivenote${interactionId}_p`;
content.style.cssText = `font-size:12px;color:#54595d;margin:0;font-style:italic;`;
content.textContent = `${NAME} is currently not active on this thread.`;
box.appendChild(content);
widgetEl.prepend(box);
}
}
}
function installClickDelegation() {
document.addEventListener('click', (e) => {
console.log(`[${NAME}] Click detected on:`, e.target.tagName, e.target.className);
// Match reply buttons and links (both old and new DiscussionTools formats)
// Need to check the actual clicked element AND traverse up
const link = e.target.closest && (
e.target.closest('.ext-discussiontools-init-replybutton') ||
e.target.closest('.ext-discussiontools-init-replylink-reply') ||
e.target.closest('a.ext-discussiontools-init-replylink') ||
e.target.closest('.ext-discussiontools-init-replylink-buttons') ||
e.target.closest('[class*="ext-discussiontools-init-"][class*="reply"]')
);
if (!link) return;
console.log(`[${NAME}] Reply link clicked:`, link.className);
// For flat discussion structures (like Research talk pages), the widget may be inserted
// outside the immediate parent. Search more broadly.
const anchorScope = link.closest('dd, li, p, div, section') || document.body;
const broadScope = anchorScope.closest('#mw-content-text, .mw-parser-output, #content') || document.body;
console.log(`[${NAME}] Anchor scope:`, anchorScope.tagName, anchorScope.className);
console.log(`[${NAME}] Broad scope:`, broadScope.tagName, broadScope.className);
const timeoutAt = Date.now() + 8000;
let attempts = 0;
const poll = setInterval(() => {
attempts++;
// Try multiple search strategies:
// 1. In immediate anchor scope (for nested structures like dd/li)
// 2. As sibling to anchor scope (for flat structures like p)
// 3. In broader content scope
// 4. Document-wide fallback
const widget = anchorScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| anchorScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| (anchorScope.nextElementSibling && (
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-replyWidget') ||
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-newTopicWidget')
) ? anchorScope.nextElementSibling : null)
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-replyWidget:last-child'))
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-newTopicWidget:last-child'))
|| broadScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| broadScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| document.querySelector('.ext-discussiontools-ui-replyWidget')
|| document.querySelector('.ext-discussiontools-ui-newTopicWidget');
if (widget) {
console.log(`[${NAME}] Widget found after ${attempts} attempts:`, widget.className);
clearInterval(poll);
startForWidget(widget);
}
else if (Date.now() > timeoutAt) {
console.log(`[${NAME}] Widget timeout after ${attempts} attempts - no widget found`);
console.log(`[${NAME}] Searched in anchorScope:`, anchorScope);
console.log(`[${NAME}] Searched in broadScope:`, broadScope);
clearInterval(poll);
} else if (attempts % 10 === 0) {
console.log(`[${NAME}] Still searching for widget, attempt ${attempts}...`);
}
}, 100);
});
}
function installWidgetObserver() {
const root = document.getElementById('content') || document.body;
console.log(`[${NAME}] Setting up mutation observer on:`, root.id || root.tagName);
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof HTMLElement)) continue;
// Log all significant additions for debugging
if (n.className && typeof n.className === 'string' && n.className.includes('ext-discussiontools')) {
console.log(`[${NAME}] DOM addition detected:`, n.tagName, n.className);
}
// Match specific DiscussionTools widgets
if (n.matches('.ext-discussiontools-ui-replyWidget') || n.matches('.ext-discussiontools-ui-newTopicWidget')) {
console.log(`[${NAME}] *** Widget detected via observer ***:`, n.className);
startForWidget(n);
} else {
const nested = n.querySelector && (
n.querySelector('.ext-discussiontools-ui-replyWidget') ||
n.querySelector('.ext-discussiontools-ui-newTopicWidget')
);
if (nested) {
console.log(`[${NAME}] *** Nested widget detected ***:`, nested.className);
startForWidget(nested);
}
}
}
}
});
obs.observe(root, { childList: true, subtree: true });
console.log(`[${NAME}] Mutation observer is now watching for widget creation`);
// Check for existing widgets on page load
const existingWidgets = document.querySelectorAll('.ext-discussiontools-ui-replyWidget, .ext-discussiontools-ui-newTopicWidget');
if (existingWidgets.length > 0) {
console.log(`[${NAME}] Found ${existingWidgets.length} existing widget(s) on page load`);
existingWidgets.forEach(w => {
console.log(`[${NAME}] Processing existing widget:`, w.className);
startForWidget(w);
});
} else {
console.log(`[${NAME}] No existing widgets found on page load`);
}
}
// kick off
installClickDelegation();
installWidgetObserver();
console.log(`[${NAME}] === INITIALIZATION COMPLETE ===`);
console.log(`[${NAME}] Click delegation: ACTIVE`);
console.log(`[${NAME}] Mutation observer: ACTIVE`);
console.log(`[${NAME}] Waiting for user interaction...`);
})();
//</nowiki>
ovsz2p21jixld5h3jzz7wk6fn7z9zvd
740023
740022
2026-05-01T15:57:42Z
Laerdon
70887
740023
javascript
text/javascript
//<nowiki>
/**
* Filename: wiki-talk-page-script.js
* ConvoWizard – Real-time Reply Feedback (Wikipedia userscript)
* Runs on Talk pages inside DiscussionTools reply widgets.
* Prompts for a token on first run; persists after authorization.
*/
(async function () {
'use strict';
// Load core util module via ResourceLoader
await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api']);
// === CONFIG ===
const SERVER = 'https://convocompass.toolforge.org/api/';
// ---- UI strings and thresholds ----
const NAME = 'ConvoWizard';
const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search);
const OPTION_KEY = 'userjs-convowizard-token';
const SCORE_CHANGE_THRESH = 0.08;
const MID_TENSION_THRESH = 0.50;
const HIGH_TENSION_THRESH = 0.75;
const CONTEXT_LOW = `${NAME} will notify you here if it detects anything in the preceding conversation.`;
const CONTEXT_MID = `${NAME} thinks this discussion is getting somewhat tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_HIGH = `${NAME} thinks this discussion is getting tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_VERY_HIGH = `${NAME} thinks this discussion is getting very tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const REPLY_LOW = `${NAME} thinks this comment might decrease tension in this discussion.`;
const REPLY_MID = `${NAME} will notify you here if it detects anything in your comment draft.`;
const REPLY_HIGH = `${NAME} thinks this comment might increase the tension in this discussion. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const TENSION_COLORS = {
mid: { border: '#e8825c', bg: 'linear-gradient(135deg, #fff0eb 0%, #fff8f5 100%)', icon: '#e8825c', inputBg: 'rgba(232,130,92,0.05)' },
high: { border: '#d73027', bg: 'linear-gradient(135deg, #ffeaea 0%, #fff5f5 100%)', icon: '#d73027', inputBg: 'rgba(215,48,39,0.05)' },
veryHigh: { border: '#a50f15', bg: 'linear-gradient(135deg, #fdd 0%, #fee 100%)', icon: '#a50f15', inputBg: 'rgba(165,15,21,0.07)' },
};
// Keep ConvoWizard logs quiet by default to reduce console noise on Wikimedia pages.
const nativeConsole = window.console;
const console = {
log: (...args) => {
if (!DEBUG && typeof args[0] === 'string' && args[0].startsWith(`[${NAME}]`)) {
return;
}
nativeConsole.log(...args);
},
warn: (...args) => nativeConsole.warn(...args),
error: (...args) => nativeConsole.error(...args)
};
const isWikipediaHost = /(^|\.)wikipedia\.org$/.test(location.hostname);
const namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
const isTalkNamespace = Number.isFinite(namespaceNumber) && namespaceNumber % 2 === 1;
const isViewAction = mw.config.get('wgAction') === 'view';
// Gadget scope guard: only run on Wikipedia talk pages in normal view mode.
if (!isWikipediaHost || !isTalkNamespace || !isViewAction) {
console.log(
`[${NAME}] Skipping page (host=${location.hostname}, ns=${namespaceNumber}, action=${mw.config.get('wgAction')})`
);
return;
}
// ---------- helpers ----------
function throttle(fn, waitMs) {
let last = 0, timer = 0, pendingArgs = null;
return (...args) => {
const now = Date.now();
const remaining = waitMs - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
} else {
pendingArgs = args;
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn(...pendingArgs); }, remaining);
}
};
}
// Rewrite Wikipedia URL so the reddit-style backend can extract a post_id.
// Backend does: url.split("comments/")[1][:6]
// We inject "comments/<hash>" where hash is derived from the wiki page path.
function wikiUrlToPostId(href) {
try {
const pagePath = href.split('/wiki/')[1] || href;
// Simple hash to produce a stable 6-char hex id per page
let h = 0;
for (let i = 0; i < pagePath.length; i++) {
h = ((h << 5) - h) + pagePath.charCodeAt(i);
h = h & h;
}
const hex = Math.abs(h).toString(16).padStart(6, '0').slice(0, 6);
return href.split('/wiki/')[0] + '/comments/' + hex;
} catch {
return href;
}
}
async function postJson(route, body) {
try {
const res = await fetch(SERVER + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
console.log(`[${NAME}] response from ${route}: status=${res.status}`, text);
try {
const data = JSON.parse(text);
console.log(`[${NAME}] json from ${route}:`, data);
return data;
} catch (err) {
console.warn(`[${NAME}] non-json response from ${route}:`, text);
return {};
}
}
}
function getTokenFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(OPTION_KEY);
if (typeof raw === 'string') {
const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
}
}
} catch (err) {
console.warn(`[${NAME}] Failed to read token from user options`, err);
}
return null;
}
async function persistToken(storageKey, token) {
if (!token) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, token);
}
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(OPTION_KEY, token || '');
await new mw.Api().saveOption(OPTION_KEY, token || '');
console.log(`[${NAME}] ${token ? 'Saved' : 'Cleared'} token in user options (notepad)`);
} catch (err) {
console.warn(`[${NAME}] Could not persist token to user options`, err);
}
}
async function validateToken(token, username) {
try {
const response = await postJson('claim_token', { token, username });
return response.valid === true;
} catch {
return false;
}
}
// ========== CREDENTIAL SETUP ==========
const USERNAME = mw.config.get('wgUserName');
if (!USERNAME) {
console.log(`[${NAME}] Not logged in, skipping`);
return;
}
let TOKEN = null;
const STORAGE_KEY = `ConvoWizard:token:${USERNAME}`;
// Load persisted token (options → localStorage), validate, sync stores
async function loadPersistedToken() {
let token = getTokenFromOptions();
let source = 'options';
if (!token) {
token = localStorage.getItem(STORAGE_KEY);
source = 'localStorage';
}
if (token && typeof token === 'string') {
token = token.trim();
}
if (!token) return;
const valid = await validateToken(token, USERNAME);
if (valid) {
TOKEN = token;
// Sync both stores
if (source === 'options') {
localStorage.setItem(STORAGE_KEY, token);
} else {
await persistToken(STORAGE_KEY, token);
}
console.log(`[${NAME}] Token loaded from ${source}`);
} else {
console.log(`[${NAME}] Persisted token invalid, clearing`);
await persistToken(STORAGE_KEY, null);
}
}
await loadPersistedToken();
console.log(`[${NAME}] === SCRIPT LOADED SUCCESSFULLY ===`);
console.log(`[${NAME}] Server: ${SERVER}`);
console.log(`[${NAME}] User: ${USERNAME}`);
console.log(`[${NAME}] Token: ${TOKEN ? 'present' : 'none (will prompt)'}`);
console.log(`[${NAME}] Page URL: ${location.href}`);
console.log(`[${NAME}] Installing click and mutation observers...`);
// ========== TOKEN INPUT FORM ==========
function showTokenForm(widgetEl) {
// Don't add duplicate forms
if (widgetEl.querySelector('.convowizard-auth-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-auth-form';
form.style.cssText = `
margin:8px 0;padding:16px 20px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
const title = document.createElement('div');
title.style.cssText = 'font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;';
title.textContent = `${NAME}: Authorization Required`;
const desc = document.createElement('div');
desc.style.cssText = 'font-size:13px;color:#333;margin-bottom:12px;line-height:1.4;';
desc.textContent = 'Please paste your ConvoWizard token below to activate the extension.';
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;align-items:center;';
const input = document.createElement('input');
input.type = 'password';
input.placeholder = 'Paste token here';
input.autocomplete = 'off';
input.spellcheck = false;
input.style.cssText = `
flex:1;padding:8px 10px;font-size:13px;border:1px solid #a2a9b1;border-radius:4px;
font-family:monospace;outline:none;transition:border-color 0.2s ease;
`;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Authorize';
btn.style.cssText = `
padding:8px 16px;font-size:13px;font-weight:600;border:none;border-radius:4px;
background:#0645ad;color:#fff;cursor:pointer;transition:background 0.2s ease;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
btn.addEventListener('mouseenter', () => { btn.style.background = '#0b5cce'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#0645ad'; });
const errDiv = document.createElement('div');
errDiv.style.cssText = 'font-size:12px;color:#d73027;margin-top:8px;display:none;';
async function onAuthorize() {
const raw = input.value.trim();
// Input validation: 33 hex chars
if (!/^[0-9a-f]{33}$/i.test(raw)) {
errDiv.textContent = 'Invalid token format. A token is 33 hexadecimal characters.';
errDiv.style.display = 'block';
input.value = '';
input.focus();
return;
}
btn.disabled = true;
btn.textContent = 'Validating...';
errDiv.style.display = 'none';
const valid = await validateToken(raw, USERNAME);
// Clear input immediately after use
input.value = '';
if (valid) {
TOKEN = raw;
await persistToken(STORAGE_KEY, TOKEN);
// Remove all auth forms on page
document.querySelectorAll('.convowizard-auth-form').forEach(f => f.remove());
// Activate the widget
startForWidget(widgetEl);
} else {
errDiv.textContent = 'Token is invalid or expired. Please check and try again.';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Authorize';
input.focus();
}
}
btn.addEventListener('click', onAuthorize);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); onAuthorize(); }
});
row.appendChild(input);
row.appendChild(btn);
form.appendChild(title);
form.appendChild(desc);
form.appendChild(row);
form.appendChild(errDiv);
widgetEl.prepend(form);
input.focus();
}
// ---- state + logs ----
const widgetState = new WeakMap();
function cleanupDuplicatePanels() {
const allPanels = document.querySelectorAll('.craftDisplay, .passiveModeDisplay');
const seen = new Set();
allPanels.forEach(panel => {
const pid = panel.id;
if (pid && pid.includes('_')) {
const parts = pid.split('_');
const type = parts[0];
const wid = parts.slice(1).join('_').replace('_d', '');
const key = `${type}|${wid}`;
if (seen.has(key)) panel.remove(); else seen.add(key);
}
});
}
// ========== MUTE THREAD ==========
/**
* Generates a unique thread identifier by combining URL section, DOM position, and content hash.
* Ensures each thread on a page gets a distinct ID for per-thread mute functionality.
*/
function getThreadIdForWidget(widgetEl) {
try {
const urlHash = location.hash ? location.hash.replace('#', '') : 'root';
// Generate DOM path for widget position
const widgetPath = [];
let el = widgetEl;
for (let i = 0; i < 8 && el; i++) {
const parent = el.parentElement;
if (!parent || parent === document.body) break;
const siblings = Array.from(parent.children);
const index = siblings.indexOf(el);
widgetPath.unshift(`${parent.tagName}_${index}`);
el = parent;
}
const domPath = widgetPath.length > 0 ? widgetPath.join('_') : 'unknown';
// Get content-based hash from first comment
const context = getContextStringFor(widgetEl);
let contentHash = '';
if (context && context.length > 0) {
const firstComment = context.split('\n\n')[0] || context.substring(0, 150);
let hash = 0;
for (let i = 0; i < Math.min(firstComment.length, 150); i++) {
const char = firstComment.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
const parentText = widgetEl.parentElement ?
(widgetEl.parentElement.textContent || '').substring(0, 50).trim() : '';
if (parentText) {
let hash = 0;
for (let i = 0; i < parentText.length; i++) {
hash = ((hash << 5) - hash) + parentText.charCodeAt(i);
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
contentHash = Math.random().toString(36).slice(2, 8);
}
}
return `thread_${urlHash}_${domPath.substring(0, 50)}_${contentHash}`;
} catch (err) {
console.warn(`[${NAME}] Error generating thread ID:`, err);
return `thread_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}
}
function getMutedThreads() {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const stored = localStorage.getItem(storageKey);
if (!stored) return new Set();
const threads = JSON.parse(stored);
return new Set(Array.isArray(threads) ? threads : []);
} catch {
return new Set();
}
}
function saveMutedThreads(threadSet) {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const threads = Array.from(threadSet);
localStorage.setItem(storageKey, JSON.stringify(threads));
} catch (err) {
console.warn(`[${NAME}] Error saving muted threads:`, err);
}
}
function isThreadMuted(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
return muted.has(threadId);
}
function toggleThreadMute(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
const wasMuted = muted.has(threadId);
if (wasMuted) {
muted.delete(threadId);
} else {
muted.add(threadId);
}
saveMutedThreads(muted);
return !wasMuted;
}
/**
* Creates an unmute indicator button that appears when a thread is muted
* Allows users to restore ConvoWizard for a muted thread
*/
function ensureUnmuteIndicator(widgetEl, threadId, widgetId) {
const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator');
if (existingIndicator) return;
const indicator = document.createElement('div');
indicator.id = `convowizard-unmute-${widgetId}`;
indicator.className = 'convowizard-unmute-indicator';
indicator.style.cssText = `
margin: 4px 0;
padding: 6px 10px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: #f8f9fa;
color: #54595d;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s ease;
`;
const icon = document.createElement('span');
icon.textContent = '🔊';
icon.style.fontSize = '12px';
const text = document.createElement('span');
text.textContent = `${NAME} is muted for this thread. Click to unmute.`;
text.style.flex = '1';
indicator.appendChild(icon);
indicator.appendChild(text);
indicator.addEventListener('mouseenter', () => {
indicator.style.background = '#e9ecef';
indicator.style.borderColor = '#0645ad';
});
indicator.addEventListener('mouseleave', () => {
indicator.style.background = '#f8f9fa';
indicator.style.borderColor = '#a2a9b1';
});
indicator.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Show panels and remove indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
if (contextPanel) contextPanel.style.display = '';
if (replyPanel) replyPanel.style.display = '';
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
// Update mute buttons
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(btn => {
btn.textContent = '🔇 Mute thread';
btn.title = 'Mute ConvoWizard for this thread';
btn.style.background = '#ffffff';
});
// Re-trigger analysis to refresh display
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
// Re-run the continue request to refresh the display
const inputText = st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '';
if (inputText.trim().length > 0) {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('continue', {
existing: existingForContinue,
new: inputText,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
if (data && !data.error) {
handleIntervention(widgetEl, st.inputEl, data);
}
} else {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('start', {
existing,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
if (data && !data.error && data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
handleIntervention(widgetEl, st.inputEl, data);
}
}
} else {
startForWidget(widgetEl);
}
});
// Insert at the beginning of the widget
widgetEl.insertBefore(indicator, widgetEl.firstChild);
}
function createMuteButton(threadId, widgetId, isMuted) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-mute-btn';
btn.setAttribute('data-thread-id', threadId);
btn.setAttribute('data-widget-id', widgetId);
btn.style.cssText = `
margin-left: auto;
padding: 4px 8px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: ${isMuted ? '#f8f9fa' : '#ffffff'};
color: #54595d;
cursor: pointer;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
btn.textContent = isMuted
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = isMuted
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.addEventListener('mouseenter', () => {
btn.style.background = isMuted ? '#e9ecef' : '#f8f9fa';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = isMuted ? '#f8f9fa' : '#ffffff';
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Update button state
btn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
// Hide/show panels and unmute indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
const widgetEl = contextPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
contextPanel?.closest('.ext-discussiontools-ui-newTopicWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-newTopicWidget');
if (contextPanel) contextPanel.style.display = newMuteState ? 'none' : '';
if (replyPanel) replyPanel.style.display = newMuteState ? 'none' : '';
// Clear textbox background when muting
if (newMuteState) {
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
}
if (newMuteState && widgetEl) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (widgetEl) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Update all mute buttons for this thread
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(otherBtn => {
if (otherBtn !== btn) {
otherBtn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
otherBtn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
otherBtn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
}
});
});
return btn;
}
function ensurePanels(widgetEl, threadId = null) {
cleanupDuplicatePanels();
// Clean up duplicate unmute indicators
const unmuteIndicators = widgetEl.querySelectorAll('.convowizard-unmute-indicator');
if (unmuteIndicators.length > 1) {
for (let i = 1; i < unmuteIndicators.length; i++) {
unmuteIndicators[i].remove();
}
}
const widgetId = widgetEl.id || `widget_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
if (!widgetEl.id) widgetEl.id = widgetId;
const contextId = `context_${widgetId}`;
const replyId = `reply_${widgetId}`;
const isMuted = threadId ? isThreadMuted(threadId) : false;
// Ensure Context panel
if (!document.getElementById(`${contextId}_d`)) {
const contextBox = document.createElement('div');
contextBox.id = `${contextId}_d`;
contextBox.className = 'craftDisplay';
contextBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const cheader = document.createElement('div');
cheader.id = `${contextId}_h`;
cheader.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Context Summary`;
cheader.appendChild(iconSvg);
cheader.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
cheader.appendChild(muteBtn);
}
const ccontent = document.createElement('div');
ccontent.id = `${contextId}_p`;
ccontent.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
ccontent.textContent = `When you type a reply ${NAME} will give you some feedback on the discussion context.`;
contextBox.appendChild(cheader);
contextBox.appendChild(ccontent);
if (isMuted) contextBox.style.display = 'none';
widgetEl.prepend(contextBox);
} else if (threadId) {
const existingPanel = document.getElementById(`${contextId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
// Show unmute indicator when muted (only if panels don't exist yet)
if (threadId && isMuted && !document.getElementById(`${contextId}_d`) && !document.getElementById(`${replyId}_d`)) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (threadId && !isMuted) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Ensure Reply panel
if (!document.getElementById(`${replyId}_d`)) {
const replyBox = document.createElement('div');
replyBox.id = `${replyId}_d`;
replyBox.className = 'craftDisplay';
replyBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const header = document.createElement('div');
header.id = `${replyId}_h`;
header.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Reply Summary`;
header.appendChild(iconSvg);
header.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
header.appendChild(muteBtn);
}
const content = document.createElement('div');
content.id = `${replyId}_p`;
content.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
content.textContent = `When you type a reply ${NAME} will give you some feedback on your comment.`;
replyBox.appendChild(header);
replyBox.appendChild(content);
if (isMuted) replyBox.style.display = 'none';
const preview = widgetEl.querySelector('.ext-discussiontools-ui-replyWidget-preview')
|| document.querySelector('.ext-discussiontools-ui-replyWidget-preview');
if (preview && preview.parentElement) {
preview.parentElement.insertBefore(replyBox, preview.nextSibling);
} else {
widgetEl.append(replyBox);
}
} else if (threadId) {
const existingPanel = document.getElementById(`${replyId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
return { replyId, contextId };
}
function updateContextPanel(contextId, score, showScores) {
console.log(`[${NAME}] updateContextPanel called - contextId: ${contextId}, score: ${score}, showScores: ${showScores}`);
const hasScore = typeof score === 'number';
let text = showScores && hasScore ? `Context craft score: ${score.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
console.log(`[${NAME}] hasScore: ${hasScore}, score value: ${score}`);
console.log(`[${NAME}] Thresholds - MID: ${MID_TENSION_THRESH}, HIGH: ${HIGH_TENSION_THRESH}`);
if (hasScore && score > MID_TENSION_THRESH) {
let level;
if (score > HIGH_TENSION_THRESH) {
console.log(`[${NAME}] Very high tension (score > ${HIGH_TENSION_THRESH})`);
text += `${CONTEXT_VERY_HIGH}`;
level = 'veryHigh';
} else if (score > 0.60) {
console.log(`[${NAME}] High tension (score > 0.60)`);
text += `${CONTEXT_HIGH}`;
level = 'high';
} else {
console.log(`[${NAME}] Mid tension (score > ${MID_TENSION_THRESH})`);
text += `${CONTEXT_MID}`;
level = 'mid';
}
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else {
console.log(`[${NAME}] Low tension or no score`);
text += `${CONTEXT_LOW}`;
}
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
console.log(`[${NAME}] DOM elements - box: ${!!box}, p: ${!!p}, h: ${!!h}`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) {
p.textContent = text;
console.log(`[${NAME}] Updated text content (length: ${text.length})`);
}
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Context Summary`;
}
console.log(`[${NAME}] Updated header color: ${iconColor}`);
}
}
function updateReplyPanel(replyId, scoreChange, rawScore, showScores, inputEl) {
let text = showScores ? `Change in craft score after your comment: ${scoreChange.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
text += `${REPLY_HIGH}`;
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else if (rawScore - scoreChange > 0.55 && scoreChange < -SCORE_CHANGE_THRESH) {
text += `${REPLY_LOW}`;
borderColor = '#4caf50';
bgGradient = 'linear-gradient(135deg, #f1f8e9 0%, #f9fbe7 100%)';
iconColor = '#4caf50';
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 6L6.5 11 4 8.5l1-1 1.5 1.5L10.5 5l1 1z"/>
</svg>`;
} else {
text += `${REPLY_MID}`;
}
if (inputEl) {
let threadId = null;
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) {
const muteBtn = replyBox.querySelector('.convowizard-mute-btn');
if (muteBtn) {
threadId = muteBtn.getAttribute('data-thread-id');
}
}
// If no thread ID from panel, try to get from widget
if (!threadId) {
const widgetEl = inputEl.closest('.ext-discussiontools-ui-replyWidget') ||
inputEl.closest('.ext-discussiontools-ui-newTopicWidget');
if (widgetEl) {
threadId = getThreadIdForWidget(widgetEl);
}
}
// If muted, clear background and return
if (threadId && isThreadMuted(threadId)) {
inputEl.style.setProperty('background-color', 'transparent', 'important');
return;
}
let inputBg = 'transparent';
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
inputBg = TENSION_COLORS[level].inputBg;
} else if (scoreChange < -SCORE_CHANGE_THRESH) {
inputBg = 'rgba(76,175,80,0.05)';
}
inputEl.style.setProperty('background-color', inputBg, 'important');
inputEl.style.setProperty('transition', 'background-color 0.3s ease', 'important');
}
const box = document.getElementById(`${replyId}_d`);
const p = document.getElementById(`${replyId}_p`);
const h = document.getElementById(`${replyId}_h`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) p.textContent = text;
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary`;
}
}
}
function getContextStringFor(widgetEl) {
try {
console.log(`[${NAME}] === Extracting context for widget ===`);
// Helper to check if element is a comment container
const isCommentNode = (el) => !!el && (el.tagName === 'DD' || el.tagName === 'LI' || el.tagName === 'P');
// Extract clean comment text, removing nested replies and UI elements
// IMPORTANT: Keep timestamps (UTC) because backend splits on them!
const extractCommentText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
// Remove nested discussion lists, widgets, and UI elements
// NOTE: DO NOT remove .ext-discussiontools-init-timestamplink - backend needs timestamps to split utterances!
clone.querySelectorAll('dl, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, .ext-discussiontools-init-replylink-buttons, [role="button"]').forEach(n => n.remove());
const text = (clone.innerText || '').trim();
return text;
};
// Wikipedia Talk pages have structure: <dd>comment text<dl><dd>reply<dl><dd>widget
// When widget appears, it's nested inside a new <dd> inside a new <dl>
// We need to find the parent <dd> that contains actual comment text
let commentDD = null;
// Strategy 1: Widget is inside <dd> → <dl> → <dd> (the target comment)
// Find the <dd> containing the widget
const widgetDD = widgetEl.closest('dd, li');
console.log(`[${NAME}] Widget container DD:`, widgetDD ? widgetDD.tagName : 'not found');
if (widgetDD) {
// Check if this DD has meaningful text (not just the widget)
const widgetDDText = extractCommentText(widgetDD);
console.log(`[${NAME}] Widget DD text length:`, widgetDDText.length);
if (widgetDDText.length > 20) {
// This DD has actual comment text
commentDD = widgetDD;
} else {
// This DD is empty/minimal - the widget was just inserted
// Go up: DD → DL → parent DD (which should have the comment)
const parentDL = widgetDD.parentElement;
console.log(`[${NAME}] Parent DL:`, parentDL ? parentDL.tagName : 'not found');
if (parentDL && (parentDL.tagName === 'DL' || parentDL.tagName === 'UL' || parentDL.tagName === 'OL')) {
// Find the parent DD/LI/P that contains this DL
commentDD = parentDL.closest('dd, li, p');
console.log(`[${NAME}] Found parent comment node:`, commentDD ? commentDD.tagName : 'not found');
}
}
}
// Strategy 2: Look for preceding paragraph with comment markers (flat structure)
if (!commentDD) {
console.log(`[${NAME}] Trying flat structure search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 10) {
probe = probe.previousElementSibling || (probe.parentElement ? probe.parentElement.previousElementSibling : null);
if (probe && probe.tagName === 'P' && probe.querySelector('[data-mw-comment-start], [data-mw-comment-end]')) {
commentDD = probe;
console.log(`[${NAME}] Found comment via flat search at hop ${hops}`);
break;
}
}
}
// Strategy 3: Last resort - traverse DOM looking for any comment node
if (!commentDD) {
console.log(`[${NAME}] Trying fallback search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 20) {
if (isCommentNode(probe) && extractCommentText(probe).length > 20) {
commentDD = probe;
console.log(`[${NAME}] Found comment via fallback at hop ${hops}`);
break;
}
probe = probe.previousElementSibling ||
(probe.parentElement ? probe.parentElement.previousElementSibling : null) ||
(probe.parentElement ? probe.parentElement.parentElement : null);
}
}
if (!commentDD) {
console.warn(`[${NAME}] Could not find any comment node!`);
return '';
}
// Now traverse up the comment chain collecting all parent comments
const texts = [];
const seen = new Set();
let current = commentDD;
let iterations = 0;
console.log(`[${NAME}] Starting comment chain traversal from:`, current.tagName);
while (current && iterations++ < 20) {
if (seen.has(current)) {
console.log(`[${NAME}] Already seen this node, breaking`);
break;
}
seen.add(current);
const text = extractCommentText(current);
if (text && text.length > 0) {
console.log(`[${NAME}] Extracted comment (${text.length} chars):`, text.substring(0, 100) + '...');
texts.unshift(text); // Add to beginning to maintain chronological order
}
// Navigate up the discussion tree
// Structure: <p>text</p><dl><dd>reply<dl><dd>reply</dd></dl></dd></dl>
// From current <dd>, go to parent element (should be <dl>)
const parentList = current.parentElement;
if (!parentList) {
console.log(`[${NAME}] No parent element, stopping`);
break;
}
console.log(`[${NAME}] Parent list:`, parentList.tagName);
// From <dl>, find the parent <dd> or <li> or <p>
if (parentList.tagName === 'DL' || parentList.tagName === 'UL' || parentList.tagName === 'OL') {
// First try to find an ancestor comment node
current = parentList.closest('dd, li, p');
if (current) {
console.log(`[${NAME}] Found ancestor comment:`, current.tagName);
} else {
// No ancestor found - the <p> might be a SIBLING of this <dl>
// This happens in flat structure: <p>comment</p><dl><dd>reply...
console.log(`[${NAME}] No ancestor, searching for previous sibling of DL...`);
let sibling = parentList.previousElementSibling;
let siblingHops = 0;
while (sibling && siblingHops++ < 10) {
console.log(`[${NAME}] Checking sibling:`, sibling.tagName);
if (isCommentNode(sibling) && !seen.has(sibling)) {
const siblingText = extractCommentText(sibling);
if (siblingText.length > 20) {
current = sibling;
console.log(`[${NAME}] Found sibling comment with text (${siblingText.length} chars)`);
break;
}
}
sibling = sibling.previousElementSibling;
}
if (!current || current === parentList.closest('dd, li, p')) {
console.log(`[${NAME}] No more comments found, stopping`);
break;
}
}
} else if (isCommentNode(parentList)) {
// Parent is itself a comment node
current = parentList;
} else {
// Try to find previous sibling comment
let sibling = current.previousElementSibling;
while (sibling && !isCommentNode(sibling)) {
sibling = sibling.previousElementSibling;
}
if (sibling && !seen.has(sibling)) {
current = sibling;
console.log(`[${NAME}] Moving to sibling:`, current.tagName);
} else {
console.log(`[${NAME}] No more siblings, stopping`);
break;
}
}
}
console.log(`[${NAME}] Extracted ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
// Additional pass: Look for any <p> elements with comment markers that might have been missed
// This catches the case where the first comment in a thread is a <p> that's a sibling of the DL
if (commentDD && commentDD.tagName === 'DD') {
const topDL = commentDD.closest('dl');
if (topDL && topDL.previousElementSibling) {
let probe = topDL.previousElementSibling;
let probeHops = 0;
console.log(`[${NAME}] Scanning backwards from top DL for more context...`);
while (probe && probeHops++ < 5) {
if (probe.tagName === 'P' && !seen.has(probe)) {
const probeText = extractCommentText(probe);
if (probeText.length > 20 && (probe.querySelector('[data-mw-comment-start]') || probe.querySelector('[data-mw-comment-end]'))) {
console.log(`[${NAME}] Found additional preceding comment (${probeText.length} chars):`, probeText.substring(0, 100) + '...');
texts.unshift(probeText);
seen.add(probe);
}
}
probe = probe.previousElementSibling;
}
}
}
console.log(`[${NAME}] Final extraction: ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
if (texts.length > 0) {
const fullContext = texts.join('\n\n');
console.log(`[${NAME}] Full context:`, fullContext);
return fullContext;
}
console.warn(`[${NAME}] No context found, returning empty string`);
return '';
} catch (err) {
console.error(`[${NAME}] Error extracting context:`, err);
return '';
}
}
async function startForWidget(widgetEl) {
if (!TOKEN) { showTokenForm(widgetEl); return; }
if (widgetState.has(widgetEl) || widgetEl.getAttribute('data-convowizard-processing') === 'true') return;
widgetEl.setAttribute('data-convowizard-processing', 'true');
console.log(`[${NAME}] Starting widget processing:`, widgetEl.className);
function findInput(scope) {
// Prefer DT textbox role, then VE contenteditable surface, then textarea
return (
scope.querySelector('div[role="textbox"]') ||
scope.querySelector('[contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface [contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface') ||
scope.querySelector('textarea')
);
}
async function waitForInput(scope, timeoutMs = 8000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tick = () => {
const el = findInput(scope) || findInput(document);
if (el) return resolve(el);
if (Date.now() > deadline) return resolve(null);
setTimeout(tick, 100);
};
tick();
});
}
const inputEl = await waitForInput(widgetEl);
if (!inputEl) {
console.warn(`[${NAME}] No input element found in widget (after waiting)`);
return;
}
console.log(`[${NAME}] Input element found:`, inputEl.tagName, inputEl.getAttribute('role') || (inputEl.getAttribute('contenteditable') ? 'contenteditable' : ''));
const context = getContextStringFor(widgetEl);
console.log(`[${NAME}] Context extracted, length: ${context.length} chars`);
console.log(`[${NAME}] Context preview:`, context.substring(0, 200));
// Split context on (UTC) timestamps to create separate utterances like Reddit does
// This prevents backend caching bug where all Wikipedia convos share id=None
const utterances = context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = utterances.map((text, index) => ({
id: `wiki_${Date.now()}_${index}`, // Unique ID prevents cache collision
text: text.trim()
}));
console.log(`[${NAME}] Created ${existing.length} utterances with unique IDs`);
existing.forEach((utt, i) => {
console.log(`[${NAME}] ${i}: ID=${utt.id}, text=${utt.text.substring(0, 60)}...`);
});
const data = await postJson('start', {
existing, // Send as array like Reddit, not context string!
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
console.log(`[${NAME}] Received response from /start:`, data);
// Handle invalid token - clear and show auth form
if (data && data.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (!data || !data.interaction_id) return;
widgetState.set(widgetEl, { interactionId: data.interaction_id, inputEl, context });
handleIntervention(widgetEl, inputEl, data);
const sendContinue = throttle(async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
// Check if thread is muted
const threadId = getThreadIdForWidget(widgetEl);
if (threadId && isThreadMuted(threadId)) {
// Clear input background if muted
if (st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
return;
}
const widgetId = widgetEl.id;
const h = document.getElementById(`reply_${widgetId}_h`);
if (h) {
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>
`;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span with processing indicator
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary (Processing...)`;
}
}
// Recreate existing array from cached context for continue requests
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`, // Use interaction_id for consistency
text: text.trim()
}));
const data2 = await postJson('continue', {
existing: existingForContinue,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
// Handle invalid token on continue
if (data2 && data2.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
handleIntervention(widgetEl, st.inputEl, data2);
}, 3000);
inputEl.addEventListener('input', sendContinue);
inputEl.addEventListener('keyup', sendContinue);
inputEl.addEventListener('paste', sendContinue);
let postBtn = widgetEl.querySelector(".oo-ui-buttonElement-button[title*='Reply']");
if (!postBtn) {
postBtn = Array.from(widgetEl.querySelectorAll('.oo-ui-buttonElement-button'))
.find(b => /reply/i.test((b.title || b.innerText || '').trim()));
}
if (postBtn) {
postBtn.addEventListener('click', async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
try {
// Recreate existing array from cached context for submit
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForSubmit = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
await postJson('submit', {
existing: existingForSubmit,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
submitted_id: null,
token: TOKEN,
username: USERNAME
});
} finally {
widgetState.delete(widgetEl);
}
});
}
}
function handleIntervention(widgetEl, inputEl, data) {
if (!data) {
console.warn(`[${NAME}] handleIntervention called with no data`);
return;
}
console.log(`[${NAME}] handleIntervention called with data:`, data);
if (data.error) {
return;
}
const interactionId = data.interaction_id;
if (!interactionId) {
console.warn(`[${NAME}] No interaction_id in data`);
return;
}
const threadId = getThreadIdForWidget(widgetEl);
// Skip display if thread is muted
if (threadId && isThreadMuted(threadId)) {
const widgetId = widgetEl.id || `widget_${Date.now()}`;
if (!widgetEl.id) widgetEl.id = widgetId;
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
return;
}
const { replyId, contextId } = ensurePanels(widgetEl, threadId);
const showScores = Object.prototype.hasOwnProperty.call(data, 'show_scores');
console.log(`[${NAME}] Panel IDs - context: ${contextId}, reply: ${replyId}`);
console.log(`[${NAME}] Show scores: ${showScores}`);
const which = (data.which || '').substring(0, 5);
console.log(`[${NAME}] Response type (which): ${which}`);
if (which === 'craft') {
console.log(`[${NAME}] CRAFT mode - craft_ctx_score:`, data.craft_ctx_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_score:`, data.craft_reply_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_change:`, data.craft_reply_change);
if (data.craft_ctx_score != null) {
console.log(`[${NAME}] Updating context panel with score: ${data.craft_ctx_score}`);
updateContextPanel(contextId, data.craft_ctx_score, showScores);
} else {
console.log(`[${NAME}] No context score, updating context panel with null`);
updateContextPanel(contextId, null, showScores);
}
if (data.craft_reply_change != null && data.craft_reply_score != null) {
console.log(`[${NAME}] Updating reply panel with change: ${data.craft_reply_change}, score: ${data.craft_reply_score}`);
updateReplyPanel(replyId, data.craft_reply_change, data.craft_reply_score, showScores, inputEl);
} else {
console.log(`[${NAME}] No reply scores, updating reply panel with zeros`);
updateReplyPanel(replyId, 0, 0, showScores, inputEl);
}
} else {
const noteId = `passivenote${interactionId}_d`;
if (!data.message && !document.getElementById(noteId)) {
const box = document.createElement('div');
box.id = noteId;
box.className = 'passiveModeDisplay';
box.style.cssText = `
margin:8px 0;padding:8px 12px;border-radius:6px;border:1px solid #a2a9b1;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;opacity:.8;
`;
const content = document.createElement('div');
content.id = `passivenote${interactionId}_p`;
content.style.cssText = `font-size:12px;color:#54595d;margin:0;font-style:italic;`;
content.textContent = `${NAME} is currently not active on this thread.`;
box.appendChild(content);
widgetEl.prepend(box);
}
}
}
function installClickDelegation() {
document.addEventListener('click', (e) => {
console.log(`[${NAME}] Click detected on:`, e.target.tagName, e.target.className);
// Match reply buttons and links (both old and new DiscussionTools formats)
// Need to check the actual clicked element AND traverse up
const link = e.target.closest && (
e.target.closest('.ext-discussiontools-init-replybutton') ||
e.target.closest('.ext-discussiontools-init-replylink-reply') ||
e.target.closest('a.ext-discussiontools-init-replylink') ||
e.target.closest('.ext-discussiontools-init-replylink-buttons') ||
e.target.closest('[class*="ext-discussiontools-init-"][class*="reply"]')
);
if (!link) return;
console.log(`[${NAME}] Reply link clicked:`, link.className);
// For flat discussion structures (like Research talk pages), the widget may be inserted
// outside the immediate parent. Search more broadly.
const anchorScope = link.closest('dd, li, p, div, section') || document.body;
const broadScope = anchorScope.closest('#mw-content-text, .mw-parser-output, #content') || document.body;
console.log(`[${NAME}] Anchor scope:`, anchorScope.tagName, anchorScope.className);
console.log(`[${NAME}] Broad scope:`, broadScope.tagName, broadScope.className);
const timeoutAt = Date.now() + 8000;
let attempts = 0;
const poll = setInterval(() => {
attempts++;
// Try multiple search strategies:
// 1. In immediate anchor scope (for nested structures like dd/li)
// 2. As sibling to anchor scope (for flat structures like p)
// 3. In broader content scope
// 4. Document-wide fallback
const widget = anchorScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| anchorScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| (anchorScope.nextElementSibling && (
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-replyWidget') ||
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-newTopicWidget')
) ? anchorScope.nextElementSibling : null)
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-replyWidget:last-child'))
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-newTopicWidget:last-child'))
|| broadScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| broadScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| document.querySelector('.ext-discussiontools-ui-replyWidget')
|| document.querySelector('.ext-discussiontools-ui-newTopicWidget');
if (widget) {
console.log(`[${NAME}] Widget found after ${attempts} attempts:`, widget.className);
clearInterval(poll);
startForWidget(widget);
}
else if (Date.now() > timeoutAt) {
console.log(`[${NAME}] Widget timeout after ${attempts} attempts - no widget found`);
console.log(`[${NAME}] Searched in anchorScope:`, anchorScope);
console.log(`[${NAME}] Searched in broadScope:`, broadScope);
clearInterval(poll);
} else if (attempts % 10 === 0) {
console.log(`[${NAME}] Still searching for widget, attempt ${attempts}...`);
}
}, 100);
});
}
function installWidgetObserver() {
const root = document.getElementById('content') || document.body;
console.log(`[${NAME}] Setting up mutation observer on:`, root.id || root.tagName);
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof HTMLElement)) continue;
// Log all significant additions for debugging
if (n.className && typeof n.className === 'string' && n.className.includes('ext-discussiontools')) {
console.log(`[${NAME}] DOM addition detected:`, n.tagName, n.className);
}
// Match specific DiscussionTools widgets
if (n.matches('.ext-discussiontools-ui-replyWidget') || n.matches('.ext-discussiontools-ui-newTopicWidget')) {
console.log(`[${NAME}] *** Widget detected via observer ***:`, n.className);
startForWidget(n);
} else {
const nested = n.querySelector && (
n.querySelector('.ext-discussiontools-ui-replyWidget') ||
n.querySelector('.ext-discussiontools-ui-newTopicWidget')
);
if (nested) {
console.log(`[${NAME}] *** Nested widget detected ***:`, nested.className);
startForWidget(nested);
}
}
}
}
});
obs.observe(root, { childList: true, subtree: true });
console.log(`[${NAME}] Mutation observer is now watching for widget creation`);
// Check for existing widgets on page load
const existingWidgets = document.querySelectorAll('.ext-discussiontools-ui-replyWidget, .ext-discussiontools-ui-newTopicWidget');
if (existingWidgets.length > 0) {
console.log(`[${NAME}] Found ${existingWidgets.length} existing widget(s) on page load`);
existingWidgets.forEach(w => {
console.log(`[${NAME}] Processing existing widget:`, w.className);
startForWidget(w);
});
} else {
console.log(`[${NAME}] No existing widgets found on page load`);
}
}
// kick off
installClickDelegation();
installWidgetObserver();
console.log(`[${NAME}] === INITIALIZATION COMPLETE ===`);
console.log(`[${NAME}] Click delegation: ACTIVE`);
console.log(`[${NAME}] Mutation observer: ACTIVE`);
console.log(`[${NAME}] Waiting for user interaction...`);
})();
//</nowiki>
s03a6wee73z3gj9jhq65fizq9y11u3i
740024
740023
2026-05-01T15:58:59Z
Laerdon
70887
740024
javascript
text/javascript
//<nowiki>
/**
* Filename: wiki-talk-page-script.js
* ConvoWizard – Real-time Reply Feedback (Wikipedia userscript)
* Runs on Talk pages inside DiscussionTools reply widgets.
* Prompts for a token on first run; persists after authorization.
*/
(async function () {
'use strict';
// Load core util module via ResourceLoader
await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api']);
// === CONFIG ===
const SERVER = 'https://convocompass.toolforge.org/api/';
// ---- UI strings and thresholds ----
const NAME = 'ConvoWizard';
const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search);
const OPTION_KEY = 'userjs-convowizard-token';
const SCORE_CHANGE_THRESH = 0.08;
const MID_TENSION_THRESH = 0.50;
const HIGH_TENSION_THRESH = 0.75;
const CONTEXT_LOW = `${NAME} will notify you here if it detects anything in the preceding conversation.`;
const CONTEXT_MID = `${NAME} thinks this discussion is getting somewhat tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_HIGH = `${NAME} thinks this discussion is getting tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_VERY_HIGH = `${NAME} thinks this discussion is getting very tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const REPLY_LOW = `${NAME} thinks this comment might decrease tension in this discussion.`;
const REPLY_MID = `${NAME} will notify you here if it detects anything in your comment draft.`;
const REPLY_HIGH = `${NAME} thinks this comment might increase the tension in this discussion. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const TENSION_COLORS = {
mid: { border: '#e8825c', bg: 'linear-gradient(135deg, #fff0eb 0%, #fff8f5 100%)', icon: '#e8825c', inputBg: 'rgba(232,130,92,0.05)' },
high: { border: '#d73027', bg: 'linear-gradient(135deg, #ffeaea 0%, #fff5f5 100%)', icon: '#d73027', inputBg: 'rgba(215,48,39,0.05)' },
veryHigh: { border: '#a50f15', bg: 'linear-gradient(135deg, #fdd 0%, #fee 100%)', icon: '#a50f15', inputBg: 'rgba(165,15,21,0.07)' },
};
// Keep ConvoWizard logs quiet by default to reduce console noise on Wikimedia pages.
const nativeConsole = window.console;
const console = {
log: (...args) => {
if (!DEBUG && typeof args[0] === 'string' && args[0].startsWith(`[${NAME}]`)) {
return;
}
nativeConsole.log(...args);
},
warn: (...args) => nativeConsole.warn(...args),
error: (...args) => nativeConsole.error(...args)
};
const isWikipediaHost = /(^|\.)wikipedia\.org$/.test(location.hostname);
const namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
const isTalkNamespace = Number.isFinite(namespaceNumber) && namespaceNumber % 2 === 1;
const isViewAction = mw.config.get('wgAction') === 'view';
// Gadget scope guard: only run on Wikipedia talk pages in normal view mode.
if (!isWikipediaHost || !isTalkNamespace || !isViewAction) {
console.log(
`[${NAME}] Skipping page (host=${location.hostname}, ns=${namespaceNumber}, action=${mw.config.get('wgAction')})`
);
return;
}
// ---------- helpers ----------
function throttle(fn, waitMs) {
let last = 0, timer = 0, pendingArgs = null;
return (...args) => {
const now = Date.now();
const remaining = waitMs - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
} else {
pendingArgs = args;
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn(...pendingArgs); }, remaining);
}
};
}
// Rewrite Wikipedia URL so the reddit-style backend can extract a post_id.
// Backend does: url.split("comments/")[1][:6]
// We inject "comments/<hash>" where hash is derived from the wiki page path.
function wikiUrlToPostId(href) {
try {
const pagePath = href.split('/wiki/')[1] || href;
// Simple hash to produce a stable 6-char hex id per page
let h = 0;
for (let i = 0; i < pagePath.length; i++) {
h = ((h << 5) - h) + pagePath.charCodeAt(i);
h = h & h;
}
const hex = Math.abs(h).toString(16).padStart(6, '0').slice(0, 6);
return href.split('/wiki/')[0] + '/comments/' + hex;
} catch {
return href;
}
}
async function postJson(route, body) {
try {
const res = await fetch(SERVER + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
nativeConsole.log(`[${NAME}] response from ${route}: status=${res.status}`, text);
try {
const data = JSON.parse(text);
nativeConsole.log(`[${NAME}] json from ${route}:`, data);
return data;
} catch (err) {
console.warn(`[${NAME}] non-json response from ${route}:`, text);
return {};
}
} catch (err) {
console.warn(`[${NAME}] request failed for ${route}:`, err);
return {};
}
}
function getTokenFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(OPTION_KEY);
if (typeof raw === 'string') {
const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
}
}
} catch (err) {
console.warn(`[${NAME}] Failed to read token from user options`, err);
}
return null;
}
async function persistToken(storageKey, token) {
if (!token) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, token);
}
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(OPTION_KEY, token || '');
await new mw.Api().saveOption(OPTION_KEY, token || '');
console.log(`[${NAME}] ${token ? 'Saved' : 'Cleared'} token in user options (notepad)`);
} catch (err) {
console.warn(`[${NAME}] Could not persist token to user options`, err);
}
}
async function validateToken(token, username) {
try {
const response = await postJson('claim_token', { token, username });
return response.valid === true;
} catch {
return false;
}
}
// ========== CREDENTIAL SETUP ==========
const USERNAME = mw.config.get('wgUserName');
if (!USERNAME) {
console.log(`[${NAME}] Not logged in, skipping`);
return;
}
let TOKEN = null;
const STORAGE_KEY = `ConvoWizard:token:${USERNAME}`;
// Load persisted token (options → localStorage), validate, sync stores
async function loadPersistedToken() {
let token = getTokenFromOptions();
let source = 'options';
if (!token) {
token = localStorage.getItem(STORAGE_KEY);
source = 'localStorage';
}
if (token && typeof token === 'string') {
token = token.trim();
}
if (!token) return;
const valid = await validateToken(token, USERNAME);
if (valid) {
TOKEN = token;
// Sync both stores
if (source === 'options') {
localStorage.setItem(STORAGE_KEY, token);
} else {
await persistToken(STORAGE_KEY, token);
}
console.log(`[${NAME}] Token loaded from ${source}`);
} else {
console.log(`[${NAME}] Persisted token invalid, clearing`);
await persistToken(STORAGE_KEY, null);
}
}
await loadPersistedToken();
console.log(`[${NAME}] === SCRIPT LOADED SUCCESSFULLY ===`);
console.log(`[${NAME}] Server: ${SERVER}`);
console.log(`[${NAME}] User: ${USERNAME}`);
console.log(`[${NAME}] Token: ${TOKEN ? 'present' : 'none (will prompt)'}`);
console.log(`[${NAME}] Page URL: ${location.href}`);
console.log(`[${NAME}] Installing click and mutation observers...`);
// ========== TOKEN INPUT FORM ==========
function showTokenForm(widgetEl) {
// Don't add duplicate forms
if (widgetEl.querySelector('.convowizard-auth-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-auth-form';
form.style.cssText = `
margin:8px 0;padding:16px 20px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
const title = document.createElement('div');
title.style.cssText = 'font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;';
title.textContent = `${NAME}: Authorization Required`;
const desc = document.createElement('div');
desc.style.cssText = 'font-size:13px;color:#333;margin-bottom:12px;line-height:1.4;';
desc.textContent = 'Please paste your ConvoWizard token below to activate the extension.';
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;align-items:center;';
const input = document.createElement('input');
input.type = 'password';
input.placeholder = 'Paste token here';
input.autocomplete = 'off';
input.spellcheck = false;
input.style.cssText = `
flex:1;padding:8px 10px;font-size:13px;border:1px solid #a2a9b1;border-radius:4px;
font-family:monospace;outline:none;transition:border-color 0.2s ease;
`;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Authorize';
btn.style.cssText = `
padding:8px 16px;font-size:13px;font-weight:600;border:none;border-radius:4px;
background:#0645ad;color:#fff;cursor:pointer;transition:background 0.2s ease;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
btn.addEventListener('mouseenter', () => { btn.style.background = '#0b5cce'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#0645ad'; });
const errDiv = document.createElement('div');
errDiv.style.cssText = 'font-size:12px;color:#d73027;margin-top:8px;display:none;';
async function onAuthorize() {
const raw = input.value.trim();
// Input validation: 33 hex chars
if (!/^[0-9a-f]{33}$/i.test(raw)) {
errDiv.textContent = 'Invalid token format. A token is 33 hexadecimal characters.';
errDiv.style.display = 'block';
input.value = '';
input.focus();
return;
}
btn.disabled = true;
btn.textContent = 'Validating...';
errDiv.style.display = 'none';
const valid = await validateToken(raw, USERNAME);
// Clear input immediately after use
input.value = '';
if (valid) {
TOKEN = raw;
await persistToken(STORAGE_KEY, TOKEN);
// Remove all auth forms on page
document.querySelectorAll('.convowizard-auth-form').forEach(f => f.remove());
// Activate the widget
startForWidget(widgetEl);
} else {
errDiv.textContent = 'Token is invalid or expired. Please check and try again.';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Authorize';
input.focus();
}
}
btn.addEventListener('click', onAuthorize);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); onAuthorize(); }
});
row.appendChild(input);
row.appendChild(btn);
form.appendChild(title);
form.appendChild(desc);
form.appendChild(row);
form.appendChild(errDiv);
widgetEl.prepend(form);
input.focus();
}
// ---- state + logs ----
const widgetState = new WeakMap();
function cleanupDuplicatePanels() {
const allPanels = document.querySelectorAll('.craftDisplay, .passiveModeDisplay');
const seen = new Set();
allPanels.forEach(panel => {
const pid = panel.id;
if (pid && pid.includes('_')) {
const parts = pid.split('_');
const type = parts[0];
const wid = parts.slice(1).join('_').replace('_d', '');
const key = `${type}|${wid}`;
if (seen.has(key)) panel.remove(); else seen.add(key);
}
});
}
// ========== MUTE THREAD ==========
/**
* Generates a unique thread identifier by combining URL section, DOM position, and content hash.
* Ensures each thread on a page gets a distinct ID for per-thread mute functionality.
*/
function getThreadIdForWidget(widgetEl) {
try {
const urlHash = location.hash ? location.hash.replace('#', '') : 'root';
// Generate DOM path for widget position
const widgetPath = [];
let el = widgetEl;
for (let i = 0; i < 8 && el; i++) {
const parent = el.parentElement;
if (!parent || parent === document.body) break;
const siblings = Array.from(parent.children);
const index = siblings.indexOf(el);
widgetPath.unshift(`${parent.tagName}_${index}`);
el = parent;
}
const domPath = widgetPath.length > 0 ? widgetPath.join('_') : 'unknown';
// Get content-based hash from first comment
const context = getContextStringFor(widgetEl);
let contentHash = '';
if (context && context.length > 0) {
const firstComment = context.split('\n\n')[0] || context.substring(0, 150);
let hash = 0;
for (let i = 0; i < Math.min(firstComment.length, 150); i++) {
const char = firstComment.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
const parentText = widgetEl.parentElement ?
(widgetEl.parentElement.textContent || '').substring(0, 50).trim() : '';
if (parentText) {
let hash = 0;
for (let i = 0; i < parentText.length; i++) {
hash = ((hash << 5) - hash) + parentText.charCodeAt(i);
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
contentHash = Math.random().toString(36).slice(2, 8);
}
}
return `thread_${urlHash}_${domPath.substring(0, 50)}_${contentHash}`;
} catch (err) {
console.warn(`[${NAME}] Error generating thread ID:`, err);
return `thread_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}
}
function getMutedThreads() {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const stored = localStorage.getItem(storageKey);
if (!stored) return new Set();
const threads = JSON.parse(stored);
return new Set(Array.isArray(threads) ? threads : []);
} catch {
return new Set();
}
}
function saveMutedThreads(threadSet) {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const threads = Array.from(threadSet);
localStorage.setItem(storageKey, JSON.stringify(threads));
} catch (err) {
console.warn(`[${NAME}] Error saving muted threads:`, err);
}
}
function isThreadMuted(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
return muted.has(threadId);
}
function toggleThreadMute(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
const wasMuted = muted.has(threadId);
if (wasMuted) {
muted.delete(threadId);
} else {
muted.add(threadId);
}
saveMutedThreads(muted);
return !wasMuted;
}
/**
* Creates an unmute indicator button that appears when a thread is muted
* Allows users to restore ConvoWizard for a muted thread
*/
function ensureUnmuteIndicator(widgetEl, threadId, widgetId) {
const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator');
if (existingIndicator) return;
const indicator = document.createElement('div');
indicator.id = `convowizard-unmute-${widgetId}`;
indicator.className = 'convowizard-unmute-indicator';
indicator.style.cssText = `
margin: 4px 0;
padding: 6px 10px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: #f8f9fa;
color: #54595d;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s ease;
`;
const icon = document.createElement('span');
icon.textContent = '🔊';
icon.style.fontSize = '12px';
const text = document.createElement('span');
text.textContent = `${NAME} is muted for this thread. Click to unmute.`;
text.style.flex = '1';
indicator.appendChild(icon);
indicator.appendChild(text);
indicator.addEventListener('mouseenter', () => {
indicator.style.background = '#e9ecef';
indicator.style.borderColor = '#0645ad';
});
indicator.addEventListener('mouseleave', () => {
indicator.style.background = '#f8f9fa';
indicator.style.borderColor = '#a2a9b1';
});
indicator.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Show panels and remove indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
if (contextPanel) contextPanel.style.display = '';
if (replyPanel) replyPanel.style.display = '';
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
// Update mute buttons
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(btn => {
btn.textContent = '🔇 Mute thread';
btn.title = 'Mute ConvoWizard for this thread';
btn.style.background = '#ffffff';
});
// Re-trigger analysis to refresh display
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
// Re-run the continue request to refresh the display
const inputText = st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '';
if (inputText.trim().length > 0) {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('continue', {
existing: existingForContinue,
new: inputText,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
if (data && !data.error) {
handleIntervention(widgetEl, st.inputEl, data);
}
} else {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('start', {
existing,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
if (data && !data.error && data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
handleIntervention(widgetEl, st.inputEl, data);
}
}
} else {
startForWidget(widgetEl);
}
});
// Insert at the beginning of the widget
widgetEl.insertBefore(indicator, widgetEl.firstChild);
}
function createMuteButton(threadId, widgetId, isMuted) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-mute-btn';
btn.setAttribute('data-thread-id', threadId);
btn.setAttribute('data-widget-id', widgetId);
btn.style.cssText = `
margin-left: auto;
padding: 4px 8px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: ${isMuted ? '#f8f9fa' : '#ffffff'};
color: #54595d;
cursor: pointer;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
btn.textContent = isMuted
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = isMuted
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.addEventListener('mouseenter', () => {
btn.style.background = isMuted ? '#e9ecef' : '#f8f9fa';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = isMuted ? '#f8f9fa' : '#ffffff';
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Update button state
btn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
// Hide/show panels and unmute indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
const widgetEl = contextPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
contextPanel?.closest('.ext-discussiontools-ui-newTopicWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-newTopicWidget');
if (contextPanel) contextPanel.style.display = newMuteState ? 'none' : '';
if (replyPanel) replyPanel.style.display = newMuteState ? 'none' : '';
// Clear textbox background when muting
if (newMuteState) {
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
}
if (newMuteState && widgetEl) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (widgetEl) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Update all mute buttons for this thread
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(otherBtn => {
if (otherBtn !== btn) {
otherBtn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
otherBtn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
otherBtn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
}
});
});
return btn;
}
function ensurePanels(widgetEl, threadId = null) {
cleanupDuplicatePanels();
// Clean up duplicate unmute indicators
const unmuteIndicators = widgetEl.querySelectorAll('.convowizard-unmute-indicator');
if (unmuteIndicators.length > 1) {
for (let i = 1; i < unmuteIndicators.length; i++) {
unmuteIndicators[i].remove();
}
}
const widgetId = widgetEl.id || `widget_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
if (!widgetEl.id) widgetEl.id = widgetId;
const contextId = `context_${widgetId}`;
const replyId = `reply_${widgetId}`;
const isMuted = threadId ? isThreadMuted(threadId) : false;
// Ensure Context panel
if (!document.getElementById(`${contextId}_d`)) {
const contextBox = document.createElement('div');
contextBox.id = `${contextId}_d`;
contextBox.className = 'craftDisplay';
contextBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const cheader = document.createElement('div');
cheader.id = `${contextId}_h`;
cheader.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Context Summary`;
cheader.appendChild(iconSvg);
cheader.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
cheader.appendChild(muteBtn);
}
const ccontent = document.createElement('div');
ccontent.id = `${contextId}_p`;
ccontent.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
ccontent.textContent = `When you type a reply ${NAME} will give you some feedback on the discussion context.`;
contextBox.appendChild(cheader);
contextBox.appendChild(ccontent);
if (isMuted) contextBox.style.display = 'none';
widgetEl.prepend(contextBox);
} else if (threadId) {
const existingPanel = document.getElementById(`${contextId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
// Show unmute indicator when muted (only if panels don't exist yet)
if (threadId && isMuted && !document.getElementById(`${contextId}_d`) && !document.getElementById(`${replyId}_d`)) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (threadId && !isMuted) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Ensure Reply panel
if (!document.getElementById(`${replyId}_d`)) {
const replyBox = document.createElement('div');
replyBox.id = `${replyId}_d`;
replyBox.className = 'craftDisplay';
replyBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const header = document.createElement('div');
header.id = `${replyId}_h`;
header.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Reply Summary`;
header.appendChild(iconSvg);
header.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
header.appendChild(muteBtn);
}
const content = document.createElement('div');
content.id = `${replyId}_p`;
content.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
content.textContent = `When you type a reply ${NAME} will give you some feedback on your comment.`;
replyBox.appendChild(header);
replyBox.appendChild(content);
if (isMuted) replyBox.style.display = 'none';
const preview = widgetEl.querySelector('.ext-discussiontools-ui-replyWidget-preview')
|| document.querySelector('.ext-discussiontools-ui-replyWidget-preview');
if (preview && preview.parentElement) {
preview.parentElement.insertBefore(replyBox, preview.nextSibling);
} else {
widgetEl.append(replyBox);
}
} else if (threadId) {
const existingPanel = document.getElementById(`${replyId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
return { replyId, contextId };
}
function updateContextPanel(contextId, score, showScores) {
console.log(`[${NAME}] updateContextPanel called - contextId: ${contextId}, score: ${score}, showScores: ${showScores}`);
const hasScore = typeof score === 'number';
let text = showScores && hasScore ? `Context craft score: ${score.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
console.log(`[${NAME}] hasScore: ${hasScore}, score value: ${score}`);
console.log(`[${NAME}] Thresholds - MID: ${MID_TENSION_THRESH}, HIGH: ${HIGH_TENSION_THRESH}`);
if (hasScore && score > MID_TENSION_THRESH) {
let level;
if (score > HIGH_TENSION_THRESH) {
console.log(`[${NAME}] Very high tension (score > ${HIGH_TENSION_THRESH})`);
text += `${CONTEXT_VERY_HIGH}`;
level = 'veryHigh';
} else if (score > 0.60) {
console.log(`[${NAME}] High tension (score > 0.60)`);
text += `${CONTEXT_HIGH}`;
level = 'high';
} else {
console.log(`[${NAME}] Mid tension (score > ${MID_TENSION_THRESH})`);
text += `${CONTEXT_MID}`;
level = 'mid';
}
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else {
console.log(`[${NAME}] Low tension or no score`);
text += `${CONTEXT_LOW}`;
}
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
console.log(`[${NAME}] DOM elements - box: ${!!box}, p: ${!!p}, h: ${!!h}`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) {
p.textContent = text;
console.log(`[${NAME}] Updated text content (length: ${text.length})`);
}
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Context Summary`;
}
console.log(`[${NAME}] Updated header color: ${iconColor}`);
}
}
function updateReplyPanel(replyId, scoreChange, rawScore, showScores, inputEl) {
let text = showScores ? `Change in craft score after your comment: ${scoreChange.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
text += `${REPLY_HIGH}`;
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else if (rawScore - scoreChange > 0.55 && scoreChange < -SCORE_CHANGE_THRESH) {
text += `${REPLY_LOW}`;
borderColor = '#4caf50';
bgGradient = 'linear-gradient(135deg, #f1f8e9 0%, #f9fbe7 100%)';
iconColor = '#4caf50';
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 6L6.5 11 4 8.5l1-1 1.5 1.5L10.5 5l1 1z"/>
</svg>`;
} else {
text += `${REPLY_MID}`;
}
if (inputEl) {
let threadId = null;
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) {
const muteBtn = replyBox.querySelector('.convowizard-mute-btn');
if (muteBtn) {
threadId = muteBtn.getAttribute('data-thread-id');
}
}
// If no thread ID from panel, try to get from widget
if (!threadId) {
const widgetEl = inputEl.closest('.ext-discussiontools-ui-replyWidget') ||
inputEl.closest('.ext-discussiontools-ui-newTopicWidget');
if (widgetEl) {
threadId = getThreadIdForWidget(widgetEl);
}
}
// If muted, clear background and return
if (threadId && isThreadMuted(threadId)) {
inputEl.style.setProperty('background-color', 'transparent', 'important');
return;
}
let inputBg = 'transparent';
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
inputBg = TENSION_COLORS[level].inputBg;
} else if (scoreChange < -SCORE_CHANGE_THRESH) {
inputBg = 'rgba(76,175,80,0.05)';
}
inputEl.style.setProperty('background-color', inputBg, 'important');
inputEl.style.setProperty('transition', 'background-color 0.3s ease', 'important');
}
const box = document.getElementById(`${replyId}_d`);
const p = document.getElementById(`${replyId}_p`);
const h = document.getElementById(`${replyId}_h`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) p.textContent = text;
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary`;
}
}
}
function getContextStringFor(widgetEl) {
try {
console.log(`[${NAME}] === Extracting context for widget ===`);
// Helper to check if element is a comment container
const isCommentNode = (el) => !!el && (el.tagName === 'DD' || el.tagName === 'LI' || el.tagName === 'P');
// Extract clean comment text, removing nested replies and UI elements
// IMPORTANT: Keep timestamps (UTC) because backend splits on them!
const extractCommentText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
// Remove nested discussion lists, widgets, and UI elements
// NOTE: DO NOT remove .ext-discussiontools-init-timestamplink - backend needs timestamps to split utterances!
clone.querySelectorAll('dl, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, .ext-discussiontools-init-replylink-buttons, [role="button"]').forEach(n => n.remove());
const text = (clone.innerText || '').trim();
return text;
};
// Wikipedia Talk pages have structure: <dd>comment text<dl><dd>reply<dl><dd>widget
// When widget appears, it's nested inside a new <dd> inside a new <dl>
// We need to find the parent <dd> that contains actual comment text
let commentDD = null;
// Strategy 1: Widget is inside <dd> → <dl> → <dd> (the target comment)
// Find the <dd> containing the widget
const widgetDD = widgetEl.closest('dd, li');
console.log(`[${NAME}] Widget container DD:`, widgetDD ? widgetDD.tagName : 'not found');
if (widgetDD) {
// Check if this DD has meaningful text (not just the widget)
const widgetDDText = extractCommentText(widgetDD);
console.log(`[${NAME}] Widget DD text length:`, widgetDDText.length);
if (widgetDDText.length > 20) {
// This DD has actual comment text
commentDD = widgetDD;
} else {
// This DD is empty/minimal - the widget was just inserted
// Go up: DD → DL → parent DD (which should have the comment)
const parentDL = widgetDD.parentElement;
console.log(`[${NAME}] Parent DL:`, parentDL ? parentDL.tagName : 'not found');
if (parentDL && (parentDL.tagName === 'DL' || parentDL.tagName === 'UL' || parentDL.tagName === 'OL')) {
// Find the parent DD/LI/P that contains this DL
commentDD = parentDL.closest('dd, li, p');
console.log(`[${NAME}] Found parent comment node:`, commentDD ? commentDD.tagName : 'not found');
}
}
}
// Strategy 2: Look for preceding paragraph with comment markers (flat structure)
if (!commentDD) {
console.log(`[${NAME}] Trying flat structure search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 10) {
probe = probe.previousElementSibling || (probe.parentElement ? probe.parentElement.previousElementSibling : null);
if (probe && probe.tagName === 'P' && probe.querySelector('[data-mw-comment-start], [data-mw-comment-end]')) {
commentDD = probe;
console.log(`[${NAME}] Found comment via flat search at hop ${hops}`);
break;
}
}
}
// Strategy 3: Last resort - traverse DOM looking for any comment node
if (!commentDD) {
console.log(`[${NAME}] Trying fallback search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 20) {
if (isCommentNode(probe) && extractCommentText(probe).length > 20) {
commentDD = probe;
console.log(`[${NAME}] Found comment via fallback at hop ${hops}`);
break;
}
probe = probe.previousElementSibling ||
(probe.parentElement ? probe.parentElement.previousElementSibling : null) ||
(probe.parentElement ? probe.parentElement.parentElement : null);
}
}
if (!commentDD) {
console.warn(`[${NAME}] Could not find any comment node!`);
return '';
}
// Now traverse up the comment chain collecting all parent comments
const texts = [];
const seen = new Set();
let current = commentDD;
let iterations = 0;
console.log(`[${NAME}] Starting comment chain traversal from:`, current.tagName);
while (current && iterations++ < 20) {
if (seen.has(current)) {
console.log(`[${NAME}] Already seen this node, breaking`);
break;
}
seen.add(current);
const text = extractCommentText(current);
if (text && text.length > 0) {
console.log(`[${NAME}] Extracted comment (${text.length} chars):`, text.substring(0, 100) + '...');
texts.unshift(text); // Add to beginning to maintain chronological order
}
// Navigate up the discussion tree
// Structure: <p>text</p><dl><dd>reply<dl><dd>reply</dd></dl></dd></dl>
// From current <dd>, go to parent element (should be <dl>)
const parentList = current.parentElement;
if (!parentList) {
console.log(`[${NAME}] No parent element, stopping`);
break;
}
console.log(`[${NAME}] Parent list:`, parentList.tagName);
// From <dl>, find the parent <dd> or <li> or <p>
if (parentList.tagName === 'DL' || parentList.tagName === 'UL' || parentList.tagName === 'OL') {
// First try to find an ancestor comment node
current = parentList.closest('dd, li, p');
if (current) {
console.log(`[${NAME}] Found ancestor comment:`, current.tagName);
} else {
// No ancestor found - the <p> might be a SIBLING of this <dl>
// This happens in flat structure: <p>comment</p><dl><dd>reply...
console.log(`[${NAME}] No ancestor, searching for previous sibling of DL...`);
let sibling = parentList.previousElementSibling;
let siblingHops = 0;
while (sibling && siblingHops++ < 10) {
console.log(`[${NAME}] Checking sibling:`, sibling.tagName);
if (isCommentNode(sibling) && !seen.has(sibling)) {
const siblingText = extractCommentText(sibling);
if (siblingText.length > 20) {
current = sibling;
console.log(`[${NAME}] Found sibling comment with text (${siblingText.length} chars)`);
break;
}
}
sibling = sibling.previousElementSibling;
}
if (!current || current === parentList.closest('dd, li, p')) {
console.log(`[${NAME}] No more comments found, stopping`);
break;
}
}
} else if (isCommentNode(parentList)) {
// Parent is itself a comment node
current = parentList;
} else {
// Try to find previous sibling comment
let sibling = current.previousElementSibling;
while (sibling && !isCommentNode(sibling)) {
sibling = sibling.previousElementSibling;
}
if (sibling && !seen.has(sibling)) {
current = sibling;
console.log(`[${NAME}] Moving to sibling:`, current.tagName);
} else {
console.log(`[${NAME}] No more siblings, stopping`);
break;
}
}
}
console.log(`[${NAME}] Extracted ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
// Additional pass: Look for any <p> elements with comment markers that might have been missed
// This catches the case where the first comment in a thread is a <p> that's a sibling of the DL
if (commentDD && commentDD.tagName === 'DD') {
const topDL = commentDD.closest('dl');
if (topDL && topDL.previousElementSibling) {
let probe = topDL.previousElementSibling;
let probeHops = 0;
console.log(`[${NAME}] Scanning backwards from top DL for more context...`);
while (probe && probeHops++ < 5) {
if (probe.tagName === 'P' && !seen.has(probe)) {
const probeText = extractCommentText(probe);
if (probeText.length > 20 && (probe.querySelector('[data-mw-comment-start]') || probe.querySelector('[data-mw-comment-end]'))) {
console.log(`[${NAME}] Found additional preceding comment (${probeText.length} chars):`, probeText.substring(0, 100) + '...');
texts.unshift(probeText);
seen.add(probe);
}
}
probe = probe.previousElementSibling;
}
}
}
console.log(`[${NAME}] Final extraction: ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
if (texts.length > 0) {
const fullContext = texts.join('\n\n');
console.log(`[${NAME}] Full context:`, fullContext);
return fullContext;
}
console.warn(`[${NAME}] No context found, returning empty string`);
return '';
} catch (err) {
console.error(`[${NAME}] Error extracting context:`, err);
return '';
}
}
async function startForWidget(widgetEl) {
if (!TOKEN) { showTokenForm(widgetEl); return; }
if (widgetState.has(widgetEl) || widgetEl.getAttribute('data-convowizard-processing') === 'true') return;
widgetEl.setAttribute('data-convowizard-processing', 'true');
console.log(`[${NAME}] Starting widget processing:`, widgetEl.className);
function findInput(scope) {
// Prefer DT textbox role, then VE contenteditable surface, then textarea
return (
scope.querySelector('div[role="textbox"]') ||
scope.querySelector('[contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface [contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface') ||
scope.querySelector('textarea')
);
}
async function waitForInput(scope, timeoutMs = 8000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tick = () => {
const el = findInput(scope) || findInput(document);
if (el) return resolve(el);
if (Date.now() > deadline) return resolve(null);
setTimeout(tick, 100);
};
tick();
});
}
const inputEl = await waitForInput(widgetEl);
if (!inputEl) {
console.warn(`[${NAME}] No input element found in widget (after waiting)`);
return;
}
console.log(`[${NAME}] Input element found:`, inputEl.tagName, inputEl.getAttribute('role') || (inputEl.getAttribute('contenteditable') ? 'contenteditable' : ''));
const context = getContextStringFor(widgetEl);
console.log(`[${NAME}] Context extracted, length: ${context.length} chars`);
console.log(`[${NAME}] Context preview:`, context.substring(0, 200));
// Split context on (UTC) timestamps to create separate utterances like Reddit does
// This prevents backend caching bug where all Wikipedia convos share id=None
const utterances = context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = utterances.map((text, index) => ({
id: `wiki_${Date.now()}_${index}`, // Unique ID prevents cache collision
text: text.trim()
}));
console.log(`[${NAME}] Created ${existing.length} utterances with unique IDs`);
existing.forEach((utt, i) => {
console.log(`[${NAME}] ${i}: ID=${utt.id}, text=${utt.text.substring(0, 60)}...`);
});
const data = await postJson('start', {
existing, // Send as array like Reddit, not context string!
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
console.log(`[${NAME}] Received response from /start:`, data);
// Handle invalid token - clear and show auth form
if (data && data.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (!data || !data.interaction_id) return;
widgetState.set(widgetEl, { interactionId: data.interaction_id, inputEl, context });
handleIntervention(widgetEl, inputEl, data);
const sendContinue = throttle(async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
// Check if thread is muted
const threadId = getThreadIdForWidget(widgetEl);
if (threadId && isThreadMuted(threadId)) {
// Clear input background if muted
if (st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
return;
}
const widgetId = widgetEl.id;
const h = document.getElementById(`reply_${widgetId}_h`);
if (h) {
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>
`;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span with processing indicator
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary (Processing...)`;
}
}
// Recreate existing array from cached context for continue requests
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`, // Use interaction_id for consistency
text: text.trim()
}));
const data2 = await postJson('continue', {
existing: existingForContinue,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
// Handle invalid token on continue
if (data2 && data2.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
handleIntervention(widgetEl, st.inputEl, data2);
}, 3000);
inputEl.addEventListener('input', sendContinue);
inputEl.addEventListener('keyup', sendContinue);
inputEl.addEventListener('paste', sendContinue);
let postBtn = widgetEl.querySelector(".oo-ui-buttonElement-button[title*='Reply']");
if (!postBtn) {
postBtn = Array.from(widgetEl.querySelectorAll('.oo-ui-buttonElement-button'))
.find(b => /reply/i.test((b.title || b.innerText || '').trim()));
}
if (postBtn) {
postBtn.addEventListener('click', async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
try {
// Recreate existing array from cached context for submit
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForSubmit = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
await postJson('submit', {
existing: existingForSubmit,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
submitted_id: null,
token: TOKEN,
username: USERNAME
});
} finally {
widgetState.delete(widgetEl);
}
});
}
}
function handleIntervention(widgetEl, inputEl, data) {
if (!data) {
console.warn(`[${NAME}] handleIntervention called with no data`);
return;
}
console.log(`[${NAME}] handleIntervention called with data:`, data);
if (data.error) {
return;
}
const interactionId = data.interaction_id;
if (!interactionId) {
console.warn(`[${NAME}] No interaction_id in data`);
return;
}
const threadId = getThreadIdForWidget(widgetEl);
// Skip display if thread is muted
if (threadId && isThreadMuted(threadId)) {
const widgetId = widgetEl.id || `widget_${Date.now()}`;
if (!widgetEl.id) widgetEl.id = widgetId;
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
return;
}
const { replyId, contextId } = ensurePanels(widgetEl, threadId);
const showScores = Object.prototype.hasOwnProperty.call(data, 'show_scores');
console.log(`[${NAME}] Panel IDs - context: ${contextId}, reply: ${replyId}`);
console.log(`[${NAME}] Show scores: ${showScores}`);
const which = (data.which || '').substring(0, 5);
console.log(`[${NAME}] Response type (which): ${which}`);
if (which === 'craft') {
console.log(`[${NAME}] CRAFT mode - craft_ctx_score:`, data.craft_ctx_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_score:`, data.craft_reply_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_change:`, data.craft_reply_change);
if (data.craft_ctx_score != null) {
console.log(`[${NAME}] Updating context panel with score: ${data.craft_ctx_score}`);
updateContextPanel(contextId, data.craft_ctx_score, showScores);
} else {
console.log(`[${NAME}] No context score, updating context panel with null`);
updateContextPanel(contextId, null, showScores);
}
if (data.craft_reply_change != null && data.craft_reply_score != null) {
console.log(`[${NAME}] Updating reply panel with change: ${data.craft_reply_change}, score: ${data.craft_reply_score}`);
updateReplyPanel(replyId, data.craft_reply_change, data.craft_reply_score, showScores, inputEl);
} else {
console.log(`[${NAME}] No reply scores, updating reply panel with zeros`);
updateReplyPanel(replyId, 0, 0, showScores, inputEl);
}
} else {
const noteId = `passivenote${interactionId}_d`;
if (!data.message && !document.getElementById(noteId)) {
const box = document.createElement('div');
box.id = noteId;
box.className = 'passiveModeDisplay';
box.style.cssText = `
margin:8px 0;padding:8px 12px;border-radius:6px;border:1px solid #a2a9b1;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;opacity:.8;
`;
const content = document.createElement('div');
content.id = `passivenote${interactionId}_p`;
content.style.cssText = `font-size:12px;color:#54595d;margin:0;font-style:italic;`;
content.textContent = `${NAME} is currently not active on this thread.`;
box.appendChild(content);
widgetEl.prepend(box);
}
}
}
function installClickDelegation() {
document.addEventListener('click', (e) => {
console.log(`[${NAME}] Click detected on:`, e.target.tagName, e.target.className);
// Match reply buttons and links (both old and new DiscussionTools formats)
// Need to check the actual clicked element AND traverse up
const link = e.target.closest && (
e.target.closest('.ext-discussiontools-init-replybutton') ||
e.target.closest('.ext-discussiontools-init-replylink-reply') ||
e.target.closest('a.ext-discussiontools-init-replylink') ||
e.target.closest('.ext-discussiontools-init-replylink-buttons') ||
e.target.closest('[class*="ext-discussiontools-init-"][class*="reply"]')
);
if (!link) return;
console.log(`[${NAME}] Reply link clicked:`, link.className);
// For flat discussion structures (like Research talk pages), the widget may be inserted
// outside the immediate parent. Search more broadly.
const anchorScope = link.closest('dd, li, p, div, section') || document.body;
const broadScope = anchorScope.closest('#mw-content-text, .mw-parser-output, #content') || document.body;
console.log(`[${NAME}] Anchor scope:`, anchorScope.tagName, anchorScope.className);
console.log(`[${NAME}] Broad scope:`, broadScope.tagName, broadScope.className);
const timeoutAt = Date.now() + 8000;
let attempts = 0;
const poll = setInterval(() => {
attempts++;
// Try multiple search strategies:
// 1. In immediate anchor scope (for nested structures like dd/li)
// 2. As sibling to anchor scope (for flat structures like p)
// 3. In broader content scope
// 4. Document-wide fallback
const widget = anchorScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| anchorScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| (anchorScope.nextElementSibling && (
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-replyWidget') ||
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-newTopicWidget')
) ? anchorScope.nextElementSibling : null)
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-replyWidget:last-child'))
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-newTopicWidget:last-child'))
|| broadScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| broadScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| document.querySelector('.ext-discussiontools-ui-replyWidget')
|| document.querySelector('.ext-discussiontools-ui-newTopicWidget');
if (widget) {
console.log(`[${NAME}] Widget found after ${attempts} attempts:`, widget.className);
clearInterval(poll);
startForWidget(widget);
}
else if (Date.now() > timeoutAt) {
console.log(`[${NAME}] Widget timeout after ${attempts} attempts - no widget found`);
console.log(`[${NAME}] Searched in anchorScope:`, anchorScope);
console.log(`[${NAME}] Searched in broadScope:`, broadScope);
clearInterval(poll);
} else if (attempts % 10 === 0) {
console.log(`[${NAME}] Still searching for widget, attempt ${attempts}...`);
}
}, 100);
});
}
function installWidgetObserver() {
const root = document.getElementById('content') || document.body;
console.log(`[${NAME}] Setting up mutation observer on:`, root.id || root.tagName);
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof HTMLElement)) continue;
// Log all significant additions for debugging
if (n.className && typeof n.className === 'string' && n.className.includes('ext-discussiontools')) {
console.log(`[${NAME}] DOM addition detected:`, n.tagName, n.className);
}
// Match specific DiscussionTools widgets
if (n.matches('.ext-discussiontools-ui-replyWidget') || n.matches('.ext-discussiontools-ui-newTopicWidget')) {
console.log(`[${NAME}] *** Widget detected via observer ***:`, n.className);
startForWidget(n);
} else {
const nested = n.querySelector && (
n.querySelector('.ext-discussiontools-ui-replyWidget') ||
n.querySelector('.ext-discussiontools-ui-newTopicWidget')
);
if (nested) {
console.log(`[${NAME}] *** Nested widget detected ***:`, nested.className);
startForWidget(nested);
}
}
}
}
});
obs.observe(root, { childList: true, subtree: true });
console.log(`[${NAME}] Mutation observer is now watching for widget creation`);
// Check for existing widgets on page load
const existingWidgets = document.querySelectorAll('.ext-discussiontools-ui-replyWidget, .ext-discussiontools-ui-newTopicWidget');
if (existingWidgets.length > 0) {
console.log(`[${NAME}] Found ${existingWidgets.length} existing widget(s) on page load`);
existingWidgets.forEach(w => {
console.log(`[${NAME}] Processing existing widget:`, w.className);
startForWidget(w);
});
} else {
console.log(`[${NAME}] No existing widgets found on page load`);
}
}
// kick off
installClickDelegation();
installWidgetObserver();
console.log(`[${NAME}] === INITIALIZATION COMPLETE ===`);
console.log(`[${NAME}] Click delegation: ACTIVE`);
console.log(`[${NAME}] Mutation observer: ACTIVE`);
console.log(`[${NAME}] Waiting for user interaction...`);
})();
//</nowiki>
gqs3nqhf6blhg26t1docydtkqnp9kif
740051
740024
2026-05-01T17:49:44Z
Laerdon
70887
740051
javascript
text/javascript
//<nowiki>
/**
* Filename: wiki-talk-page-script.js
* ConvoWizard – Real-time Reply Feedback (Wikipedia userscript)
* Runs on Talk pages inside DiscussionTools reply widgets.
* Prompts for a token on first run; persists after authorization.
*/
(async function () {
'use strict';
// Load core util module via ResourceLoader
await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api']);
// === CONFIG ===
const SERVER = 'https://craft.infosci.cornell.edu/extension';
// ---- UI strings and thresholds ----
const NAME = 'ConvoWizard';
const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search);
const OPTION_KEY = 'userjs-convowizard-token';
const SCORE_CHANGE_THRESH = 0.08;
const MID_TENSION_THRESH = 0.50;
const HIGH_TENSION_THRESH = 0.75;
const CONTEXT_LOW = `${NAME} will notify you here if it detects anything in the preceding conversation.`;
const CONTEXT_MID = `${NAME} thinks this discussion is getting somewhat tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_HIGH = `${NAME} thinks this discussion is getting tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_VERY_HIGH = `${NAME} thinks this discussion is getting very tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const REPLY_LOW = `${NAME} thinks this comment might decrease tension in this discussion.`;
const REPLY_MID = `${NAME} will notify you here if it detects anything in your comment draft.`;
const REPLY_HIGH = `${NAME} thinks this comment might increase the tension in this discussion. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const TENSION_COLORS = {
mid: { border: '#e8825c', bg: 'linear-gradient(135deg, #fff0eb 0%, #fff8f5 100%)', icon: '#e8825c', inputBg: 'rgba(232,130,92,0.05)' },
high: { border: '#d73027', bg: 'linear-gradient(135deg, #ffeaea 0%, #fff5f5 100%)', icon: '#d73027', inputBg: 'rgba(215,48,39,0.05)' },
veryHigh: { border: '#a50f15', bg: 'linear-gradient(135deg, #fdd 0%, #fee 100%)', icon: '#a50f15', inputBg: 'rgba(165,15,21,0.07)' },
};
// Keep ConvoWizard logs quiet by default to reduce console noise on Wikimedia pages.
const nativeConsole = window.console;
const console = {
log: (...args) => {
if (!DEBUG && typeof args[0] === 'string' && args[0].startsWith(`[${NAME}]`)) {
return;
}
nativeConsole.log(...args);
},
warn: (...args) => nativeConsole.warn(...args),
error: (...args) => nativeConsole.error(...args)
};
const isWikipediaHost = /(^|\.)wikipedia\.org$/.test(location.hostname);
const namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
const isTalkNamespace = Number.isFinite(namespaceNumber) && namespaceNumber % 2 === 1;
const isViewAction = mw.config.get('wgAction') === 'view';
// Gadget scope guard: only run on Wikipedia talk pages in normal view mode.
if (!isWikipediaHost || !isTalkNamespace || !isViewAction) {
console.log(
`[${NAME}] Skipping page (host=${location.hostname}, ns=${namespaceNumber}, action=${mw.config.get('wgAction')})`
);
return;
}
// ---------- helpers ----------
function throttle(fn, waitMs) {
let last = 0, timer = 0, pendingArgs = null;
return (...args) => {
const now = Date.now();
const remaining = waitMs - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
} else {
pendingArgs = args;
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn(...pendingArgs); }, remaining);
}
};
}
// Rewrite Wikipedia URL so the reddit-style backend can extract a post_id.
// Backend does: url.split("comments/")[1][:6]
// We inject "comments/<hash>" where hash is derived from the wiki page path.
function wikiUrlToPostId(href) {
try {
const pagePath = href.split('/wiki/')[1] || href;
// Simple hash to produce a stable 6-char hex id per page
let h = 0;
for (let i = 0; i < pagePath.length; i++) {
h = ((h << 5) - h) + pagePath.charCodeAt(i);
h = h & h;
}
const hex = Math.abs(h).toString(16).padStart(6, '0').slice(0, 6);
return href.split('/wiki/')[0] + '/comments/' + hex;
} catch {
return href;
}
}
async function postJson(route, body) {
try {
const res = await fetch(SERVER + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
nativeConsole.log(`[${NAME}] response from ${route}: status=${res.status}`, text);
try {
const data = JSON.parse(text);
nativeConsole.log(`[${NAME}] json from ${route}:`, data);
return data;
} catch (err) {
console.warn(`[${NAME}] non-json response from ${route}:`, text);
return {};
}
} catch (err) {
console.warn(`[${NAME}] request failed for ${route}:`, err);
return {};
}
}
function getTokenFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(OPTION_KEY);
if (typeof raw === 'string') {
const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
}
}
} catch (err) {
console.warn(`[${NAME}] Failed to read token from user options`, err);
}
return null;
}
async function persistToken(storageKey, token) {
if (!token) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, token);
}
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(OPTION_KEY, token || '');
await new mw.Api().saveOption(OPTION_KEY, token || '');
console.log(`[${NAME}] ${token ? 'Saved' : 'Cleared'} token in user options (notepad)`);
} catch (err) {
console.warn(`[${NAME}] Could not persist token to user options`, err);
}
}
async function validateToken(token, username) {
try {
const response = await postJson('claim_token', { token, username });
return response.valid === true;
} catch {
return false;
}
}
// ========== CREDENTIAL SETUP ==========
const USERNAME = mw.config.get('wgUserName');
if (!USERNAME) {
console.log(`[${NAME}] Not logged in, skipping`);
return;
}
let TOKEN = null;
const STORAGE_KEY = `ConvoWizard:token:${USERNAME}`;
// Load persisted token (options → localStorage), validate, sync stores
async function loadPersistedToken() {
let token = getTokenFromOptions();
let source = 'options';
if (!token) {
token = localStorage.getItem(STORAGE_KEY);
source = 'localStorage';
}
if (token && typeof token === 'string') {
token = token.trim();
}
if (!token) return;
const valid = await validateToken(token, USERNAME);
if (valid) {
TOKEN = token;
// Sync both stores
if (source === 'options') {
localStorage.setItem(STORAGE_KEY, token);
} else {
await persistToken(STORAGE_KEY, token);
}
console.log(`[${NAME}] Token loaded from ${source}`);
} else {
console.log(`[${NAME}] Persisted token invalid, clearing`);
await persistToken(STORAGE_KEY, null);
}
}
await loadPersistedToken();
console.log(`[${NAME}] === SCRIPT LOADED SUCCESSFULLY ===`);
console.log(`[${NAME}] Server: ${SERVER}`);
console.log(`[${NAME}] User: ${USERNAME}`);
console.log(`[${NAME}] Token: ${TOKEN ? 'present' : 'none (will prompt)'}`);
console.log(`[${NAME}] Page URL: ${location.href}`);
console.log(`[${NAME}] Installing click and mutation observers...`);
// ========== TOKEN INPUT FORM ==========
function showTokenForm(widgetEl) {
// Don't add duplicate forms
if (widgetEl.querySelector('.convowizard-auth-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-auth-form';
form.style.cssText = `
margin:8px 0;padding:16px 20px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
const title = document.createElement('div');
title.style.cssText = 'font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;';
title.textContent = `${NAME}: Authorization Required`;
const desc = document.createElement('div');
desc.style.cssText = 'font-size:13px;color:#333;margin-bottom:12px;line-height:1.4;';
desc.textContent = 'Please paste your ConvoWizard token below to activate the extension.';
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;align-items:center;';
const input = document.createElement('input');
input.type = 'password';
input.placeholder = 'Paste token here';
input.autocomplete = 'off';
input.spellcheck = false;
input.style.cssText = `
flex:1;padding:8px 10px;font-size:13px;border:1px solid #a2a9b1;border-radius:4px;
font-family:monospace;outline:none;transition:border-color 0.2s ease;
`;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Authorize';
btn.style.cssText = `
padding:8px 16px;font-size:13px;font-weight:600;border:none;border-radius:4px;
background:#0645ad;color:#fff;cursor:pointer;transition:background 0.2s ease;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
btn.addEventListener('mouseenter', () => { btn.style.background = '#0b5cce'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#0645ad'; });
const errDiv = document.createElement('div');
errDiv.style.cssText = 'font-size:12px;color:#d73027;margin-top:8px;display:none;';
async function onAuthorize() {
const raw = input.value.trim();
// Input validation: 33 hex chars
if (!/^[0-9a-f]{33}$/i.test(raw)) {
errDiv.textContent = 'Invalid token format. A token is 33 hexadecimal characters.';
errDiv.style.display = 'block';
input.value = '';
input.focus();
return;
}
btn.disabled = true;
btn.textContent = 'Validating...';
errDiv.style.display = 'none';
const valid = await validateToken(raw, USERNAME);
// Clear input immediately after use
input.value = '';
if (valid) {
TOKEN = raw;
await persistToken(STORAGE_KEY, TOKEN);
// Remove all auth forms on page
document.querySelectorAll('.convowizard-auth-form').forEach(f => f.remove());
// Activate the widget
startForWidget(widgetEl);
} else {
errDiv.textContent = 'Token is invalid or expired. Please check and try again.';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Authorize';
input.focus();
}
}
btn.addEventListener('click', onAuthorize);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); onAuthorize(); }
});
row.appendChild(input);
row.appendChild(btn);
form.appendChild(title);
form.appendChild(desc);
form.appendChild(row);
form.appendChild(errDiv);
widgetEl.prepend(form);
input.focus();
}
// ---- state + logs ----
const widgetState = new WeakMap();
function cleanupDuplicatePanels() {
const allPanels = document.querySelectorAll('.craftDisplay, .passiveModeDisplay');
const seen = new Set();
allPanels.forEach(panel => {
const pid = panel.id;
if (pid && pid.includes('_')) {
const parts = pid.split('_');
const type = parts[0];
const wid = parts.slice(1).join('_').replace('_d', '');
const key = `${type}|${wid}`;
if (seen.has(key)) panel.remove(); else seen.add(key);
}
});
}
// ========== MUTE THREAD ==========
/**
* Generates a unique thread identifier by combining URL section, DOM position, and content hash.
* Ensures each thread on a page gets a distinct ID for per-thread mute functionality.
*/
function getThreadIdForWidget(widgetEl) {
try {
const urlHash = location.hash ? location.hash.replace('#', '') : 'root';
// Generate DOM path for widget position
const widgetPath = [];
let el = widgetEl;
for (let i = 0; i < 8 && el; i++) {
const parent = el.parentElement;
if (!parent || parent === document.body) break;
const siblings = Array.from(parent.children);
const index = siblings.indexOf(el);
widgetPath.unshift(`${parent.tagName}_${index}`);
el = parent;
}
const domPath = widgetPath.length > 0 ? widgetPath.join('_') : 'unknown';
// Get content-based hash from first comment
const context = getContextStringFor(widgetEl);
let contentHash = '';
if (context && context.length > 0) {
const firstComment = context.split('\n\n')[0] || context.substring(0, 150);
let hash = 0;
for (let i = 0; i < Math.min(firstComment.length, 150); i++) {
const char = firstComment.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
const parentText = widgetEl.parentElement ?
(widgetEl.parentElement.textContent || '').substring(0, 50).trim() : '';
if (parentText) {
let hash = 0;
for (let i = 0; i < parentText.length; i++) {
hash = ((hash << 5) - hash) + parentText.charCodeAt(i);
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
contentHash = Math.random().toString(36).slice(2, 8);
}
}
return `thread_${urlHash}_${domPath.substring(0, 50)}_${contentHash}`;
} catch (err) {
console.warn(`[${NAME}] Error generating thread ID:`, err);
return `thread_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}
}
function getMutedThreads() {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const stored = localStorage.getItem(storageKey);
if (!stored) return new Set();
const threads = JSON.parse(stored);
return new Set(Array.isArray(threads) ? threads : []);
} catch {
return new Set();
}
}
function saveMutedThreads(threadSet) {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const threads = Array.from(threadSet);
localStorage.setItem(storageKey, JSON.stringify(threads));
} catch (err) {
console.warn(`[${NAME}] Error saving muted threads:`, err);
}
}
function isThreadMuted(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
return muted.has(threadId);
}
function toggleThreadMute(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
const wasMuted = muted.has(threadId);
if (wasMuted) {
muted.delete(threadId);
} else {
muted.add(threadId);
}
saveMutedThreads(muted);
return !wasMuted;
}
/**
* Creates an unmute indicator button that appears when a thread is muted
* Allows users to restore ConvoWizard for a muted thread
*/
function ensureUnmuteIndicator(widgetEl, threadId, widgetId) {
const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator');
if (existingIndicator) return;
const indicator = document.createElement('div');
indicator.id = `convowizard-unmute-${widgetId}`;
indicator.className = 'convowizard-unmute-indicator';
indicator.style.cssText = `
margin: 4px 0;
padding: 6px 10px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: #f8f9fa;
color: #54595d;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s ease;
`;
const icon = document.createElement('span');
icon.textContent = '🔊';
icon.style.fontSize = '12px';
const text = document.createElement('span');
text.textContent = `${NAME} is muted for this thread. Click to unmute.`;
text.style.flex = '1';
indicator.appendChild(icon);
indicator.appendChild(text);
indicator.addEventListener('mouseenter', () => {
indicator.style.background = '#e9ecef';
indicator.style.borderColor = '#0645ad';
});
indicator.addEventListener('mouseleave', () => {
indicator.style.background = '#f8f9fa';
indicator.style.borderColor = '#a2a9b1';
});
indicator.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Show panels and remove indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
if (contextPanel) contextPanel.style.display = '';
if (replyPanel) replyPanel.style.display = '';
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
// Update mute buttons
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(btn => {
btn.textContent = '🔇 Mute thread';
btn.title = 'Mute ConvoWizard for this thread';
btn.style.background = '#ffffff';
});
// Re-trigger analysis to refresh display
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
// Re-run the continue request to refresh the display
const inputText = st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '';
if (inputText.trim().length > 0) {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('continue', {
existing: existingForContinue,
new: inputText,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
if (data && !data.error) {
handleIntervention(widgetEl, st.inputEl, data);
}
} else {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('start', {
existing,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
if (data && !data.error && data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
handleIntervention(widgetEl, st.inputEl, data);
}
}
} else {
startForWidget(widgetEl);
}
});
// Insert at the beginning of the widget
widgetEl.insertBefore(indicator, widgetEl.firstChild);
}
function createMuteButton(threadId, widgetId, isMuted) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-mute-btn';
btn.setAttribute('data-thread-id', threadId);
btn.setAttribute('data-widget-id', widgetId);
btn.style.cssText = `
margin-left: auto;
padding: 4px 8px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: ${isMuted ? '#f8f9fa' : '#ffffff'};
color: #54595d;
cursor: pointer;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
btn.textContent = isMuted
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = isMuted
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.addEventListener('mouseenter', () => {
btn.style.background = isMuted ? '#e9ecef' : '#f8f9fa';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = isMuted ? '#f8f9fa' : '#ffffff';
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Update button state
btn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
// Hide/show panels and unmute indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
const widgetEl = contextPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
contextPanel?.closest('.ext-discussiontools-ui-newTopicWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-newTopicWidget');
if (contextPanel) contextPanel.style.display = newMuteState ? 'none' : '';
if (replyPanel) replyPanel.style.display = newMuteState ? 'none' : '';
// Clear textbox background when muting
if (newMuteState) {
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
}
if (newMuteState && widgetEl) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (widgetEl) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Update all mute buttons for this thread
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(otherBtn => {
if (otherBtn !== btn) {
otherBtn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
otherBtn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
otherBtn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
}
});
});
return btn;
}
function ensurePanels(widgetEl, threadId = null) {
cleanupDuplicatePanels();
// Clean up duplicate unmute indicators
const unmuteIndicators = widgetEl.querySelectorAll('.convowizard-unmute-indicator');
if (unmuteIndicators.length > 1) {
for (let i = 1; i < unmuteIndicators.length; i++) {
unmuteIndicators[i].remove();
}
}
const widgetId = widgetEl.id || `widget_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
if (!widgetEl.id) widgetEl.id = widgetId;
const contextId = `context_${widgetId}`;
const replyId = `reply_${widgetId}`;
const isMuted = threadId ? isThreadMuted(threadId) : false;
// Ensure Context panel
if (!document.getElementById(`${contextId}_d`)) {
const contextBox = document.createElement('div');
contextBox.id = `${contextId}_d`;
contextBox.className = 'craftDisplay';
contextBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const cheader = document.createElement('div');
cheader.id = `${contextId}_h`;
cheader.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Context Summary`;
cheader.appendChild(iconSvg);
cheader.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
cheader.appendChild(muteBtn);
}
const ccontent = document.createElement('div');
ccontent.id = `${contextId}_p`;
ccontent.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
ccontent.textContent = `When you type a reply ${NAME} will give you some feedback on the discussion context.`;
contextBox.appendChild(cheader);
contextBox.appendChild(ccontent);
if (isMuted) contextBox.style.display = 'none';
widgetEl.prepend(contextBox);
} else if (threadId) {
const existingPanel = document.getElementById(`${contextId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
// Show unmute indicator when muted (only if panels don't exist yet)
if (threadId && isMuted && !document.getElementById(`${contextId}_d`) && !document.getElementById(`${replyId}_d`)) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (threadId && !isMuted) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Ensure Reply panel
if (!document.getElementById(`${replyId}_d`)) {
const replyBox = document.createElement('div');
replyBox.id = `${replyId}_d`;
replyBox.className = 'craftDisplay';
replyBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const header = document.createElement('div');
header.id = `${replyId}_h`;
header.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Reply Summary`;
header.appendChild(iconSvg);
header.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
header.appendChild(muteBtn);
}
const content = document.createElement('div');
content.id = `${replyId}_p`;
content.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
content.textContent = `When you type a reply ${NAME} will give you some feedback on your comment.`;
replyBox.appendChild(header);
replyBox.appendChild(content);
if (isMuted) replyBox.style.display = 'none';
const preview = widgetEl.querySelector('.ext-discussiontools-ui-replyWidget-preview')
|| document.querySelector('.ext-discussiontools-ui-replyWidget-preview');
if (preview && preview.parentElement) {
preview.parentElement.insertBefore(replyBox, preview.nextSibling);
} else {
widgetEl.append(replyBox);
}
} else if (threadId) {
const existingPanel = document.getElementById(`${replyId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
return { replyId, contextId };
}
function updateContextPanel(contextId, score, showScores) {
console.log(`[${NAME}] updateContextPanel called - contextId: ${contextId}, score: ${score}, showScores: ${showScores}`);
const hasScore = typeof score === 'number';
let text = showScores && hasScore ? `Context craft score: ${score.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
console.log(`[${NAME}] hasScore: ${hasScore}, score value: ${score}`);
console.log(`[${NAME}] Thresholds - MID: ${MID_TENSION_THRESH}, HIGH: ${HIGH_TENSION_THRESH}`);
if (hasScore && score > MID_TENSION_THRESH) {
let level;
if (score > HIGH_TENSION_THRESH) {
console.log(`[${NAME}] Very high tension (score > ${HIGH_TENSION_THRESH})`);
text += `${CONTEXT_VERY_HIGH}`;
level = 'veryHigh';
} else if (score > 0.60) {
console.log(`[${NAME}] High tension (score > 0.60)`);
text += `${CONTEXT_HIGH}`;
level = 'high';
} else {
console.log(`[${NAME}] Mid tension (score > ${MID_TENSION_THRESH})`);
text += `${CONTEXT_MID}`;
level = 'mid';
}
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else {
console.log(`[${NAME}] Low tension or no score`);
text += `${CONTEXT_LOW}`;
}
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
console.log(`[${NAME}] DOM elements - box: ${!!box}, p: ${!!p}, h: ${!!h}`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) {
p.textContent = text;
console.log(`[${NAME}] Updated text content (length: ${text.length})`);
}
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Context Summary`;
}
console.log(`[${NAME}] Updated header color: ${iconColor}`);
}
}
function updateReplyPanel(replyId, scoreChange, rawScore, showScores, inputEl) {
let text = showScores ? `Change in craft score after your comment: ${scoreChange.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
text += `${REPLY_HIGH}`;
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else if (rawScore - scoreChange > 0.55 && scoreChange < -SCORE_CHANGE_THRESH) {
text += `${REPLY_LOW}`;
borderColor = '#4caf50';
bgGradient = 'linear-gradient(135deg, #f1f8e9 0%, #f9fbe7 100%)';
iconColor = '#4caf50';
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 6L6.5 11 4 8.5l1-1 1.5 1.5L10.5 5l1 1z"/>
</svg>`;
} else {
text += `${REPLY_MID}`;
}
if (inputEl) {
let threadId = null;
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) {
const muteBtn = replyBox.querySelector('.convowizard-mute-btn');
if (muteBtn) {
threadId = muteBtn.getAttribute('data-thread-id');
}
}
// If no thread ID from panel, try to get from widget
if (!threadId) {
const widgetEl = inputEl.closest('.ext-discussiontools-ui-replyWidget') ||
inputEl.closest('.ext-discussiontools-ui-newTopicWidget');
if (widgetEl) {
threadId = getThreadIdForWidget(widgetEl);
}
}
// If muted, clear background and return
if (threadId && isThreadMuted(threadId)) {
inputEl.style.setProperty('background-color', 'transparent', 'important');
return;
}
let inputBg = 'transparent';
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
inputBg = TENSION_COLORS[level].inputBg;
} else if (scoreChange < -SCORE_CHANGE_THRESH) {
inputBg = 'rgba(76,175,80,0.05)';
}
inputEl.style.setProperty('background-color', inputBg, 'important');
inputEl.style.setProperty('transition', 'background-color 0.3s ease', 'important');
}
const box = document.getElementById(`${replyId}_d`);
const p = document.getElementById(`${replyId}_p`);
const h = document.getElementById(`${replyId}_h`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) p.textContent = text;
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary`;
}
}
}
function getContextStringFor(widgetEl) {
try {
console.log(`[${NAME}] === Extracting context for widget ===`);
// Helper to check if element is a comment container
const isCommentNode = (el) => !!el && (el.tagName === 'DD' || el.tagName === 'LI' || el.tagName === 'P');
// Extract clean comment text, removing nested replies and UI elements
// IMPORTANT: Keep timestamps (UTC) because backend splits on them!
const extractCommentText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
// Remove nested discussion lists, widgets, and UI elements
// NOTE: DO NOT remove .ext-discussiontools-init-timestamplink - backend needs timestamps to split utterances!
clone.querySelectorAll('dl, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, .ext-discussiontools-init-replylink-buttons, [role="button"]').forEach(n => n.remove());
const text = (clone.innerText || '').trim();
return text;
};
// Wikipedia Talk pages have structure: <dd>comment text<dl><dd>reply<dl><dd>widget
// When widget appears, it's nested inside a new <dd> inside a new <dl>
// We need to find the parent <dd> that contains actual comment text
let commentDD = null;
// Strategy 1: Widget is inside <dd> → <dl> → <dd> (the target comment)
// Find the <dd> containing the widget
const widgetDD = widgetEl.closest('dd, li');
console.log(`[${NAME}] Widget container DD:`, widgetDD ? widgetDD.tagName : 'not found');
if (widgetDD) {
// Check if this DD has meaningful text (not just the widget)
const widgetDDText = extractCommentText(widgetDD);
console.log(`[${NAME}] Widget DD text length:`, widgetDDText.length);
if (widgetDDText.length > 20) {
// This DD has actual comment text
commentDD = widgetDD;
} else {
// This DD is empty/minimal - the widget was just inserted
// Go up: DD → DL → parent DD (which should have the comment)
const parentDL = widgetDD.parentElement;
console.log(`[${NAME}] Parent DL:`, parentDL ? parentDL.tagName : 'not found');
if (parentDL && (parentDL.tagName === 'DL' || parentDL.tagName === 'UL' || parentDL.tagName === 'OL')) {
// Find the parent DD/LI/P that contains this DL
commentDD = parentDL.closest('dd, li, p');
console.log(`[${NAME}] Found parent comment node:`, commentDD ? commentDD.tagName : 'not found');
}
}
}
// Strategy 2: Look for preceding paragraph with comment markers (flat structure)
if (!commentDD) {
console.log(`[${NAME}] Trying flat structure search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 10) {
probe = probe.previousElementSibling || (probe.parentElement ? probe.parentElement.previousElementSibling : null);
if (probe && probe.tagName === 'P' && probe.querySelector('[data-mw-comment-start], [data-mw-comment-end]')) {
commentDD = probe;
console.log(`[${NAME}] Found comment via flat search at hop ${hops}`);
break;
}
}
}
// Strategy 3: Last resort - traverse DOM looking for any comment node
if (!commentDD) {
console.log(`[${NAME}] Trying fallback search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 20) {
if (isCommentNode(probe) && extractCommentText(probe).length > 20) {
commentDD = probe;
console.log(`[${NAME}] Found comment via fallback at hop ${hops}`);
break;
}
probe = probe.previousElementSibling ||
(probe.parentElement ? probe.parentElement.previousElementSibling : null) ||
(probe.parentElement ? probe.parentElement.parentElement : null);
}
}
if (!commentDD) {
console.warn(`[${NAME}] Could not find any comment node!`);
return '';
}
// Now traverse up the comment chain collecting all parent comments
const texts = [];
const seen = new Set();
let current = commentDD;
let iterations = 0;
console.log(`[${NAME}] Starting comment chain traversal from:`, current.tagName);
while (current && iterations++ < 20) {
if (seen.has(current)) {
console.log(`[${NAME}] Already seen this node, breaking`);
break;
}
seen.add(current);
const text = extractCommentText(current);
if (text && text.length > 0) {
console.log(`[${NAME}] Extracted comment (${text.length} chars):`, text.substring(0, 100) + '...');
texts.unshift(text); // Add to beginning to maintain chronological order
}
// Navigate up the discussion tree
// Structure: <p>text</p><dl><dd>reply<dl><dd>reply</dd></dl></dd></dl>
// From current <dd>, go to parent element (should be <dl>)
const parentList = current.parentElement;
if (!parentList) {
console.log(`[${NAME}] No parent element, stopping`);
break;
}
console.log(`[${NAME}] Parent list:`, parentList.tagName);
// From <dl>, find the parent <dd> or <li> or <p>
if (parentList.tagName === 'DL' || parentList.tagName === 'UL' || parentList.tagName === 'OL') {
// First try to find an ancestor comment node
current = parentList.closest('dd, li, p');
if (current) {
console.log(`[${NAME}] Found ancestor comment:`, current.tagName);
} else {
// No ancestor found - the <p> might be a SIBLING of this <dl>
// This happens in flat structure: <p>comment</p><dl><dd>reply...
console.log(`[${NAME}] No ancestor, searching for previous sibling of DL...`);
let sibling = parentList.previousElementSibling;
let siblingHops = 0;
while (sibling && siblingHops++ < 10) {
console.log(`[${NAME}] Checking sibling:`, sibling.tagName);
if (isCommentNode(sibling) && !seen.has(sibling)) {
const siblingText = extractCommentText(sibling);
if (siblingText.length > 20) {
current = sibling;
console.log(`[${NAME}] Found sibling comment with text (${siblingText.length} chars)`);
break;
}
}
sibling = sibling.previousElementSibling;
}
if (!current || current === parentList.closest('dd, li, p')) {
console.log(`[${NAME}] No more comments found, stopping`);
break;
}
}
} else if (isCommentNode(parentList)) {
// Parent is itself a comment node
current = parentList;
} else {
// Try to find previous sibling comment
let sibling = current.previousElementSibling;
while (sibling && !isCommentNode(sibling)) {
sibling = sibling.previousElementSibling;
}
if (sibling && !seen.has(sibling)) {
current = sibling;
console.log(`[${NAME}] Moving to sibling:`, current.tagName);
} else {
console.log(`[${NAME}] No more siblings, stopping`);
break;
}
}
}
console.log(`[${NAME}] Extracted ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
// Additional pass: Look for any <p> elements with comment markers that might have been missed
// This catches the case where the first comment in a thread is a <p> that's a sibling of the DL
if (commentDD && commentDD.tagName === 'DD') {
const topDL = commentDD.closest('dl');
if (topDL && topDL.previousElementSibling) {
let probe = topDL.previousElementSibling;
let probeHops = 0;
console.log(`[${NAME}] Scanning backwards from top DL for more context...`);
while (probe && probeHops++ < 5) {
if (probe.tagName === 'P' && !seen.has(probe)) {
const probeText = extractCommentText(probe);
if (probeText.length > 20 && (probe.querySelector('[data-mw-comment-start]') || probe.querySelector('[data-mw-comment-end]'))) {
console.log(`[${NAME}] Found additional preceding comment (${probeText.length} chars):`, probeText.substring(0, 100) + '...');
texts.unshift(probeText);
seen.add(probe);
}
}
probe = probe.previousElementSibling;
}
}
}
console.log(`[${NAME}] Final extraction: ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
if (texts.length > 0) {
const fullContext = texts.join('\n\n');
console.log(`[${NAME}] Full context:`, fullContext);
return fullContext;
}
console.warn(`[${NAME}] No context found, returning empty string`);
return '';
} catch (err) {
console.error(`[${NAME}] Error extracting context:`, err);
return '';
}
}
async function startForWidget(widgetEl) {
if (!TOKEN) { showTokenForm(widgetEl); return; }
if (widgetState.has(widgetEl) || widgetEl.getAttribute('data-convowizard-processing') === 'true') return;
widgetEl.setAttribute('data-convowizard-processing', 'true');
console.log(`[${NAME}] Starting widget processing:`, widgetEl.className);
function findInput(scope) {
// Prefer DT textbox role, then VE contenteditable surface, then textarea
return (
scope.querySelector('div[role="textbox"]') ||
scope.querySelector('[contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface [contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface') ||
scope.querySelector('textarea')
);
}
async function waitForInput(scope, timeoutMs = 8000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tick = () => {
const el = findInput(scope) || findInput(document);
if (el) return resolve(el);
if (Date.now() > deadline) return resolve(null);
setTimeout(tick, 100);
};
tick();
});
}
const inputEl = await waitForInput(widgetEl);
if (!inputEl) {
console.warn(`[${NAME}] No input element found in widget (after waiting)`);
return;
}
console.log(`[${NAME}] Input element found:`, inputEl.tagName, inputEl.getAttribute('role') || (inputEl.getAttribute('contenteditable') ? 'contenteditable' : ''));
const context = getContextStringFor(widgetEl);
console.log(`[${NAME}] Context extracted, length: ${context.length} chars`);
console.log(`[${NAME}] Context preview:`, context.substring(0, 200));
// Split context on (UTC) timestamps to create separate utterances like Reddit does
// This prevents backend caching bug where all Wikipedia convos share id=None
const utterances = context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = utterances.map((text, index) => ({
id: `wiki_${Date.now()}_${index}`, // Unique ID prevents cache collision
text: text.trim()
}));
console.log(`[${NAME}] Created ${existing.length} utterances with unique IDs`);
existing.forEach((utt, i) => {
console.log(`[${NAME}] ${i}: ID=${utt.id}, text=${utt.text.substring(0, 60)}...`);
});
const data = await postJson('start', {
existing, // Send as array like Reddit, not context string!
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
console.log(`[${NAME}] Received response from /start:`, data);
// Handle invalid token - clear and show auth form
if (data && data.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (!data || !data.interaction_id) return;
widgetState.set(widgetEl, { interactionId: data.interaction_id, inputEl, context });
handleIntervention(widgetEl, inputEl, data);
const sendContinue = throttle(async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
// Check if thread is muted
const threadId = getThreadIdForWidget(widgetEl);
if (threadId && isThreadMuted(threadId)) {
// Clear input background if muted
if (st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
return;
}
const widgetId = widgetEl.id;
const h = document.getElementById(`reply_${widgetId}_h`);
if (h) {
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>
`;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span with processing indicator
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary (Processing...)`;
}
}
// Recreate existing array from cached context for continue requests
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`, // Use interaction_id for consistency
text: text.trim()
}));
const data2 = await postJson('continue', {
existing: existingForContinue,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
// Handle invalid token on continue
if (data2 && data2.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
handleIntervention(widgetEl, st.inputEl, data2);
}, 3000);
inputEl.addEventListener('input', sendContinue);
inputEl.addEventListener('keyup', sendContinue);
inputEl.addEventListener('paste', sendContinue);
let postBtn = widgetEl.querySelector(".oo-ui-buttonElement-button[title*='Reply']");
if (!postBtn) {
postBtn = Array.from(widgetEl.querySelectorAll('.oo-ui-buttonElement-button'))
.find(b => /reply/i.test((b.title || b.innerText || '').trim()));
}
if (postBtn) {
postBtn.addEventListener('click', async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
try {
// Recreate existing array from cached context for submit
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForSubmit = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
await postJson('submit', {
existing: existingForSubmit,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
submitted_id: null,
token: TOKEN,
username: USERNAME
});
} finally {
widgetState.delete(widgetEl);
}
});
}
}
function handleIntervention(widgetEl, inputEl, data) {
if (!data) {
console.warn(`[${NAME}] handleIntervention called with no data`);
return;
}
console.log(`[${NAME}] handleIntervention called with data:`, data);
if (data.error) {
return;
}
const interactionId = data.interaction_id;
if (!interactionId) {
console.warn(`[${NAME}] No interaction_id in data`);
return;
}
const threadId = getThreadIdForWidget(widgetEl);
// Skip display if thread is muted
if (threadId && isThreadMuted(threadId)) {
const widgetId = widgetEl.id || `widget_${Date.now()}`;
if (!widgetEl.id) widgetEl.id = widgetId;
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
return;
}
const { replyId, contextId } = ensurePanels(widgetEl, threadId);
const showScores = Object.prototype.hasOwnProperty.call(data, 'show_scores');
console.log(`[${NAME}] Panel IDs - context: ${contextId}, reply: ${replyId}`);
console.log(`[${NAME}] Show scores: ${showScores}`);
const which = (data.which || '').substring(0, 5);
console.log(`[${NAME}] Response type (which): ${which}`);
if (which === 'craft') {
console.log(`[${NAME}] CRAFT mode - craft_ctx_score:`, data.craft_ctx_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_score:`, data.craft_reply_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_change:`, data.craft_reply_change);
if (data.craft_ctx_score != null) {
console.log(`[${NAME}] Updating context panel with score: ${data.craft_ctx_score}`);
updateContextPanel(contextId, data.craft_ctx_score, showScores);
} else {
console.log(`[${NAME}] No context score, updating context panel with null`);
updateContextPanel(contextId, null, showScores);
}
if (data.craft_reply_change != null && data.craft_reply_score != null) {
console.log(`[${NAME}] Updating reply panel with change: ${data.craft_reply_change}, score: ${data.craft_reply_score}`);
updateReplyPanel(replyId, data.craft_reply_change, data.craft_reply_score, showScores, inputEl);
} else {
console.log(`[${NAME}] No reply scores, updating reply panel with zeros`);
updateReplyPanel(replyId, 0, 0, showScores, inputEl);
}
} else {
const noteId = `passivenote${interactionId}_d`;
if (!data.message && !document.getElementById(noteId)) {
const box = document.createElement('div');
box.id = noteId;
box.className = 'passiveModeDisplay';
box.style.cssText = `
margin:8px 0;padding:8px 12px;border-radius:6px;border:1px solid #a2a9b1;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;opacity:.8;
`;
const content = document.createElement('div');
content.id = `passivenote${interactionId}_p`;
content.style.cssText = `font-size:12px;color:#54595d;margin:0;font-style:italic;`;
content.textContent = `${NAME} is currently not active on this thread.`;
box.appendChild(content);
widgetEl.prepend(box);
}
}
}
function installClickDelegation() {
document.addEventListener('click', (e) => {
console.log(`[${NAME}] Click detected on:`, e.target.tagName, e.target.className);
// Match reply buttons and links (both old and new DiscussionTools formats)
// Need to check the actual clicked element AND traverse up
const link = e.target.closest && (
e.target.closest('.ext-discussiontools-init-replybutton') ||
e.target.closest('.ext-discussiontools-init-replylink-reply') ||
e.target.closest('a.ext-discussiontools-init-replylink') ||
e.target.closest('.ext-discussiontools-init-replylink-buttons') ||
e.target.closest('[class*="ext-discussiontools-init-"][class*="reply"]')
);
if (!link) return;
console.log(`[${NAME}] Reply link clicked:`, link.className);
// For flat discussion structures (like Research talk pages), the widget may be inserted
// outside the immediate parent. Search more broadly.
const anchorScope = link.closest('dd, li, p, div, section') || document.body;
const broadScope = anchorScope.closest('#mw-content-text, .mw-parser-output, #content') || document.body;
console.log(`[${NAME}] Anchor scope:`, anchorScope.tagName, anchorScope.className);
console.log(`[${NAME}] Broad scope:`, broadScope.tagName, broadScope.className);
const timeoutAt = Date.now() + 8000;
let attempts = 0;
const poll = setInterval(() => {
attempts++;
// Try multiple search strategies:
// 1. In immediate anchor scope (for nested structures like dd/li)
// 2. As sibling to anchor scope (for flat structures like p)
// 3. In broader content scope
// 4. Document-wide fallback
const widget = anchorScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| anchorScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| (anchorScope.nextElementSibling && (
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-replyWidget') ||
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-newTopicWidget')
) ? anchorScope.nextElementSibling : null)
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-replyWidget:last-child'))
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-newTopicWidget:last-child'))
|| broadScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| broadScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| document.querySelector('.ext-discussiontools-ui-replyWidget')
|| document.querySelector('.ext-discussiontools-ui-newTopicWidget');
if (widget) {
console.log(`[${NAME}] Widget found after ${attempts} attempts:`, widget.className);
clearInterval(poll);
startForWidget(widget);
}
else if (Date.now() > timeoutAt) {
console.log(`[${NAME}] Widget timeout after ${attempts} attempts - no widget found`);
console.log(`[${NAME}] Searched in anchorScope:`, anchorScope);
console.log(`[${NAME}] Searched in broadScope:`, broadScope);
clearInterval(poll);
} else if (attempts % 10 === 0) {
console.log(`[${NAME}] Still searching for widget, attempt ${attempts}...`);
}
}, 100);
});
}
function installWidgetObserver() {
const root = document.getElementById('content') || document.body;
console.log(`[${NAME}] Setting up mutation observer on:`, root.id || root.tagName);
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof HTMLElement)) continue;
// Log all significant additions for debugging
if (n.className && typeof n.className === 'string' && n.className.includes('ext-discussiontools')) {
console.log(`[${NAME}] DOM addition detected:`, n.tagName, n.className);
}
// Match specific DiscussionTools widgets
if (n.matches('.ext-discussiontools-ui-replyWidget') || n.matches('.ext-discussiontools-ui-newTopicWidget')) {
console.log(`[${NAME}] *** Widget detected via observer ***:`, n.className);
startForWidget(n);
} else {
const nested = n.querySelector && (
n.querySelector('.ext-discussiontools-ui-replyWidget') ||
n.querySelector('.ext-discussiontools-ui-newTopicWidget')
);
if (nested) {
console.log(`[${NAME}] *** Nested widget detected ***:`, nested.className);
startForWidget(nested);
}
}
}
}
});
obs.observe(root, { childList: true, subtree: true });
console.log(`[${NAME}] Mutation observer is now watching for widget creation`);
// Check for existing widgets on page load
const existingWidgets = document.querySelectorAll('.ext-discussiontools-ui-replyWidget, .ext-discussiontools-ui-newTopicWidget');
if (existingWidgets.length > 0) {
console.log(`[${NAME}] Found ${existingWidgets.length} existing widget(s) on page load`);
existingWidgets.forEach(w => {
console.log(`[${NAME}] Processing existing widget:`, w.className);
startForWidget(w);
});
} else {
console.log(`[${NAME}] No existing widgets found on page load`);
}
}
// kick off
installClickDelegation();
installWidgetObserver();
console.log(`[${NAME}] === INITIALIZATION COMPLETE ===`);
console.log(`[${NAME}] Click delegation: ACTIVE`);
console.log(`[${NAME}] Mutation observer: ACTIVE`);
console.log(`[${NAME}] Waiting for user interaction...`);
})();
//</nowiki>
kja8tstbas61fu3su10xu48a30ozl6v
740052
740051
2026-05-01T17:51:17Z
Laerdon
70887
740052
javascript
text/javascript
//<nowiki>
/**
* Filename: wiki-talk-page-script.js
* ConvoWizard – Real-time Reply Feedback (Wikipedia userscript)
* Runs on Talk pages inside DiscussionTools reply widgets.
* Prompts for a token on first run; persists after authorization.
*/
(async function () {
'use strict';
// Load core util module via ResourceLoader
await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api']);
// === CONFIG ===
const SERVER = 'https://convocompass.toolforge.org/api';
// ---- UI strings and thresholds ----
const NAME = 'ConvoWizard';
const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search);
const OPTION_KEY = 'userjs-convowizard-token';
const SCORE_CHANGE_THRESH = 0.08;
const MID_TENSION_THRESH = 0.50;
const HIGH_TENSION_THRESH = 0.75;
const CONTEXT_LOW = `${NAME} will notify you here if it detects anything in the preceding conversation.`;
const CONTEXT_MID = `${NAME} thinks this discussion is getting somewhat tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_HIGH = `${NAME} thinks this discussion is getting tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_VERY_HIGH = `${NAME} thinks this discussion is getting very tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const REPLY_LOW = `${NAME} thinks this comment might decrease tension in this discussion.`;
const REPLY_MID = `${NAME} will notify you here if it detects anything in your comment draft.`;
const REPLY_HIGH = `${NAME} thinks this comment might increase the tension in this discussion. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const TENSION_COLORS = {
mid: { border: '#e8825c', bg: 'linear-gradient(135deg, #fff0eb 0%, #fff8f5 100%)', icon: '#e8825c', inputBg: 'rgba(232,130,92,0.05)' },
high: { border: '#d73027', bg: 'linear-gradient(135deg, #ffeaea 0%, #fff5f5 100%)', icon: '#d73027', inputBg: 'rgba(215,48,39,0.05)' },
veryHigh: { border: '#a50f15', bg: 'linear-gradient(135deg, #fdd 0%, #fee 100%)', icon: '#a50f15', inputBg: 'rgba(165,15,21,0.07)' },
};
// Keep ConvoWizard logs quiet by default to reduce console noise on Wikimedia pages.
const nativeConsole = window.console;
const console = {
log: (...args) => {
if (!DEBUG && typeof args[0] === 'string' && args[0].startsWith(`[${NAME}]`)) {
return;
}
nativeConsole.log(...args);
},
warn: (...args) => nativeConsole.warn(...args),
error: (...args) => nativeConsole.error(...args)
};
const isWikipediaHost = /(^|\.)wikipedia\.org$/.test(location.hostname);
const namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
const isTalkNamespace = Number.isFinite(namespaceNumber) && namespaceNumber % 2 === 1;
const isViewAction = mw.config.get('wgAction') === 'view';
// Gadget scope guard: only run on Wikipedia talk pages in normal view mode.
if (!isWikipediaHost || !isTalkNamespace || !isViewAction) {
console.log(
`[${NAME}] Skipping page (host=${location.hostname}, ns=${namespaceNumber}, action=${mw.config.get('wgAction')})`
);
return;
}
// ---------- helpers ----------
function throttle(fn, waitMs) {
let last = 0, timer = 0, pendingArgs = null;
return (...args) => {
const now = Date.now();
const remaining = waitMs - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
} else {
pendingArgs = args;
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn(...pendingArgs); }, remaining);
}
};
}
// Rewrite Wikipedia URL so the reddit-style backend can extract a post_id.
// Backend does: url.split("comments/")[1][:6]
// We inject "comments/<hash>" where hash is derived from the wiki page path.
function wikiUrlToPostId(href) {
try {
const pagePath = href.split('/wiki/')[1] || href;
// Simple hash to produce a stable 6-char hex id per page
let h = 0;
for (let i = 0; i < pagePath.length; i++) {
h = ((h << 5) - h) + pagePath.charCodeAt(i);
h = h & h;
}
const hex = Math.abs(h).toString(16).padStart(6, '0').slice(0, 6);
return href.split('/wiki/')[0] + '/comments/' + hex;
} catch {
return href;
}
}
async function postJson(route, body) {
try {
const res = await fetch(SERVER + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
nativeConsole.log(`[${NAME}] response from ${route}: status=${res.status}`, text);
try {
const data = JSON.parse(text);
nativeConsole.log(`[${NAME}] json from ${route}:`, data);
return data;
} catch (err) {
console.warn(`[${NAME}] non-json response from ${route}:`, text);
return {};
}
} catch (err) {
console.warn(`[${NAME}] request failed for ${route}:`, err);
return {};
}
}
function getTokenFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(OPTION_KEY);
if (typeof raw === 'string') {
const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
}
}
} catch (err) {
console.warn(`[${NAME}] Failed to read token from user options`, err);
}
return null;
}
async function persistToken(storageKey, token) {
if (!token) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, token);
}
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(OPTION_KEY, token || '');
await new mw.Api().saveOption(OPTION_KEY, token || '');
console.log(`[${NAME}] ${token ? 'Saved' : 'Cleared'} token in user options (notepad)`);
} catch (err) {
console.warn(`[${NAME}] Could not persist token to user options`, err);
}
}
async function validateToken(token, username) {
try {
const response = await postJson('claim_token', { token, username });
return response.valid === true;
} catch {
return false;
}
}
// ========== CREDENTIAL SETUP ==========
const USERNAME = mw.config.get('wgUserName');
if (!USERNAME) {
console.log(`[${NAME}] Not logged in, skipping`);
return;
}
let TOKEN = null;
const STORAGE_KEY = `ConvoWizard:token:${USERNAME}`;
// Load persisted token (options → localStorage), validate, sync stores
async function loadPersistedToken() {
let token = getTokenFromOptions();
let source = 'options';
if (!token) {
token = localStorage.getItem(STORAGE_KEY);
source = 'localStorage';
}
if (token && typeof token === 'string') {
token = token.trim();
}
if (!token) return;
const valid = await validateToken(token, USERNAME);
if (valid) {
TOKEN = token;
// Sync both stores
if (source === 'options') {
localStorage.setItem(STORAGE_KEY, token);
} else {
await persistToken(STORAGE_KEY, token);
}
console.log(`[${NAME}] Token loaded from ${source}`);
} else {
console.log(`[${NAME}] Persisted token invalid, clearing`);
await persistToken(STORAGE_KEY, null);
}
}
await loadPersistedToken();
console.log(`[${NAME}] === SCRIPT LOADED SUCCESSFULLY ===`);
console.log(`[${NAME}] Server: ${SERVER}`);
console.log(`[${NAME}] User: ${USERNAME}`);
console.log(`[${NAME}] Token: ${TOKEN ? 'present' : 'none (will prompt)'}`);
console.log(`[${NAME}] Page URL: ${location.href}`);
console.log(`[${NAME}] Installing click and mutation observers...`);
// ========== TOKEN INPUT FORM ==========
function showTokenForm(widgetEl) {
// Don't add duplicate forms
if (widgetEl.querySelector('.convowizard-auth-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-auth-form';
form.style.cssText = `
margin:8px 0;padding:16px 20px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
const title = document.createElement('div');
title.style.cssText = 'font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;';
title.textContent = `${NAME}: Authorization Required`;
const desc = document.createElement('div');
desc.style.cssText = 'font-size:13px;color:#333;margin-bottom:12px;line-height:1.4;';
desc.textContent = 'Please paste your ConvoWizard token below to activate the extension.';
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;align-items:center;';
const input = document.createElement('input');
input.type = 'password';
input.placeholder = 'Paste token here';
input.autocomplete = 'off';
input.spellcheck = false;
input.style.cssText = `
flex:1;padding:8px 10px;font-size:13px;border:1px solid #a2a9b1;border-radius:4px;
font-family:monospace;outline:none;transition:border-color 0.2s ease;
`;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Authorize';
btn.style.cssText = `
padding:8px 16px;font-size:13px;font-weight:600;border:none;border-radius:4px;
background:#0645ad;color:#fff;cursor:pointer;transition:background 0.2s ease;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
btn.addEventListener('mouseenter', () => { btn.style.background = '#0b5cce'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#0645ad'; });
const errDiv = document.createElement('div');
errDiv.style.cssText = 'font-size:12px;color:#d73027;margin-top:8px;display:none;';
async function onAuthorize() {
const raw = input.value.trim();
// Input validation: 33 hex chars
if (!/^[0-9a-f]{33}$/i.test(raw)) {
errDiv.textContent = 'Invalid token format. A token is 33 hexadecimal characters.';
errDiv.style.display = 'block';
input.value = '';
input.focus();
return;
}
btn.disabled = true;
btn.textContent = 'Validating...';
errDiv.style.display = 'none';
const valid = await validateToken(raw, USERNAME);
// Clear input immediately after use
input.value = '';
if (valid) {
TOKEN = raw;
await persistToken(STORAGE_KEY, TOKEN);
// Remove all auth forms on page
document.querySelectorAll('.convowizard-auth-form').forEach(f => f.remove());
// Activate the widget
startForWidget(widgetEl);
} else {
errDiv.textContent = 'Token is invalid or expired. Please check and try again.';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Authorize';
input.focus();
}
}
btn.addEventListener('click', onAuthorize);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); onAuthorize(); }
});
row.appendChild(input);
row.appendChild(btn);
form.appendChild(title);
form.appendChild(desc);
form.appendChild(row);
form.appendChild(errDiv);
widgetEl.prepend(form);
input.focus();
}
// ---- state + logs ----
const widgetState = new WeakMap();
function cleanupDuplicatePanels() {
const allPanels = document.querySelectorAll('.craftDisplay, .passiveModeDisplay');
const seen = new Set();
allPanels.forEach(panel => {
const pid = panel.id;
if (pid && pid.includes('_')) {
const parts = pid.split('_');
const type = parts[0];
const wid = parts.slice(1).join('_').replace('_d', '');
const key = `${type}|${wid}`;
if (seen.has(key)) panel.remove(); else seen.add(key);
}
});
}
// ========== MUTE THREAD ==========
/**
* Generates a unique thread identifier by combining URL section, DOM position, and content hash.
* Ensures each thread on a page gets a distinct ID for per-thread mute functionality.
*/
function getThreadIdForWidget(widgetEl) {
try {
const urlHash = location.hash ? location.hash.replace('#', '') : 'root';
// Generate DOM path for widget position
const widgetPath = [];
let el = widgetEl;
for (let i = 0; i < 8 && el; i++) {
const parent = el.parentElement;
if (!parent || parent === document.body) break;
const siblings = Array.from(parent.children);
const index = siblings.indexOf(el);
widgetPath.unshift(`${parent.tagName}_${index}`);
el = parent;
}
const domPath = widgetPath.length > 0 ? widgetPath.join('_') : 'unknown';
// Get content-based hash from first comment
const context = getContextStringFor(widgetEl);
let contentHash = '';
if (context && context.length > 0) {
const firstComment = context.split('\n\n')[0] || context.substring(0, 150);
let hash = 0;
for (let i = 0; i < Math.min(firstComment.length, 150); i++) {
const char = firstComment.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
const parentText = widgetEl.parentElement ?
(widgetEl.parentElement.textContent || '').substring(0, 50).trim() : '';
if (parentText) {
let hash = 0;
for (let i = 0; i < parentText.length; i++) {
hash = ((hash << 5) - hash) + parentText.charCodeAt(i);
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
contentHash = Math.random().toString(36).slice(2, 8);
}
}
return `thread_${urlHash}_${domPath.substring(0, 50)}_${contentHash}`;
} catch (err) {
console.warn(`[${NAME}] Error generating thread ID:`, err);
return `thread_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}
}
function getMutedThreads() {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const stored = localStorage.getItem(storageKey);
if (!stored) return new Set();
const threads = JSON.parse(stored);
return new Set(Array.isArray(threads) ? threads : []);
} catch {
return new Set();
}
}
function saveMutedThreads(threadSet) {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const threads = Array.from(threadSet);
localStorage.setItem(storageKey, JSON.stringify(threads));
} catch (err) {
console.warn(`[${NAME}] Error saving muted threads:`, err);
}
}
function isThreadMuted(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
return muted.has(threadId);
}
function toggleThreadMute(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
const wasMuted = muted.has(threadId);
if (wasMuted) {
muted.delete(threadId);
} else {
muted.add(threadId);
}
saveMutedThreads(muted);
return !wasMuted;
}
/**
* Creates an unmute indicator button that appears when a thread is muted
* Allows users to restore ConvoWizard for a muted thread
*/
function ensureUnmuteIndicator(widgetEl, threadId, widgetId) {
const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator');
if (existingIndicator) return;
const indicator = document.createElement('div');
indicator.id = `convowizard-unmute-${widgetId}`;
indicator.className = 'convowizard-unmute-indicator';
indicator.style.cssText = `
margin: 4px 0;
padding: 6px 10px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: #f8f9fa;
color: #54595d;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s ease;
`;
const icon = document.createElement('span');
icon.textContent = '🔊';
icon.style.fontSize = '12px';
const text = document.createElement('span');
text.textContent = `${NAME} is muted for this thread. Click to unmute.`;
text.style.flex = '1';
indicator.appendChild(icon);
indicator.appendChild(text);
indicator.addEventListener('mouseenter', () => {
indicator.style.background = '#e9ecef';
indicator.style.borderColor = '#0645ad';
});
indicator.addEventListener('mouseleave', () => {
indicator.style.background = '#f8f9fa';
indicator.style.borderColor = '#a2a9b1';
});
indicator.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Show panels and remove indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
if (contextPanel) contextPanel.style.display = '';
if (replyPanel) replyPanel.style.display = '';
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
// Update mute buttons
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(btn => {
btn.textContent = '🔇 Mute thread';
btn.title = 'Mute ConvoWizard for this thread';
btn.style.background = '#ffffff';
});
// Re-trigger analysis to refresh display
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
// Re-run the continue request to refresh the display
const inputText = st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '';
if (inputText.trim().length > 0) {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('continue', {
existing: existingForContinue,
new: inputText,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
if (data && !data.error) {
handleIntervention(widgetEl, st.inputEl, data);
}
} else {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('start', {
existing,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
if (data && !data.error && data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
handleIntervention(widgetEl, st.inputEl, data);
}
}
} else {
startForWidget(widgetEl);
}
});
// Insert at the beginning of the widget
widgetEl.insertBefore(indicator, widgetEl.firstChild);
}
function createMuteButton(threadId, widgetId, isMuted) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-mute-btn';
btn.setAttribute('data-thread-id', threadId);
btn.setAttribute('data-widget-id', widgetId);
btn.style.cssText = `
margin-left: auto;
padding: 4px 8px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: ${isMuted ? '#f8f9fa' : '#ffffff'};
color: #54595d;
cursor: pointer;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
btn.textContent = isMuted
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = isMuted
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.addEventListener('mouseenter', () => {
btn.style.background = isMuted ? '#e9ecef' : '#f8f9fa';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = isMuted ? '#f8f9fa' : '#ffffff';
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Update button state
btn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
// Hide/show panels and unmute indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
const widgetEl = contextPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
contextPanel?.closest('.ext-discussiontools-ui-newTopicWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-newTopicWidget');
if (contextPanel) contextPanel.style.display = newMuteState ? 'none' : '';
if (replyPanel) replyPanel.style.display = newMuteState ? 'none' : '';
// Clear textbox background when muting
if (newMuteState) {
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
}
if (newMuteState && widgetEl) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (widgetEl) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Update all mute buttons for this thread
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(otherBtn => {
if (otherBtn !== btn) {
otherBtn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
otherBtn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
otherBtn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
}
});
});
return btn;
}
function ensurePanels(widgetEl, threadId = null) {
cleanupDuplicatePanels();
// Clean up duplicate unmute indicators
const unmuteIndicators = widgetEl.querySelectorAll('.convowizard-unmute-indicator');
if (unmuteIndicators.length > 1) {
for (let i = 1; i < unmuteIndicators.length; i++) {
unmuteIndicators[i].remove();
}
}
const widgetId = widgetEl.id || `widget_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
if (!widgetEl.id) widgetEl.id = widgetId;
const contextId = `context_${widgetId}`;
const replyId = `reply_${widgetId}`;
const isMuted = threadId ? isThreadMuted(threadId) : false;
// Ensure Context panel
if (!document.getElementById(`${contextId}_d`)) {
const contextBox = document.createElement('div');
contextBox.id = `${contextId}_d`;
contextBox.className = 'craftDisplay';
contextBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const cheader = document.createElement('div');
cheader.id = `${contextId}_h`;
cheader.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Context Summary`;
cheader.appendChild(iconSvg);
cheader.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
cheader.appendChild(muteBtn);
}
const ccontent = document.createElement('div');
ccontent.id = `${contextId}_p`;
ccontent.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
ccontent.textContent = `When you type a reply ${NAME} will give you some feedback on the discussion context.`;
contextBox.appendChild(cheader);
contextBox.appendChild(ccontent);
if (isMuted) contextBox.style.display = 'none';
widgetEl.prepend(contextBox);
} else if (threadId) {
const existingPanel = document.getElementById(`${contextId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
// Show unmute indicator when muted (only if panels don't exist yet)
if (threadId && isMuted && !document.getElementById(`${contextId}_d`) && !document.getElementById(`${replyId}_d`)) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (threadId && !isMuted) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Ensure Reply panel
if (!document.getElementById(`${replyId}_d`)) {
const replyBox = document.createElement('div');
replyBox.id = `${replyId}_d`;
replyBox.className = 'craftDisplay';
replyBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const header = document.createElement('div');
header.id = `${replyId}_h`;
header.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Reply Summary`;
header.appendChild(iconSvg);
header.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
header.appendChild(muteBtn);
}
const content = document.createElement('div');
content.id = `${replyId}_p`;
content.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
content.textContent = `When you type a reply ${NAME} will give you some feedback on your comment.`;
replyBox.appendChild(header);
replyBox.appendChild(content);
if (isMuted) replyBox.style.display = 'none';
const preview = widgetEl.querySelector('.ext-discussiontools-ui-replyWidget-preview')
|| document.querySelector('.ext-discussiontools-ui-replyWidget-preview');
if (preview && preview.parentElement) {
preview.parentElement.insertBefore(replyBox, preview.nextSibling);
} else {
widgetEl.append(replyBox);
}
} else if (threadId) {
const existingPanel = document.getElementById(`${replyId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
return { replyId, contextId };
}
function updateContextPanel(contextId, score, showScores) {
console.log(`[${NAME}] updateContextPanel called - contextId: ${contextId}, score: ${score}, showScores: ${showScores}`);
const hasScore = typeof score === 'number';
let text = showScores && hasScore ? `Context craft score: ${score.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
console.log(`[${NAME}] hasScore: ${hasScore}, score value: ${score}`);
console.log(`[${NAME}] Thresholds - MID: ${MID_TENSION_THRESH}, HIGH: ${HIGH_TENSION_THRESH}`);
if (hasScore && score > MID_TENSION_THRESH) {
let level;
if (score > HIGH_TENSION_THRESH) {
console.log(`[${NAME}] Very high tension (score > ${HIGH_TENSION_THRESH})`);
text += `${CONTEXT_VERY_HIGH}`;
level = 'veryHigh';
} else if (score > 0.60) {
console.log(`[${NAME}] High tension (score > 0.60)`);
text += `${CONTEXT_HIGH}`;
level = 'high';
} else {
console.log(`[${NAME}] Mid tension (score > ${MID_TENSION_THRESH})`);
text += `${CONTEXT_MID}`;
level = 'mid';
}
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else {
console.log(`[${NAME}] Low tension or no score`);
text += `${CONTEXT_LOW}`;
}
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
console.log(`[${NAME}] DOM elements - box: ${!!box}, p: ${!!p}, h: ${!!h}`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) {
p.textContent = text;
console.log(`[${NAME}] Updated text content (length: ${text.length})`);
}
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Context Summary`;
}
console.log(`[${NAME}] Updated header color: ${iconColor}`);
}
}
function updateReplyPanel(replyId, scoreChange, rawScore, showScores, inputEl) {
let text = showScores ? `Change in craft score after your comment: ${scoreChange.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
text += `${REPLY_HIGH}`;
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else if (rawScore - scoreChange > 0.55 && scoreChange < -SCORE_CHANGE_THRESH) {
text += `${REPLY_LOW}`;
borderColor = '#4caf50';
bgGradient = 'linear-gradient(135deg, #f1f8e9 0%, #f9fbe7 100%)';
iconColor = '#4caf50';
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 6L6.5 11 4 8.5l1-1 1.5 1.5L10.5 5l1 1z"/>
</svg>`;
} else {
text += `${REPLY_MID}`;
}
if (inputEl) {
let threadId = null;
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) {
const muteBtn = replyBox.querySelector('.convowizard-mute-btn');
if (muteBtn) {
threadId = muteBtn.getAttribute('data-thread-id');
}
}
// If no thread ID from panel, try to get from widget
if (!threadId) {
const widgetEl = inputEl.closest('.ext-discussiontools-ui-replyWidget') ||
inputEl.closest('.ext-discussiontools-ui-newTopicWidget');
if (widgetEl) {
threadId = getThreadIdForWidget(widgetEl);
}
}
// If muted, clear background and return
if (threadId && isThreadMuted(threadId)) {
inputEl.style.setProperty('background-color', 'transparent', 'important');
return;
}
let inputBg = 'transparent';
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
inputBg = TENSION_COLORS[level].inputBg;
} else if (scoreChange < -SCORE_CHANGE_THRESH) {
inputBg = 'rgba(76,175,80,0.05)';
}
inputEl.style.setProperty('background-color', inputBg, 'important');
inputEl.style.setProperty('transition', 'background-color 0.3s ease', 'important');
}
const box = document.getElementById(`${replyId}_d`);
const p = document.getElementById(`${replyId}_p`);
const h = document.getElementById(`${replyId}_h`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) p.textContent = text;
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary`;
}
}
}
function getContextStringFor(widgetEl) {
try {
console.log(`[${NAME}] === Extracting context for widget ===`);
// Helper to check if element is a comment container
const isCommentNode = (el) => !!el && (el.tagName === 'DD' || el.tagName === 'LI' || el.tagName === 'P');
// Extract clean comment text, removing nested replies and UI elements
// IMPORTANT: Keep timestamps (UTC) because backend splits on them!
const extractCommentText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
// Remove nested discussion lists, widgets, and UI elements
// NOTE: DO NOT remove .ext-discussiontools-init-timestamplink - backend needs timestamps to split utterances!
clone.querySelectorAll('dl, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, .ext-discussiontools-init-replylink-buttons, [role="button"]').forEach(n => n.remove());
const text = (clone.innerText || '').trim();
return text;
};
// Wikipedia Talk pages have structure: <dd>comment text<dl><dd>reply<dl><dd>widget
// When widget appears, it's nested inside a new <dd> inside a new <dl>
// We need to find the parent <dd> that contains actual comment text
let commentDD = null;
// Strategy 1: Widget is inside <dd> → <dl> → <dd> (the target comment)
// Find the <dd> containing the widget
const widgetDD = widgetEl.closest('dd, li');
console.log(`[${NAME}] Widget container DD:`, widgetDD ? widgetDD.tagName : 'not found');
if (widgetDD) {
// Check if this DD has meaningful text (not just the widget)
const widgetDDText = extractCommentText(widgetDD);
console.log(`[${NAME}] Widget DD text length:`, widgetDDText.length);
if (widgetDDText.length > 20) {
// This DD has actual comment text
commentDD = widgetDD;
} else {
// This DD is empty/minimal - the widget was just inserted
// Go up: DD → DL → parent DD (which should have the comment)
const parentDL = widgetDD.parentElement;
console.log(`[${NAME}] Parent DL:`, parentDL ? parentDL.tagName : 'not found');
if (parentDL && (parentDL.tagName === 'DL' || parentDL.tagName === 'UL' || parentDL.tagName === 'OL')) {
// Find the parent DD/LI/P that contains this DL
commentDD = parentDL.closest('dd, li, p');
console.log(`[${NAME}] Found parent comment node:`, commentDD ? commentDD.tagName : 'not found');
}
}
}
// Strategy 2: Look for preceding paragraph with comment markers (flat structure)
if (!commentDD) {
console.log(`[${NAME}] Trying flat structure search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 10) {
probe = probe.previousElementSibling || (probe.parentElement ? probe.parentElement.previousElementSibling : null);
if (probe && probe.tagName === 'P' && probe.querySelector('[data-mw-comment-start], [data-mw-comment-end]')) {
commentDD = probe;
console.log(`[${NAME}] Found comment via flat search at hop ${hops}`);
break;
}
}
}
// Strategy 3: Last resort - traverse DOM looking for any comment node
if (!commentDD) {
console.log(`[${NAME}] Trying fallback search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 20) {
if (isCommentNode(probe) && extractCommentText(probe).length > 20) {
commentDD = probe;
console.log(`[${NAME}] Found comment via fallback at hop ${hops}`);
break;
}
probe = probe.previousElementSibling ||
(probe.parentElement ? probe.parentElement.previousElementSibling : null) ||
(probe.parentElement ? probe.parentElement.parentElement : null);
}
}
if (!commentDD) {
console.warn(`[${NAME}] Could not find any comment node!`);
return '';
}
// Now traverse up the comment chain collecting all parent comments
const texts = [];
const seen = new Set();
let current = commentDD;
let iterations = 0;
console.log(`[${NAME}] Starting comment chain traversal from:`, current.tagName);
while (current && iterations++ < 20) {
if (seen.has(current)) {
console.log(`[${NAME}] Already seen this node, breaking`);
break;
}
seen.add(current);
const text = extractCommentText(current);
if (text && text.length > 0) {
console.log(`[${NAME}] Extracted comment (${text.length} chars):`, text.substring(0, 100) + '...');
texts.unshift(text); // Add to beginning to maintain chronological order
}
// Navigate up the discussion tree
// Structure: <p>text</p><dl><dd>reply<dl><dd>reply</dd></dl></dd></dl>
// From current <dd>, go to parent element (should be <dl>)
const parentList = current.parentElement;
if (!parentList) {
console.log(`[${NAME}] No parent element, stopping`);
break;
}
console.log(`[${NAME}] Parent list:`, parentList.tagName);
// From <dl>, find the parent <dd> or <li> or <p>
if (parentList.tagName === 'DL' || parentList.tagName === 'UL' || parentList.tagName === 'OL') {
// First try to find an ancestor comment node
current = parentList.closest('dd, li, p');
if (current) {
console.log(`[${NAME}] Found ancestor comment:`, current.tagName);
} else {
// No ancestor found - the <p> might be a SIBLING of this <dl>
// This happens in flat structure: <p>comment</p><dl><dd>reply...
console.log(`[${NAME}] No ancestor, searching for previous sibling of DL...`);
let sibling = parentList.previousElementSibling;
let siblingHops = 0;
while (sibling && siblingHops++ < 10) {
console.log(`[${NAME}] Checking sibling:`, sibling.tagName);
if (isCommentNode(sibling) && !seen.has(sibling)) {
const siblingText = extractCommentText(sibling);
if (siblingText.length > 20) {
current = sibling;
console.log(`[${NAME}] Found sibling comment with text (${siblingText.length} chars)`);
break;
}
}
sibling = sibling.previousElementSibling;
}
if (!current || current === parentList.closest('dd, li, p')) {
console.log(`[${NAME}] No more comments found, stopping`);
break;
}
}
} else if (isCommentNode(parentList)) {
// Parent is itself a comment node
current = parentList;
} else {
// Try to find previous sibling comment
let sibling = current.previousElementSibling;
while (sibling && !isCommentNode(sibling)) {
sibling = sibling.previousElementSibling;
}
if (sibling && !seen.has(sibling)) {
current = sibling;
console.log(`[${NAME}] Moving to sibling:`, current.tagName);
} else {
console.log(`[${NAME}] No more siblings, stopping`);
break;
}
}
}
console.log(`[${NAME}] Extracted ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
// Additional pass: Look for any <p> elements with comment markers that might have been missed
// This catches the case where the first comment in a thread is a <p> that's a sibling of the DL
if (commentDD && commentDD.tagName === 'DD') {
const topDL = commentDD.closest('dl');
if (topDL && topDL.previousElementSibling) {
let probe = topDL.previousElementSibling;
let probeHops = 0;
console.log(`[${NAME}] Scanning backwards from top DL for more context...`);
while (probe && probeHops++ < 5) {
if (probe.tagName === 'P' && !seen.has(probe)) {
const probeText = extractCommentText(probe);
if (probeText.length > 20 && (probe.querySelector('[data-mw-comment-start]') || probe.querySelector('[data-mw-comment-end]'))) {
console.log(`[${NAME}] Found additional preceding comment (${probeText.length} chars):`, probeText.substring(0, 100) + '...');
texts.unshift(probeText);
seen.add(probe);
}
}
probe = probe.previousElementSibling;
}
}
}
console.log(`[${NAME}] Final extraction: ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
if (texts.length > 0) {
const fullContext = texts.join('\n\n');
console.log(`[${NAME}] Full context:`, fullContext);
return fullContext;
}
console.warn(`[${NAME}] No context found, returning empty string`);
return '';
} catch (err) {
console.error(`[${NAME}] Error extracting context:`, err);
return '';
}
}
async function startForWidget(widgetEl) {
if (!TOKEN) { showTokenForm(widgetEl); return; }
if (widgetState.has(widgetEl) || widgetEl.getAttribute('data-convowizard-processing') === 'true') return;
widgetEl.setAttribute('data-convowizard-processing', 'true');
console.log(`[${NAME}] Starting widget processing:`, widgetEl.className);
function findInput(scope) {
// Prefer DT textbox role, then VE contenteditable surface, then textarea
return (
scope.querySelector('div[role="textbox"]') ||
scope.querySelector('[contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface [contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface') ||
scope.querySelector('textarea')
);
}
async function waitForInput(scope, timeoutMs = 8000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tick = () => {
const el = findInput(scope) || findInput(document);
if (el) return resolve(el);
if (Date.now() > deadline) return resolve(null);
setTimeout(tick, 100);
};
tick();
});
}
const inputEl = await waitForInput(widgetEl);
if (!inputEl) {
console.warn(`[${NAME}] No input element found in widget (after waiting)`);
return;
}
console.log(`[${NAME}] Input element found:`, inputEl.tagName, inputEl.getAttribute('role') || (inputEl.getAttribute('contenteditable') ? 'contenteditable' : ''));
const context = getContextStringFor(widgetEl);
console.log(`[${NAME}] Context extracted, length: ${context.length} chars`);
console.log(`[${NAME}] Context preview:`, context.substring(0, 200));
// Split context on (UTC) timestamps to create separate utterances like Reddit does
// This prevents backend caching bug where all Wikipedia convos share id=None
const utterances = context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = utterances.map((text, index) => ({
id: `wiki_${Date.now()}_${index}`, // Unique ID prevents cache collision
text: text.trim()
}));
console.log(`[${NAME}] Created ${existing.length} utterances with unique IDs`);
existing.forEach((utt, i) => {
console.log(`[${NAME}] ${i}: ID=${utt.id}, text=${utt.text.substring(0, 60)}...`);
});
const data = await postJson('start', {
existing, // Send as array like Reddit, not context string!
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
console.log(`[${NAME}] Received response from /start:`, data);
// Handle invalid token - clear and show auth form
if (data && data.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (!data || !data.interaction_id) return;
widgetState.set(widgetEl, { interactionId: data.interaction_id, inputEl, context });
handleIntervention(widgetEl, inputEl, data);
const sendContinue = throttle(async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
// Check if thread is muted
const threadId = getThreadIdForWidget(widgetEl);
if (threadId && isThreadMuted(threadId)) {
// Clear input background if muted
if (st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
return;
}
const widgetId = widgetEl.id;
const h = document.getElementById(`reply_${widgetId}_h`);
if (h) {
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>
`;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span with processing indicator
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary (Processing...)`;
}
}
// Recreate existing array from cached context for continue requests
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`, // Use interaction_id for consistency
text: text.trim()
}));
const data2 = await postJson('continue', {
existing: existingForContinue,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
// Handle invalid token on continue
if (data2 && data2.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
handleIntervention(widgetEl, st.inputEl, data2);
}, 3000);
inputEl.addEventListener('input', sendContinue);
inputEl.addEventListener('keyup', sendContinue);
inputEl.addEventListener('paste', sendContinue);
let postBtn = widgetEl.querySelector(".oo-ui-buttonElement-button[title*='Reply']");
if (!postBtn) {
postBtn = Array.from(widgetEl.querySelectorAll('.oo-ui-buttonElement-button'))
.find(b => /reply/i.test((b.title || b.innerText || '').trim()));
}
if (postBtn) {
postBtn.addEventListener('click', async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
try {
// Recreate existing array from cached context for submit
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForSubmit = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
await postJson('submit', {
existing: existingForSubmit,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
submitted_id: null,
token: TOKEN,
username: USERNAME
});
} finally {
widgetState.delete(widgetEl);
}
});
}
}
function handleIntervention(widgetEl, inputEl, data) {
if (!data) {
console.warn(`[${NAME}] handleIntervention called with no data`);
return;
}
console.log(`[${NAME}] handleIntervention called with data:`, data);
if (data.error) {
return;
}
const interactionId = data.interaction_id;
if (!interactionId) {
console.warn(`[${NAME}] No interaction_id in data`);
return;
}
const threadId = getThreadIdForWidget(widgetEl);
// Skip display if thread is muted
if (threadId && isThreadMuted(threadId)) {
const widgetId = widgetEl.id || `widget_${Date.now()}`;
if (!widgetEl.id) widgetEl.id = widgetId;
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
return;
}
const { replyId, contextId } = ensurePanels(widgetEl, threadId);
const showScores = Object.prototype.hasOwnProperty.call(data, 'show_scores');
console.log(`[${NAME}] Panel IDs - context: ${contextId}, reply: ${replyId}`);
console.log(`[${NAME}] Show scores: ${showScores}`);
const which = (data.which || '').substring(0, 5);
console.log(`[${NAME}] Response type (which): ${which}`);
if (which === 'craft') {
console.log(`[${NAME}] CRAFT mode - craft_ctx_score:`, data.craft_ctx_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_score:`, data.craft_reply_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_change:`, data.craft_reply_change);
if (data.craft_ctx_score != null) {
console.log(`[${NAME}] Updating context panel with score: ${data.craft_ctx_score}`);
updateContextPanel(contextId, data.craft_ctx_score, showScores);
} else {
console.log(`[${NAME}] No context score, updating context panel with null`);
updateContextPanel(contextId, null, showScores);
}
if (data.craft_reply_change != null && data.craft_reply_score != null) {
console.log(`[${NAME}] Updating reply panel with change: ${data.craft_reply_change}, score: ${data.craft_reply_score}`);
updateReplyPanel(replyId, data.craft_reply_change, data.craft_reply_score, showScores, inputEl);
} else {
console.log(`[${NAME}] No reply scores, updating reply panel with zeros`);
updateReplyPanel(replyId, 0, 0, showScores, inputEl);
}
} else {
const noteId = `passivenote${interactionId}_d`;
if (!data.message && !document.getElementById(noteId)) {
const box = document.createElement('div');
box.id = noteId;
box.className = 'passiveModeDisplay';
box.style.cssText = `
margin:8px 0;padding:8px 12px;border-radius:6px;border:1px solid #a2a9b1;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;opacity:.8;
`;
const content = document.createElement('div');
content.id = `passivenote${interactionId}_p`;
content.style.cssText = `font-size:12px;color:#54595d;margin:0;font-style:italic;`;
content.textContent = `${NAME} is currently not active on this thread.`;
box.appendChild(content);
widgetEl.prepend(box);
}
}
}
function installClickDelegation() {
document.addEventListener('click', (e) => {
console.log(`[${NAME}] Click detected on:`, e.target.tagName, e.target.className);
// Match reply buttons and links (both old and new DiscussionTools formats)
// Need to check the actual clicked element AND traverse up
const link = e.target.closest && (
e.target.closest('.ext-discussiontools-init-replybutton') ||
e.target.closest('.ext-discussiontools-init-replylink-reply') ||
e.target.closest('a.ext-discussiontools-init-replylink') ||
e.target.closest('.ext-discussiontools-init-replylink-buttons') ||
e.target.closest('[class*="ext-discussiontools-init-"][class*="reply"]')
);
if (!link) return;
console.log(`[${NAME}] Reply link clicked:`, link.className);
// For flat discussion structures (like Research talk pages), the widget may be inserted
// outside the immediate parent. Search more broadly.
const anchorScope = link.closest('dd, li, p, div, section') || document.body;
const broadScope = anchorScope.closest('#mw-content-text, .mw-parser-output, #content') || document.body;
console.log(`[${NAME}] Anchor scope:`, anchorScope.tagName, anchorScope.className);
console.log(`[${NAME}] Broad scope:`, broadScope.tagName, broadScope.className);
const timeoutAt = Date.now() + 8000;
let attempts = 0;
const poll = setInterval(() => {
attempts++;
// Try multiple search strategies:
// 1. In immediate anchor scope (for nested structures like dd/li)
// 2. As sibling to anchor scope (for flat structures like p)
// 3. In broader content scope
// 4. Document-wide fallback
const widget = anchorScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| anchorScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| (anchorScope.nextElementSibling && (
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-replyWidget') ||
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-newTopicWidget')
) ? anchorScope.nextElementSibling : null)
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-replyWidget:last-child'))
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-newTopicWidget:last-child'))
|| broadScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| broadScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| document.querySelector('.ext-discussiontools-ui-replyWidget')
|| document.querySelector('.ext-discussiontools-ui-newTopicWidget');
if (widget) {
console.log(`[${NAME}] Widget found after ${attempts} attempts:`, widget.className);
clearInterval(poll);
startForWidget(widget);
}
else if (Date.now() > timeoutAt) {
console.log(`[${NAME}] Widget timeout after ${attempts} attempts - no widget found`);
console.log(`[${NAME}] Searched in anchorScope:`, anchorScope);
console.log(`[${NAME}] Searched in broadScope:`, broadScope);
clearInterval(poll);
} else if (attempts % 10 === 0) {
console.log(`[${NAME}] Still searching for widget, attempt ${attempts}...`);
}
}, 100);
});
}
function installWidgetObserver() {
const root = document.getElementById('content') || document.body;
console.log(`[${NAME}] Setting up mutation observer on:`, root.id || root.tagName);
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof HTMLElement)) continue;
// Log all significant additions for debugging
if (n.className && typeof n.className === 'string' && n.className.includes('ext-discussiontools')) {
console.log(`[${NAME}] DOM addition detected:`, n.tagName, n.className);
}
// Match specific DiscussionTools widgets
if (n.matches('.ext-discussiontools-ui-replyWidget') || n.matches('.ext-discussiontools-ui-newTopicWidget')) {
console.log(`[${NAME}] *** Widget detected via observer ***:`, n.className);
startForWidget(n);
} else {
const nested = n.querySelector && (
n.querySelector('.ext-discussiontools-ui-replyWidget') ||
n.querySelector('.ext-discussiontools-ui-newTopicWidget')
);
if (nested) {
console.log(`[${NAME}] *** Nested widget detected ***:`, nested.className);
startForWidget(nested);
}
}
}
}
});
obs.observe(root, { childList: true, subtree: true });
console.log(`[${NAME}] Mutation observer is now watching for widget creation`);
// Check for existing widgets on page load
const existingWidgets = document.querySelectorAll('.ext-discussiontools-ui-replyWidget, .ext-discussiontools-ui-newTopicWidget');
if (existingWidgets.length > 0) {
console.log(`[${NAME}] Found ${existingWidgets.length} existing widget(s) on page load`);
existingWidgets.forEach(w => {
console.log(`[${NAME}] Processing existing widget:`, w.className);
startForWidget(w);
});
} else {
console.log(`[${NAME}] No existing widgets found on page load`);
}
}
// kick off
installClickDelegation();
installWidgetObserver();
console.log(`[${NAME}] === INITIALIZATION COMPLETE ===`);
console.log(`[${NAME}] Click delegation: ACTIVE`);
console.log(`[${NAME}] Mutation observer: ACTIVE`);
console.log(`[${NAME}] Waiting for user interaction...`);
})();
//</nowiki>
ftcww4kvrmxid40orbuts40gnt5co50
740053
740052
2026-05-01T17:51:39Z
Laerdon
70887
740053
javascript
text/javascript
//<nowiki>
/**
* Filename: wiki-talk-page-script.js
* ConvoWizard – Real-time Reply Feedback (Wikipedia userscript)
* Runs on Talk pages inside DiscussionTools reply widgets.
* Prompts for a token on first run; persists after authorization.
*/
(async function () {
'use strict';
// Load core util module via ResourceLoader
await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api']);
// === CONFIG ===
const SERVER = 'https://convocompass.toolforge.org/api/';
// ---- UI strings and thresholds ----
const NAME = 'ConvoWizard';
const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search);
const OPTION_KEY = 'userjs-convowizard-token';
const SCORE_CHANGE_THRESH = 0.08;
const MID_TENSION_THRESH = 0.50;
const HIGH_TENSION_THRESH = 0.75;
const CONTEXT_LOW = `${NAME} will notify you here if it detects anything in the preceding conversation.`;
const CONTEXT_MID = `${NAME} thinks this discussion is getting somewhat tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_HIGH = `${NAME} thinks this discussion is getting tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const CONTEXT_VERY_HIGH = `${NAME} thinks this discussion is getting very tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const REPLY_LOW = `${NAME} thinks this comment might decrease tension in this discussion.`;
const REPLY_MID = `${NAME} will notify you here if it detects anything in your comment draft.`;
const REPLY_HIGH = `${NAME} thinks this comment might increase the tension in this discussion. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`;
const TENSION_COLORS = {
mid: { border: '#e8825c', bg: 'linear-gradient(135deg, #fff0eb 0%, #fff8f5 100%)', icon: '#e8825c', inputBg: 'rgba(232,130,92,0.05)' },
high: { border: '#d73027', bg: 'linear-gradient(135deg, #ffeaea 0%, #fff5f5 100%)', icon: '#d73027', inputBg: 'rgba(215,48,39,0.05)' },
veryHigh: { border: '#a50f15', bg: 'linear-gradient(135deg, #fdd 0%, #fee 100%)', icon: '#a50f15', inputBg: 'rgba(165,15,21,0.07)' },
};
// Keep ConvoWizard logs quiet by default to reduce console noise on Wikimedia pages.
const nativeConsole = window.console;
const console = {
log: (...args) => {
if (!DEBUG && typeof args[0] === 'string' && args[0].startsWith(`[${NAME}]`)) {
return;
}
nativeConsole.log(...args);
},
warn: (...args) => nativeConsole.warn(...args),
error: (...args) => nativeConsole.error(...args)
};
const isWikipediaHost = /(^|\.)wikipedia\.org$/.test(location.hostname);
const namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
const isTalkNamespace = Number.isFinite(namespaceNumber) && namespaceNumber % 2 === 1;
const isViewAction = mw.config.get('wgAction') === 'view';
// Gadget scope guard: only run on Wikipedia talk pages in normal view mode.
if (!isWikipediaHost || !isTalkNamespace || !isViewAction) {
console.log(
`[${NAME}] Skipping page (host=${location.hostname}, ns=${namespaceNumber}, action=${mw.config.get('wgAction')})`
);
return;
}
// ---------- helpers ----------
function throttle(fn, waitMs) {
let last = 0, timer = 0, pendingArgs = null;
return (...args) => {
const now = Date.now();
const remaining = waitMs - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
} else {
pendingArgs = args;
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn(...pendingArgs); }, remaining);
}
};
}
// Rewrite Wikipedia URL so the reddit-style backend can extract a post_id.
// Backend does: url.split("comments/")[1][:6]
// We inject "comments/<hash>" where hash is derived from the wiki page path.
function wikiUrlToPostId(href) {
try {
const pagePath = href.split('/wiki/')[1] || href;
// Simple hash to produce a stable 6-char hex id per page
let h = 0;
for (let i = 0; i < pagePath.length; i++) {
h = ((h << 5) - h) + pagePath.charCodeAt(i);
h = h & h;
}
const hex = Math.abs(h).toString(16).padStart(6, '0').slice(0, 6);
return href.split('/wiki/')[0] + '/comments/' + hex;
} catch {
return href;
}
}
async function postJson(route, body) {
try {
const res = await fetch(SERVER + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
nativeConsole.log(`[${NAME}] response from ${route}: status=${res.status}`, text);
try {
const data = JSON.parse(text);
nativeConsole.log(`[${NAME}] json from ${route}:`, data);
return data;
} catch (err) {
console.warn(`[${NAME}] non-json response from ${route}:`, text);
return {};
}
} catch (err) {
console.warn(`[${NAME}] request failed for ${route}:`, err);
return {};
}
}
function getTokenFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(OPTION_KEY);
if (typeof raw === 'string') {
const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
}
}
} catch (err) {
console.warn(`[${NAME}] Failed to read token from user options`, err);
}
return null;
}
async function persistToken(storageKey, token) {
if (!token) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, token);
}
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(OPTION_KEY, token || '');
await new mw.Api().saveOption(OPTION_KEY, token || '');
console.log(`[${NAME}] ${token ? 'Saved' : 'Cleared'} token in user options (notepad)`);
} catch (err) {
console.warn(`[${NAME}] Could not persist token to user options`, err);
}
}
async function validateToken(token, username) {
try {
const response = await postJson('claim_token', { token, username });
return response.valid === true;
} catch {
return false;
}
}
// ========== CREDENTIAL SETUP ==========
const USERNAME = mw.config.get('wgUserName');
if (!USERNAME) {
console.log(`[${NAME}] Not logged in, skipping`);
return;
}
let TOKEN = null;
const STORAGE_KEY = `ConvoWizard:token:${USERNAME}`;
// Load persisted token (options → localStorage), validate, sync stores
async function loadPersistedToken() {
let token = getTokenFromOptions();
let source = 'options';
if (!token) {
token = localStorage.getItem(STORAGE_KEY);
source = 'localStorage';
}
if (token && typeof token === 'string') {
token = token.trim();
}
if (!token) return;
const valid = await validateToken(token, USERNAME);
if (valid) {
TOKEN = token;
// Sync both stores
if (source === 'options') {
localStorage.setItem(STORAGE_KEY, token);
} else {
await persistToken(STORAGE_KEY, token);
}
console.log(`[${NAME}] Token loaded from ${source}`);
} else {
console.log(`[${NAME}] Persisted token invalid, clearing`);
await persistToken(STORAGE_KEY, null);
}
}
await loadPersistedToken();
console.log(`[${NAME}] === SCRIPT LOADED SUCCESSFULLY ===`);
console.log(`[${NAME}] Server: ${SERVER}`);
console.log(`[${NAME}] User: ${USERNAME}`);
console.log(`[${NAME}] Token: ${TOKEN ? 'present' : 'none (will prompt)'}`);
console.log(`[${NAME}] Page URL: ${location.href}`);
console.log(`[${NAME}] Installing click and mutation observers...`);
// ========== TOKEN INPUT FORM ==========
function showTokenForm(widgetEl) {
// Don't add duplicate forms
if (widgetEl.querySelector('.convowizard-auth-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-auth-form';
form.style.cssText = `
margin:8px 0;padding:16px 20px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
const title = document.createElement('div');
title.style.cssText = 'font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;';
title.textContent = `${NAME}: Authorization Required`;
const desc = document.createElement('div');
desc.style.cssText = 'font-size:13px;color:#333;margin-bottom:12px;line-height:1.4;';
desc.textContent = 'Please paste your ConvoWizard token below to activate the extension.';
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;align-items:center;';
const input = document.createElement('input');
input.type = 'password';
input.placeholder = 'Paste token here';
input.autocomplete = 'off';
input.spellcheck = false;
input.style.cssText = `
flex:1;padding:8px 10px;font-size:13px;border:1px solid #a2a9b1;border-radius:4px;
font-family:monospace;outline:none;transition:border-color 0.2s ease;
`;
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Authorize';
btn.style.cssText = `
padding:8px 16px;font-size:13px;font-weight:600;border:none;border-radius:4px;
background:#0645ad;color:#fff;cursor:pointer;transition:background 0.2s ease;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
`;
btn.addEventListener('mouseenter', () => { btn.style.background = '#0b5cce'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#0645ad'; });
const errDiv = document.createElement('div');
errDiv.style.cssText = 'font-size:12px;color:#d73027;margin-top:8px;display:none;';
async function onAuthorize() {
const raw = input.value.trim();
// Input validation: 33 hex chars
if (!/^[0-9a-f]{33}$/i.test(raw)) {
errDiv.textContent = 'Invalid token format. A token is 33 hexadecimal characters.';
errDiv.style.display = 'block';
input.value = '';
input.focus();
return;
}
btn.disabled = true;
btn.textContent = 'Validating...';
errDiv.style.display = 'none';
const valid = await validateToken(raw, USERNAME);
// Clear input immediately after use
input.value = '';
if (valid) {
TOKEN = raw;
await persistToken(STORAGE_KEY, TOKEN);
// Remove all auth forms on page
document.querySelectorAll('.convowizard-auth-form').forEach(f => f.remove());
// Activate the widget
startForWidget(widgetEl);
} else {
errDiv.textContent = 'Token is invalid or expired. Please check and try again.';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Authorize';
input.focus();
}
}
btn.addEventListener('click', onAuthorize);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); onAuthorize(); }
});
row.appendChild(input);
row.appendChild(btn);
form.appendChild(title);
form.appendChild(desc);
form.appendChild(row);
form.appendChild(errDiv);
widgetEl.prepend(form);
input.focus();
}
// ---- state + logs ----
const widgetState = new WeakMap();
function cleanupDuplicatePanels() {
const allPanels = document.querySelectorAll('.craftDisplay, .passiveModeDisplay');
const seen = new Set();
allPanels.forEach(panel => {
const pid = panel.id;
if (pid && pid.includes('_')) {
const parts = pid.split('_');
const type = parts[0];
const wid = parts.slice(1).join('_').replace('_d', '');
const key = `${type}|${wid}`;
if (seen.has(key)) panel.remove(); else seen.add(key);
}
});
}
// ========== MUTE THREAD ==========
/**
* Generates a unique thread identifier by combining URL section, DOM position, and content hash.
* Ensures each thread on a page gets a distinct ID for per-thread mute functionality.
*/
function getThreadIdForWidget(widgetEl) {
try {
const urlHash = location.hash ? location.hash.replace('#', '') : 'root';
// Generate DOM path for widget position
const widgetPath = [];
let el = widgetEl;
for (let i = 0; i < 8 && el; i++) {
const parent = el.parentElement;
if (!parent || parent === document.body) break;
const siblings = Array.from(parent.children);
const index = siblings.indexOf(el);
widgetPath.unshift(`${parent.tagName}_${index}`);
el = parent;
}
const domPath = widgetPath.length > 0 ? widgetPath.join('_') : 'unknown';
// Get content-based hash from first comment
const context = getContextStringFor(widgetEl);
let contentHash = '';
if (context && context.length > 0) {
const firstComment = context.split('\n\n')[0] || context.substring(0, 150);
let hash = 0;
for (let i = 0; i < Math.min(firstComment.length, 150); i++) {
const char = firstComment.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
const parentText = widgetEl.parentElement ?
(widgetEl.parentElement.textContent || '').substring(0, 50).trim() : '';
if (parentText) {
let hash = 0;
for (let i = 0; i < parentText.length; i++) {
hash = ((hash << 5) - hash) + parentText.charCodeAt(i);
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
contentHash = Math.random().toString(36).slice(2, 8);
}
}
return `thread_${urlHash}_${domPath.substring(0, 50)}_${contentHash}`;
} catch (err) {
console.warn(`[${NAME}] Error generating thread ID:`, err);
return `thread_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}
}
function getMutedThreads() {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const stored = localStorage.getItem(storageKey);
if (!stored) return new Set();
const threads = JSON.parse(stored);
return new Set(Array.isArray(threads) ? threads : []);
} catch {
return new Set();
}
}
function saveMutedThreads(threadSet) {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const threads = Array.from(threadSet);
localStorage.setItem(storageKey, JSON.stringify(threads));
} catch (err) {
console.warn(`[${NAME}] Error saving muted threads:`, err);
}
}
function isThreadMuted(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
return muted.has(threadId);
}
function toggleThreadMute(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
const wasMuted = muted.has(threadId);
if (wasMuted) {
muted.delete(threadId);
} else {
muted.add(threadId);
}
saveMutedThreads(muted);
return !wasMuted;
}
/**
* Creates an unmute indicator button that appears when a thread is muted
* Allows users to restore ConvoWizard for a muted thread
*/
function ensureUnmuteIndicator(widgetEl, threadId, widgetId) {
const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator');
if (existingIndicator) return;
const indicator = document.createElement('div');
indicator.id = `convowizard-unmute-${widgetId}`;
indicator.className = 'convowizard-unmute-indicator';
indicator.style.cssText = `
margin: 4px 0;
padding: 6px 10px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: #f8f9fa;
color: #54595d;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.2s ease;
`;
const icon = document.createElement('span');
icon.textContent = '🔊';
icon.style.fontSize = '12px';
const text = document.createElement('span');
text.textContent = `${NAME} is muted for this thread. Click to unmute.`;
text.style.flex = '1';
indicator.appendChild(icon);
indicator.appendChild(text);
indicator.addEventListener('mouseenter', () => {
indicator.style.background = '#e9ecef';
indicator.style.borderColor = '#0645ad';
});
indicator.addEventListener('mouseleave', () => {
indicator.style.background = '#f8f9fa';
indicator.style.borderColor = '#a2a9b1';
});
indicator.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Show panels and remove indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
if (contextPanel) contextPanel.style.display = '';
if (replyPanel) replyPanel.style.display = '';
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
// Update mute buttons
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(btn => {
btn.textContent = '🔇 Mute thread';
btn.title = 'Mute ConvoWizard for this thread';
btn.style.background = '#ffffff';
});
// Re-trigger analysis to refresh display
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
// Re-run the continue request to refresh the display
const inputText = st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '';
if (inputText.trim().length > 0) {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('continue', {
existing: existingForContinue,
new: inputText,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
if (data && !data.error) {
handleIntervention(widgetEl, st.inputEl, data);
}
} else {
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
const data = await postJson('start', {
existing,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
if (data && !data.error && data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
handleIntervention(widgetEl, st.inputEl, data);
}
}
} else {
startForWidget(widgetEl);
}
});
// Insert at the beginning of the widget
widgetEl.insertBefore(indicator, widgetEl.firstChild);
}
function createMuteButton(threadId, widgetId, isMuted) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-mute-btn';
btn.setAttribute('data-thread-id', threadId);
btn.setAttribute('data-widget-id', widgetId);
btn.style.cssText = `
margin-left: auto;
padding: 4px 8px;
font-size: 11px;
border: 1px solid #a2a9b1;
border-radius: 4px;
background: ${isMuted ? '#f8f9fa' : '#ffffff'};
color: #54595d;
cursor: pointer;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
btn.textContent = isMuted
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = isMuted
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.addEventListener('mouseenter', () => {
btn.style.background = isMuted ? '#e9ecef' : '#f8f9fa';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = isMuted ? '#f8f9fa' : '#ffffff';
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Update button state
btn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
btn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
btn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
// Hide/show panels and unmute indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
const widgetEl = contextPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
contextPanel?.closest('.ext-discussiontools-ui-newTopicWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-newTopicWidget');
if (contextPanel) contextPanel.style.display = newMuteState ? 'none' : '';
if (replyPanel) replyPanel.style.display = newMuteState ? 'none' : '';
// Clear textbox background when muting
if (newMuteState) {
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
}
if (newMuteState && widgetEl) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (widgetEl) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Update all mute buttons for this thread
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(otherBtn => {
if (otherBtn !== btn) {
otherBtn.textContent = newMuteState
? '🔊 Unmute thread'
: '🔇 Mute thread';
otherBtn.title = newMuteState
? 'Unmute ConvoWizard for this thread'
: 'Mute ConvoWizard for this thread';
otherBtn.style.background = newMuteState ? '#f8f9fa' : '#ffffff';
}
});
});
return btn;
}
function ensurePanels(widgetEl, threadId = null) {
cleanupDuplicatePanels();
// Clean up duplicate unmute indicators
const unmuteIndicators = widgetEl.querySelectorAll('.convowizard-unmute-indicator');
if (unmuteIndicators.length > 1) {
for (let i = 1; i < unmuteIndicators.length; i++) {
unmuteIndicators[i].remove();
}
}
const widgetId = widgetEl.id || `widget_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
if (!widgetEl.id) widgetEl.id = widgetId;
const contextId = `context_${widgetId}`;
const replyId = `reply_${widgetId}`;
const isMuted = threadId ? isThreadMuted(threadId) : false;
// Ensure Context panel
if (!document.getElementById(`${contextId}_d`)) {
const contextBox = document.createElement('div');
contextBox.id = `${contextId}_d`;
contextBox.className = 'craftDisplay';
contextBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const cheader = document.createElement('div');
cheader.id = `${contextId}_h`;
cheader.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Context Summary`;
cheader.appendChild(iconSvg);
cheader.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
cheader.appendChild(muteBtn);
}
const ccontent = document.createElement('div');
ccontent.id = `${contextId}_p`;
ccontent.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
ccontent.textContent = `When you type a reply ${NAME} will give you some feedback on the discussion context.`;
contextBox.appendChild(cheader);
contextBox.appendChild(ccontent);
if (isMuted) contextBox.style.display = 'none';
widgetEl.prepend(contextBox);
} else if (threadId) {
const existingPanel = document.getElementById(`${contextId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
// Show unmute indicator when muted (only if panels don't exist yet)
if (threadId && isMuted && !document.getElementById(`${contextId}_d`) && !document.getElementById(`${replyId}_d`)) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (threadId && !isMuted) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Ensure Reply panel
if (!document.getElementById(`${replyId}_d`)) {
const replyBox = document.createElement('div');
replyBox.id = `${replyId}_d`;
replyBox.className = 'craftDisplay';
replyBox.style.cssText = `
margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative;
`;
const header = document.createElement('div');
header.id = `${replyId}_h`;
header.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '16');
iconSvg.setAttribute('height', '16');
iconSvg.setAttribute('viewBox', '0 0 16 16');
iconSvg.setAttribute('fill', 'currentColor');
iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = `${NAME}: Reply Summary`;
header.appendChild(iconSvg);
header.appendChild(textSpan);
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
header.appendChild(muteBtn);
}
const content = document.createElement('div');
content.id = `${replyId}_p`;
content.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`;
content.textContent = `When you type a reply ${NAME} will give you some feedback on your comment.`;
replyBox.appendChild(header);
replyBox.appendChild(content);
if (isMuted) replyBox.style.display = 'none';
const preview = widgetEl.querySelector('.ext-discussiontools-ui-replyWidget-preview')
|| document.querySelector('.ext-discussiontools-ui-replyWidget-preview');
if (preview && preview.parentElement) {
preview.parentElement.insertBefore(replyBox, preview.nextSibling);
} else {
widgetEl.append(replyBox);
}
} else if (threadId) {
const existingPanel = document.getElementById(`${replyId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
return { replyId, contextId };
}
function updateContextPanel(contextId, score, showScores) {
console.log(`[${NAME}] updateContextPanel called - contextId: ${contextId}, score: ${score}, showScores: ${showScores}`);
const hasScore = typeof score === 'number';
let text = showScores && hasScore ? `Context craft score: ${score.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
console.log(`[${NAME}] hasScore: ${hasScore}, score value: ${score}`);
console.log(`[${NAME}] Thresholds - MID: ${MID_TENSION_THRESH}, HIGH: ${HIGH_TENSION_THRESH}`);
if (hasScore && score > MID_TENSION_THRESH) {
let level;
if (score > HIGH_TENSION_THRESH) {
console.log(`[${NAME}] Very high tension (score > ${HIGH_TENSION_THRESH})`);
text += `${CONTEXT_VERY_HIGH}`;
level = 'veryHigh';
} else if (score > 0.60) {
console.log(`[${NAME}] High tension (score > 0.60)`);
text += `${CONTEXT_HIGH}`;
level = 'high';
} else {
console.log(`[${NAME}] Mid tension (score > ${MID_TENSION_THRESH})`);
text += `${CONTEXT_MID}`;
level = 'mid';
}
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else {
console.log(`[${NAME}] Low tension or no score`);
text += `${CONTEXT_LOW}`;
}
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
console.log(`[${NAME}] DOM elements - box: ${!!box}, p: ${!!p}, h: ${!!h}`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) {
p.textContent = text;
console.log(`[${NAME}] Updated text content (length: ${text.length})`);
}
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Context Summary`;
}
console.log(`[${NAME}] Updated header color: ${iconColor}`);
}
}
function updateReplyPanel(replyId, scoreChange, rawScore, showScores, inputEl) {
let text = showScores ? `Change in craft score after your comment: ${scoreChange.toFixed(4)} ` : '';
let borderColor = '#0645ad';
let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)';
let iconColor = '#0645ad';
let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>`;
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
text += `${REPLY_HIGH}`;
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
borderColor = TENSION_COLORS[level].border;
bgGradient = TENSION_COLORS[level].bg;
iconColor = TENSION_COLORS[level].icon;
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/>
</svg>`;
} else if (rawScore - scoreChange > 0.55 && scoreChange < -SCORE_CHANGE_THRESH) {
text += `${REPLY_LOW}`;
borderColor = '#4caf50';
bgGradient = 'linear-gradient(135deg, #f1f8e9 0%, #f9fbe7 100%)';
iconColor = '#4caf50';
icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 6L6.5 11 4 8.5l1-1 1.5 1.5L10.5 5l1 1z"/>
</svg>`;
} else {
text += `${REPLY_MID}`;
}
if (inputEl) {
let threadId = null;
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) {
const muteBtn = replyBox.querySelector('.convowizard-mute-btn');
if (muteBtn) {
threadId = muteBtn.getAttribute('data-thread-id');
}
}
// If no thread ID from panel, try to get from widget
if (!threadId) {
const widgetEl = inputEl.closest('.ext-discussiontools-ui-replyWidget') ||
inputEl.closest('.ext-discussiontools-ui-newTopicWidget');
if (widgetEl) {
threadId = getThreadIdForWidget(widgetEl);
}
}
// If muted, clear background and return
if (threadId && isThreadMuted(threadId)) {
inputEl.style.setProperty('background-color', 'transparent', 'important');
return;
}
let inputBg = 'transparent';
if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) {
const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid';
inputBg = TENSION_COLORS[level].inputBg;
} else if (scoreChange < -SCORE_CHANGE_THRESH) {
inputBg = 'rgba(76,175,80,0.05)';
}
inputEl.style.setProperty('background-color', inputBg, 'important');
inputEl.style.setProperty('transition', 'background-color 0.3s ease', 'important');
}
const box = document.getElementById(`${replyId}_d`);
const p = document.getElementById(`${replyId}_p`);
const h = document.getElementById(`${replyId}_h`);
if (box) {
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
box.style.borderColor = borderColor;
box.style.background = bgGradient;
box.style.boxShadow = `0 2px 8px ${borderColor}20`;
if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) {
box.style.display = '';
}
}
if (p) p.textContent = text;
if (h) {
h.style.color = iconColor;
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = icon;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary`;
}
}
}
function getContextStringFor(widgetEl) {
try {
console.log(`[${NAME}] === Extracting context for widget ===`);
// Helper to check if element is a comment container
const isCommentNode = (el) => !!el && (el.tagName === 'DD' || el.tagName === 'LI' || el.tagName === 'P');
// Extract clean comment text, removing nested replies and UI elements
// IMPORTANT: Keep timestamps (UTC) because backend splits on them!
const extractCommentText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
// Remove nested discussion lists, widgets, and UI elements
// NOTE: DO NOT remove .ext-discussiontools-init-timestamplink - backend needs timestamps to split utterances!
clone.querySelectorAll('dl, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, .ext-discussiontools-init-replylink-buttons, [role="button"]').forEach(n => n.remove());
const text = (clone.innerText || '').trim();
return text;
};
// Wikipedia Talk pages have structure: <dd>comment text<dl><dd>reply<dl><dd>widget
// When widget appears, it's nested inside a new <dd> inside a new <dl>
// We need to find the parent <dd> that contains actual comment text
let commentDD = null;
// Strategy 1: Widget is inside <dd> → <dl> → <dd> (the target comment)
// Find the <dd> containing the widget
const widgetDD = widgetEl.closest('dd, li');
console.log(`[${NAME}] Widget container DD:`, widgetDD ? widgetDD.tagName : 'not found');
if (widgetDD) {
// Check if this DD has meaningful text (not just the widget)
const widgetDDText = extractCommentText(widgetDD);
console.log(`[${NAME}] Widget DD text length:`, widgetDDText.length);
if (widgetDDText.length > 20) {
// This DD has actual comment text
commentDD = widgetDD;
} else {
// This DD is empty/minimal - the widget was just inserted
// Go up: DD → DL → parent DD (which should have the comment)
const parentDL = widgetDD.parentElement;
console.log(`[${NAME}] Parent DL:`, parentDL ? parentDL.tagName : 'not found');
if (parentDL && (parentDL.tagName === 'DL' || parentDL.tagName === 'UL' || parentDL.tagName === 'OL')) {
// Find the parent DD/LI/P that contains this DL
commentDD = parentDL.closest('dd, li, p');
console.log(`[${NAME}] Found parent comment node:`, commentDD ? commentDD.tagName : 'not found');
}
}
}
// Strategy 2: Look for preceding paragraph with comment markers (flat structure)
if (!commentDD) {
console.log(`[${NAME}] Trying flat structure search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 10) {
probe = probe.previousElementSibling || (probe.parentElement ? probe.parentElement.previousElementSibling : null);
if (probe && probe.tagName === 'P' && probe.querySelector('[data-mw-comment-start], [data-mw-comment-end]')) {
commentDD = probe;
console.log(`[${NAME}] Found comment via flat search at hop ${hops}`);
break;
}
}
}
// Strategy 3: Last resort - traverse DOM looking for any comment node
if (!commentDD) {
console.log(`[${NAME}] Trying fallback search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 20) {
if (isCommentNode(probe) && extractCommentText(probe).length > 20) {
commentDD = probe;
console.log(`[${NAME}] Found comment via fallback at hop ${hops}`);
break;
}
probe = probe.previousElementSibling ||
(probe.parentElement ? probe.parentElement.previousElementSibling : null) ||
(probe.parentElement ? probe.parentElement.parentElement : null);
}
}
if (!commentDD) {
console.warn(`[${NAME}] Could not find any comment node!`);
return '';
}
// Now traverse up the comment chain collecting all parent comments
const texts = [];
const seen = new Set();
let current = commentDD;
let iterations = 0;
console.log(`[${NAME}] Starting comment chain traversal from:`, current.tagName);
while (current && iterations++ < 20) {
if (seen.has(current)) {
console.log(`[${NAME}] Already seen this node, breaking`);
break;
}
seen.add(current);
const text = extractCommentText(current);
if (text && text.length > 0) {
console.log(`[${NAME}] Extracted comment (${text.length} chars):`, text.substring(0, 100) + '...');
texts.unshift(text); // Add to beginning to maintain chronological order
}
// Navigate up the discussion tree
// Structure: <p>text</p><dl><dd>reply<dl><dd>reply</dd></dl></dd></dl>
// From current <dd>, go to parent element (should be <dl>)
const parentList = current.parentElement;
if (!parentList) {
console.log(`[${NAME}] No parent element, stopping`);
break;
}
console.log(`[${NAME}] Parent list:`, parentList.tagName);
// From <dl>, find the parent <dd> or <li> or <p>
if (parentList.tagName === 'DL' || parentList.tagName === 'UL' || parentList.tagName === 'OL') {
// First try to find an ancestor comment node
current = parentList.closest('dd, li, p');
if (current) {
console.log(`[${NAME}] Found ancestor comment:`, current.tagName);
} else {
// No ancestor found - the <p> might be a SIBLING of this <dl>
// This happens in flat structure: <p>comment</p><dl><dd>reply...
console.log(`[${NAME}] No ancestor, searching for previous sibling of DL...`);
let sibling = parentList.previousElementSibling;
let siblingHops = 0;
while (sibling && siblingHops++ < 10) {
console.log(`[${NAME}] Checking sibling:`, sibling.tagName);
if (isCommentNode(sibling) && !seen.has(sibling)) {
const siblingText = extractCommentText(sibling);
if (siblingText.length > 20) {
current = sibling;
console.log(`[${NAME}] Found sibling comment with text (${siblingText.length} chars)`);
break;
}
}
sibling = sibling.previousElementSibling;
}
if (!current || current === parentList.closest('dd, li, p')) {
console.log(`[${NAME}] No more comments found, stopping`);
break;
}
}
} else if (isCommentNode(parentList)) {
// Parent is itself a comment node
current = parentList;
} else {
// Try to find previous sibling comment
let sibling = current.previousElementSibling;
while (sibling && !isCommentNode(sibling)) {
sibling = sibling.previousElementSibling;
}
if (sibling && !seen.has(sibling)) {
current = sibling;
console.log(`[${NAME}] Moving to sibling:`, current.tagName);
} else {
console.log(`[${NAME}] No more siblings, stopping`);
break;
}
}
}
console.log(`[${NAME}] Extracted ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
// Additional pass: Look for any <p> elements with comment markers that might have been missed
// This catches the case where the first comment in a thread is a <p> that's a sibling of the DL
if (commentDD && commentDD.tagName === 'DD') {
const topDL = commentDD.closest('dl');
if (topDL && topDL.previousElementSibling) {
let probe = topDL.previousElementSibling;
let probeHops = 0;
console.log(`[${NAME}] Scanning backwards from top DL for more context...`);
while (probe && probeHops++ < 5) {
if (probe.tagName === 'P' && !seen.has(probe)) {
const probeText = extractCommentText(probe);
if (probeText.length > 20 && (probe.querySelector('[data-mw-comment-start]') || probe.querySelector('[data-mw-comment-end]'))) {
console.log(`[${NAME}] Found additional preceding comment (${probeText.length} chars):`, probeText.substring(0, 100) + '...');
texts.unshift(probeText);
seen.add(probe);
}
}
probe = probe.previousElementSibling;
}
}
}
console.log(`[${NAME}] Final extraction: ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
if (texts.length > 0) {
const fullContext = texts.join('\n\n');
console.log(`[${NAME}] Full context:`, fullContext);
return fullContext;
}
console.warn(`[${NAME}] No context found, returning empty string`);
return '';
} catch (err) {
console.error(`[${NAME}] Error extracting context:`, err);
return '';
}
}
async function startForWidget(widgetEl) {
if (!TOKEN) { showTokenForm(widgetEl); return; }
if (widgetState.has(widgetEl) || widgetEl.getAttribute('data-convowizard-processing') === 'true') return;
widgetEl.setAttribute('data-convowizard-processing', 'true');
console.log(`[${NAME}] Starting widget processing:`, widgetEl.className);
function findInput(scope) {
// Prefer DT textbox role, then VE contenteditable surface, then textarea
return (
scope.querySelector('div[role="textbox"]') ||
scope.querySelector('[contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface [contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface') ||
scope.querySelector('textarea')
);
}
async function waitForInput(scope, timeoutMs = 8000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tick = () => {
const el = findInput(scope) || findInput(document);
if (el) return resolve(el);
if (Date.now() > deadline) return resolve(null);
setTimeout(tick, 100);
};
tick();
});
}
const inputEl = await waitForInput(widgetEl);
if (!inputEl) {
console.warn(`[${NAME}] No input element found in widget (after waiting)`);
return;
}
console.log(`[${NAME}] Input element found:`, inputEl.tagName, inputEl.getAttribute('role') || (inputEl.getAttribute('contenteditable') ? 'contenteditable' : ''));
const context = getContextStringFor(widgetEl);
console.log(`[${NAME}] Context extracted, length: ${context.length} chars`);
console.log(`[${NAME}] Context preview:`, context.substring(0, 200));
// Split context on (UTC) timestamps to create separate utterances like Reddit does
// This prevents backend caching bug where all Wikipedia convos share id=None
const utterances = context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existing = utterances.map((text, index) => ({
id: `wiki_${Date.now()}_${index}`, // Unique ID prevents cache collision
text: text.trim()
}));
console.log(`[${NAME}] Created ${existing.length} utterances with unique IDs`);
existing.forEach((utt, i) => {
console.log(`[${NAME}] ${i}: ID=${utt.id}, text=${utt.text.substring(0, 60)}...`);
});
const data = await postJson('start', {
existing, // Send as array like Reddit, not context string!
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME
});
console.log(`[${NAME}] Received response from /start:`, data);
// Handle invalid token - clear and show auth form
if (data && data.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (!data || !data.interaction_id) return;
widgetState.set(widgetEl, { interactionId: data.interaction_id, inputEl, context });
handleIntervention(widgetEl, inputEl, data);
const sendContinue = throttle(async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
// Check if thread is muted
const threadId = getThreadIdForWidget(widgetEl);
if (threadId && isThreadMuted(threadId)) {
// Clear input background if muted
if (st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
return;
}
const widgetId = widgetEl.id;
const h = document.getElementById(`reply_${widgetId}_h`);
if (h) {
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>
</svg>
`;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span with processing indicator
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
textSpan.textContent = `${NAME}: Reply Summary (Processing...)`;
}
}
// Recreate existing array from cached context for continue requests
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForContinue = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`, // Use interaction_id for consistency
text: text.trim()
}));
const data2 = await postJson('continue', {
existing: existingForContinue,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME
});
// Handle invalid token on continue
if (data2 && data2.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
handleIntervention(widgetEl, st.inputEl, data2);
}, 3000);
inputEl.addEventListener('input', sendContinue);
inputEl.addEventListener('keyup', sendContinue);
inputEl.addEventListener('paste', sendContinue);
let postBtn = widgetEl.querySelector(".oo-ui-buttonElement-button[title*='Reply']");
if (!postBtn) {
postBtn = Array.from(widgetEl.querySelectorAll('.oo-ui-buttonElement-button'))
.find(b => /reply/i.test((b.title || b.innerText || '').trim()));
}
if (postBtn) {
postBtn.addEventListener('click', async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
try {
// Recreate existing array from cached context for submit
const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0);
const existingForSubmit = contextUtterances.map((text, index) => ({
id: `wiki_${st.interactionId}_${index}`,
text: text.trim()
}));
await postJson('submit', {
existing: existingForSubmit,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
submitted_id: null,
token: TOKEN,
username: USERNAME
});
} finally {
widgetState.delete(widgetEl);
}
});
}
}
function handleIntervention(widgetEl, inputEl, data) {
if (!data) {
console.warn(`[${NAME}] handleIntervention called with no data`);
return;
}
console.log(`[${NAME}] handleIntervention called with data:`, data);
if (data.error) {
return;
}
const interactionId = data.interaction_id;
if (!interactionId) {
console.warn(`[${NAME}] No interaction_id in data`);
return;
}
const threadId = getThreadIdForWidget(widgetEl);
// Skip display if thread is muted
if (threadId && isThreadMuted(threadId)) {
const widgetId = widgetEl.id || `widget_${Date.now()}`;
if (!widgetEl.id) widgetEl.id = widgetId;
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
return;
}
const { replyId, contextId } = ensurePanels(widgetEl, threadId);
const showScores = Object.prototype.hasOwnProperty.call(data, 'show_scores');
console.log(`[${NAME}] Panel IDs - context: ${contextId}, reply: ${replyId}`);
console.log(`[${NAME}] Show scores: ${showScores}`);
const which = (data.which || '').substring(0, 5);
console.log(`[${NAME}] Response type (which): ${which}`);
if (which === 'craft') {
console.log(`[${NAME}] CRAFT mode - craft_ctx_score:`, data.craft_ctx_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_score:`, data.craft_reply_score);
console.log(`[${NAME}] CRAFT mode - craft_reply_change:`, data.craft_reply_change);
if (data.craft_ctx_score != null) {
console.log(`[${NAME}] Updating context panel with score: ${data.craft_ctx_score}`);
updateContextPanel(contextId, data.craft_ctx_score, showScores);
} else {
console.log(`[${NAME}] No context score, updating context panel with null`);
updateContextPanel(contextId, null, showScores);
}
if (data.craft_reply_change != null && data.craft_reply_score != null) {
console.log(`[${NAME}] Updating reply panel with change: ${data.craft_reply_change}, score: ${data.craft_reply_score}`);
updateReplyPanel(replyId, data.craft_reply_change, data.craft_reply_score, showScores, inputEl);
} else {
console.log(`[${NAME}] No reply scores, updating reply panel with zeros`);
updateReplyPanel(replyId, 0, 0, showScores, inputEl);
}
} else {
const noteId = `passivenote${interactionId}_d`;
if (!data.message && !document.getElementById(noteId)) {
const box = document.createElement('div');
box.id = noteId;
box.className = 'passiveModeDisplay';
box.style.cssText = `
margin:8px 0;padding:8px 12px;border-radius:6px;border:1px solid #a2a9b1;
background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;opacity:.8;
`;
const content = document.createElement('div');
content.id = `passivenote${interactionId}_p`;
content.style.cssText = `font-size:12px;color:#54595d;margin:0;font-style:italic;`;
content.textContent = `${NAME} is currently not active on this thread.`;
box.appendChild(content);
widgetEl.prepend(box);
}
}
}
function installClickDelegation() {
document.addEventListener('click', (e) => {
console.log(`[${NAME}] Click detected on:`, e.target.tagName, e.target.className);
// Match reply buttons and links (both old and new DiscussionTools formats)
// Need to check the actual clicked element AND traverse up
const link = e.target.closest && (
e.target.closest('.ext-discussiontools-init-replybutton') ||
e.target.closest('.ext-discussiontools-init-replylink-reply') ||
e.target.closest('a.ext-discussiontools-init-replylink') ||
e.target.closest('.ext-discussiontools-init-replylink-buttons') ||
e.target.closest('[class*="ext-discussiontools-init-"][class*="reply"]')
);
if (!link) return;
console.log(`[${NAME}] Reply link clicked:`, link.className);
// For flat discussion structures (like Research talk pages), the widget may be inserted
// outside the immediate parent. Search more broadly.
const anchorScope = link.closest('dd, li, p, div, section') || document.body;
const broadScope = anchorScope.closest('#mw-content-text, .mw-parser-output, #content') || document.body;
console.log(`[${NAME}] Anchor scope:`, anchorScope.tagName, anchorScope.className);
console.log(`[${NAME}] Broad scope:`, broadScope.tagName, broadScope.className);
const timeoutAt = Date.now() + 8000;
let attempts = 0;
const poll = setInterval(() => {
attempts++;
// Try multiple search strategies:
// 1. In immediate anchor scope (for nested structures like dd/li)
// 2. As sibling to anchor scope (for flat structures like p)
// 3. In broader content scope
// 4. Document-wide fallback
const widget = anchorScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| anchorScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| (anchorScope.nextElementSibling && (
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-replyWidget') ||
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-newTopicWidget')
) ? anchorScope.nextElementSibling : null)
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-replyWidget:last-child'))
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-newTopicWidget:last-child'))
|| broadScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| broadScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| document.querySelector('.ext-discussiontools-ui-replyWidget')
|| document.querySelector('.ext-discussiontools-ui-newTopicWidget');
if (widget) {
console.log(`[${NAME}] Widget found after ${attempts} attempts:`, widget.className);
clearInterval(poll);
startForWidget(widget);
}
else if (Date.now() > timeoutAt) {
console.log(`[${NAME}] Widget timeout after ${attempts} attempts - no widget found`);
console.log(`[${NAME}] Searched in anchorScope:`, anchorScope);
console.log(`[${NAME}] Searched in broadScope:`, broadScope);
clearInterval(poll);
} else if (attempts % 10 === 0) {
console.log(`[${NAME}] Still searching for widget, attempt ${attempts}...`);
}
}, 100);
});
}
function installWidgetObserver() {
const root = document.getElementById('content') || document.body;
console.log(`[${NAME}] Setting up mutation observer on:`, root.id || root.tagName);
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof HTMLElement)) continue;
// Log all significant additions for debugging
if (n.className && typeof n.className === 'string' && n.className.includes('ext-discussiontools')) {
console.log(`[${NAME}] DOM addition detected:`, n.tagName, n.className);
}
// Match specific DiscussionTools widgets
if (n.matches('.ext-discussiontools-ui-replyWidget') || n.matches('.ext-discussiontools-ui-newTopicWidget')) {
console.log(`[${NAME}] *** Widget detected via observer ***:`, n.className);
startForWidget(n);
} else {
const nested = n.querySelector && (
n.querySelector('.ext-discussiontools-ui-replyWidget') ||
n.querySelector('.ext-discussiontools-ui-newTopicWidget')
);
if (nested) {
console.log(`[${NAME}] *** Nested widget detected ***:`, nested.className);
startForWidget(nested);
}
}
}
}
});
obs.observe(root, { childList: true, subtree: true });
console.log(`[${NAME}] Mutation observer is now watching for widget creation`);
// Check for existing widgets on page load
const existingWidgets = document.querySelectorAll('.ext-discussiontools-ui-replyWidget, .ext-discussiontools-ui-newTopicWidget');
if (existingWidgets.length > 0) {
console.log(`[${NAME}] Found ${existingWidgets.length} existing widget(s) on page load`);
existingWidgets.forEach(w => {
console.log(`[${NAME}] Processing existing widget:`, w.className);
startForWidget(w);
});
} else {
console.log(`[${NAME}] No existing widgets found on page load`);
}
}
// kick off
installClickDelegation();
installWidgetObserver();
console.log(`[${NAME}] === INITIALIZATION COMPLETE ===`);
console.log(`[${NAME}] Click delegation: ACTIVE`);
console.log(`[${NAME}] Mutation observer: ACTIVE`);
console.log(`[${NAME}] Waiting for user interaction...`);
})();
//</nowiki>
gqs3nqhf6blhg26t1docydtkqnp9kif
User:Sam Sailor/test.js/comrade-lib.js
2
175022
739981
739969
2026-05-01T12:49:15Z
Sam Sailor
26820
Test
739981
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
$btn.prop('disabled', true).text('Auditing...');
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkPage.revisions[0].content.match(/\|class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log(oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
9bwsi3mvqb7gjd1qkwh408mx79v2hl3
739982
739981
2026-05-01T12:51:31Z
Sam Sailor
26820
Test
739982
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
$btn.prop('disabled', true).text('Auditing...');
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
npw5s2vr04tsmn21yn2wz8kct4m8ap8
740021
739982
2026-05-01T15:43:34Z
Sam Sailor
26820
Test
740021
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
$btn.prop('disabled', true).text('Auditing...');
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
diqlcff3qrwn5i09b6ktywp2zq4wi8o
740030
740021
2026-05-01T16:17:33Z
Sam Sailor
26820
Test
740030
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 500) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
er70utjkrunwonr9pmkr3mrkpdkf6b6
740032
740030
2026-05-01T16:20:53Z
Sam Sailor
26820
Test
740032
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 450) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
0lxei0uak0vyf0xdrq5plcko5kbd3eb
740033
740032
2026-05-01T16:22:29Z
Sam Sailor
26820
Test
740033
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 400) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
3l2bffhb1seg4g4y5d2gxh41k2kk4zc
740035
740033
2026-05-01T16:28:04Z
Sam Sailor
26820
Test
740035
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 300) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
5uyb83670z41i9agvegoi1wv7zpez4h
740036
740035
2026-05-01T16:30:24Z
Sam Sailor
26820
Test
740036
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 350) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
7wc0x3br7w9311g4q0t1zuih4242vxu
740049
740036
2026-05-01T17:32:26Z
Sam Sailor
26820
Test
740049
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 350) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').fadeOut(2000);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`);
if (!$link.length) return;
let label = '';
let style = 'font-size: 0.85em; margin-left: 8px; font-weight: normal;';
if (talkClass !== 'Stub' && talkClass !== 'Unrated' && hasStub) {
label = `${talkClass} (stub tag present) : ORES: ${predicted || '?'}`;
style += ' color: #f0ad4e;';
} else if (talkClass === 'Stub' && predicted && predicted !== 'Stub') {
label = `Stub : ${predicted}`;
style += ' color: #27ae60;';
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += ' color: #36c;';
}
if (label) {
$link.parent().find('.comrade-audit-label').remove();
const $span = $('<span>').addClass('comrade-audit-label').attr('style', style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Lead capitalization
window.ComradeLib.checkLeadCapitalization = function(currentTitle) {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
const titleElement = document.querySelector("#mw-content-text > div > p:not([class]) b");
if (!titleElement) return;
const leadTitle = titleElement.textContent.trim();
const articleTitle = currentTitle.trim();
if (leadTitle !== articleTitle && leadTitle.toLowerCase() === articleTitle.toLowerCase()) {
ComradeLib.renderLeadCapitalizationNudge(titleElement, leadTitle, articleTitle);
}
};
window.ComradeLib.renderLeadCapitalizationNudge = function(element, leadTitle, articleTitle) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Lead Capitalization:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append(
$('<span>').text(`Lead title uses incorrect casing (${leadTitle}). Correct to `),
$('<code>').text(articleTitle),
'?'
);
const $btn = $('<button>').text('Fix lead casing').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
const escaped = leadTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(`'''${escaped}'''`, 'g');
content = content.replace(searchRegex, `'''${articleTitle}'''`);
await api.postWithToken('csrf', {
action: 'edit',
title: title,
text: content,
summary: `Fix bold lead title capitalization per [[MOS:LEADTITLE]]`,
nocreate: true
});
mw.notify('Lead capitalization corrected!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update lead title capitalization.', {
type: 'error'
});
console.error(e);
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
3mxmvytrad4usnqw0trx3u4g1s2stqk
740133
740049
2026-05-02T08:45:14Z
Sam Sailor
26820
Test
740133
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 400) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').prop('disabled', true);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`).first();
if (!$link.length) {
return;
}
let label = "";
let style = "font-size: 0.85em; margin-left: 4px; font-weight: normal;";
if (talkClass !== "Stub" && talkClass !== "Unrated" && hasStub) {
label = `${talkClass} (stub tag present) : ${predicted || "?"}`;
style += " color: #f0ad4e;";
} else if (talkClass === "Stub" && predicted && predicted !== "Stub") {
label = `Stub : ${predicted}`;
style += " color: #27ae60;";
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += " color: #36c;";
}
if (label) {
const $parent = $link.parent();
$parent.find(".comrade-audit-label").remove();
const $span = $("<span>").addClass("comrade-audit-label").attr("style", style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Lead capitalization
window.ComradeLib.checkLeadCapitalization = function(currentTitle) {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
const titleElement = document.querySelector("#mw-content-text > div > p:not([class]) b");
if (!titleElement) return;
const leadTitle = titleElement.textContent.trim();
const articleTitle = currentTitle.trim();
if (leadTitle !== articleTitle && leadTitle.toLowerCase() === articleTitle.toLowerCase()) {
ComradeLib.renderLeadCapitalizationNudge(titleElement, leadTitle, articleTitle);
}
};
window.ComradeLib.renderLeadCapitalizationNudge = function(element, leadTitle, articleTitle) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Lead Capitalization:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text(`Lead title uses incorrect casing (${leadTitle}). Correct to `), $('<code>').text(articleTitle), '?');
const $btn = $('<button>').text('Fix lead casing').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
const escaped = leadTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(`'''${escaped}'''`, 'g');
content = content.replace(searchRegex, `'''${articleTitle}'''`);
await api.postWithToken('csrf', {
action: 'edit',
title: title,
text: content,
summary: `Fix bold lead title capitalization per [[MOS:LEADTITLE]]`,
nocreate: true
});
mw.notify('Lead capitalization corrected!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update lead title capitalization.', {
type: 'error'
});
console.error(e);
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
ngur8wq6f3wqr378e1fwsxiaxeh044j
740134
740133
2026-05-02T08:53:45Z
Sam Sailor
26820
Test
740134
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a');
const titles = $links.map((i, el) => $(el).attr('title')).get().filter(t => t && !t.startsWith('Talk:') && !t.startsWith('Template:'));
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 400) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').prop('disabled', true);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`).first();
if (!$link.length) {
return;
}
let label = "";
let style = "font-size: 0.85em; margin-left: 4px; font-weight: normal;";
if (talkClass !== "Stub" && talkClass !== "Unrated" && hasStub) {
label = `${talkClass} (stub tag present) : ${predicted || "?"}`;
style += " color: #f0ad4e;";
} else if (talkClass === "Stub" && predicted && predicted !== "Stub") {
label = `Stub : ${predicted}`;
style += " color: #27ae60;";
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += " color: #36c;";
}
if (label) {
const $parent = $link.parent();
$parent.find(".comrade-audit-label").remove();
const $span = $("<span>").addClass("comrade-audit-label").attr("style", style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Lead capitalization
window.ComradeLib.checkLeadCapitalization = function(currentTitle) {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
const titleElement = document.querySelector("#mw-content-text > div > p:not([class]) b");
if (!titleElement) return;
const leadTitle = titleElement.textContent.trim();
const articleTitle = currentTitle.trim();
if (leadTitle !== articleTitle && leadTitle.toLowerCase() === articleTitle.toLowerCase()) {
ComradeLib.renderLeadCapitalizationNudge(titleElement, leadTitle, articleTitle);
}
};
window.ComradeLib.renderLeadCapitalizationNudge = function(element, leadTitle, articleTitle) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Lead Capitalization:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text(`Lead title uses incorrect casing (${leadTitle}). Correct to `), $('<code>').text(articleTitle), '?');
const $btn = $('<button>').text('Fix lead casing').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
const escaped = leadTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(`'''${escaped}'''`, 'g');
content = content.replace(searchRegex, `'''${articleTitle}'''`);
await api.postWithToken('csrf', {
action: 'edit',
title: title,
text: content,
summary: `Fix bold lead title capitalization per [[MOS:LEADTITLE]]`,
nocreate: true
});
mw.notify('Lead capitalization corrected!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update lead title capitalization.', {
type: 'error'
});
console.error(e);
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
5ujbw6cze2v95tzy3nq0gvzjl7mh5lc
740135
740134
2026-05-02T08:59:13Z
Sam Sailor
26820
Test
740135
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a').filter(function() {
const title = $(this).attr('title');
return title && !title.startsWith('Talk:') && !title.startsWith('Template:');
});
const titles = $links.map((i, el) => $(el).attr('title')).get();
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 400) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').prop('disabled', true);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`).first();
if (!$link.length) {
return;
}
let label = "";
let style = "font-size: 0.85em; margin-left: 4px; font-weight: normal;";
if (talkClass !== "Stub" && talkClass !== "Unrated" && hasStub) {
label = `${talkClass} (stub tag present) : ${predicted || "?"}`;
style += " color: #f0ad4e;";
} else if (talkClass === "Stub" && predicted && predicted !== "Stub") {
label = `Stub : ${predicted}`;
style += " color: #27ae60;";
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += " color: #36c;";
}
if (label) {
const $parent = $link.parent();
$parent.find(".comrade-audit-label").remove();
const $span = $("<span>").addClass("comrade-audit-label").attr("style", style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Lead capitalization
window.ComradeLib.checkLeadCapitalization = function(currentTitle) {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
const titleElement = document.querySelector("#mw-content-text > div > p:not([class]) b");
if (!titleElement) return;
const leadTitle = titleElement.textContent.trim();
const articleTitle = currentTitle.trim();
if (leadTitle !== articleTitle && leadTitle.toLowerCase() === articleTitle.toLowerCase()) {
ComradeLib.renderLeadCapitalizationNudge(titleElement, leadTitle, articleTitle);
}
};
window.ComradeLib.renderLeadCapitalizationNudge = function(element, leadTitle, articleTitle) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Lead Capitalization:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text(`Lead title uses incorrect casing (${leadTitle}). Correct to `), $('<code>').text(articleTitle), '?');
const $btn = $('<button>').text('Fix lead casing').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
const escaped = leadTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(`'''${escaped}'''`, 'g');
content = content.replace(searchRegex, `'''${articleTitle}'''`);
await api.postWithToken('csrf', {
action: 'edit',
title: title,
text: content,
summary: `Fix bold lead title capitalization per [[MOS:LEADTITLE]]`,
nocreate: true
});
mw.notify('Lead capitalization corrected!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update lead title capitalization.', {
type: 'error'
});
console.error(e);
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
0z3nuolh0q6ekmgtq11y3xkyw9v5nzk
740136
740135
2026-05-02T09:04:01Z
Sam Sailor
26820
Test
740136
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a').filter(function() {
const title = $(this).attr('title') || '';
const href = $(this).attr('href') || '';
if (title.startsWith('Talk:') || title.startsWith('Template:')) return false;
if (href.includes('Template:') || href.includes('Template_talk:')) return false;
return true;
});
const titles = $links.map((i, el) => $(el).attr('title')).get();
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 400) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').prop('disabled', true);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`).first();
if (!$link.length) {
return;
}
let label = "";
let style = "font-size: 0.85em; margin-left: 3px; font-weight: normal;";
if (talkClass !== "Stub" && talkClass !== "Unrated" && hasStub) {
label = `${talkClass} (stub tag present) : ${predicted || "?"}`;
style += " color: #f0ad4e;";
} else if (talkClass === "Stub" && predicted && predicted !== "Stub") {
label = `Stub : ${predicted}`;
style += " color: #27ae60;";
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += " color: #36c;";
}
if (label) {
const $parent = $link.parent();
$parent.find(".comrade-audit-label").remove();
const $span = $("<span>").addClass("comrade-audit-label").attr("style", style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Lead capitalization
window.ComradeLib.checkLeadCapitalization = function(currentTitle) {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
const titleElement = document.querySelector("#mw-content-text > div > p:not([class]) b");
if (!titleElement) return;
const leadTitle = titleElement.textContent.trim();
const articleTitle = currentTitle.trim();
if (leadTitle !== articleTitle && leadTitle.toLowerCase() === articleTitle.toLowerCase()) {
ComradeLib.renderLeadCapitalizationNudge(titleElement, leadTitle, articleTitle);
}
};
window.ComradeLib.renderLeadCapitalizationNudge = function(element, leadTitle, articleTitle) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Lead Capitalization:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text(`Lead title uses incorrect casing (${leadTitle}). Correct to `), $('<code>').text(articleTitle), '?');
const $btn = $('<button>').text('Fix lead casing').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
const escaped = leadTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(`'''${escaped}'''`, 'g');
content = content.replace(searchRegex, `'''${articleTitle}'''`);
await api.postWithToken('csrf', {
action: 'edit',
title: title,
text: content,
summary: `Fix bold lead title capitalization per [[MOS:LEADTITLE]]`,
nocreate: true
});
mw.notify('Lead capitalization corrected!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update lead title capitalization.', {
type: 'error'
});
console.error(e);
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
gxjttxqkuivabfgfesf7v8ymfxczrva
740137
740136
2026-05-02T09:11:18Z
Sam Sailor
26820
Test
740137
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a').filter(function() {
const title = $(this).attr('title') || '';
if (title.startsWith('Talk:') || title.startsWith('Template:') || title.startsWith('Category:')) {
return false;
}
return true;
});
const titles = $links.map((i, el) => $(el).attr('title')).get();
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 400) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').prop('disabled', true);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`).first();
if (!$link.length) {
return;
}
let label = "";
let style = "font-size: 0.85em; margin-left: 3px; font-weight: normal;";
if (talkClass !== "Stub" && talkClass !== "Unrated" && hasStub) {
label = `${talkClass} (stub tag present) : ${predicted || "?"}`;
style += " color: #f0ad4e;";
} else if (talkClass === "Stub" && predicted && predicted !== "Stub") {
label = `Stub : ${predicted}`;
style += " color: #27ae60;";
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += " color: #36c;";
}
if (label) {
const $parent = $link.parent();
$parent.find(".comrade-audit-label").remove();
const $span = $("<span>").addClass("comrade-audit-label").attr("style", style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Lead capitalization
window.ComradeLib.checkLeadCapitalization = function(currentTitle) {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
const titleElement = document.querySelector("#mw-content-text > div > p:not([class]) b");
if (!titleElement) return;
const leadTitle = titleElement.textContent.trim();
const articleTitle = currentTitle.trim();
if (leadTitle !== articleTitle && leadTitle.toLowerCase() === articleTitle.toLowerCase()) {
ComradeLib.renderLeadCapitalizationNudge(titleElement, leadTitle, articleTitle);
}
};
window.ComradeLib.renderLeadCapitalizationNudge = function(element, leadTitle, articleTitle) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Lead Capitalization:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text(`Lead title uses incorrect casing (${leadTitle}). Correct to `), $('<code>').text(articleTitle), '?');
const $btn = $('<button>').text('Fix lead casing').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
const escaped = leadTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(`'''${escaped}'''`, 'g');
content = content.replace(searchRegex, `'''${articleTitle}'''`);
await api.postWithToken('csrf', {
action: 'edit',
title: title,
text: content,
summary: `Fix bold lead title capitalization per [[MOS:LEADTITLE]]`,
nocreate: true
});
mw.notify('Lead capitalization corrected!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update lead title capitalization.', {
type: 'error'
});
console.error(e);
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
aze4qhrlhzh5qdy78ybyl1jmwkhc07v
740138
740137
2026-05-02T09:23:46Z
Sam Sailor
26820
Test
740138
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a').filter(function() {
const title = $(this).attr('title') || '';
if (title.startsWith('Talk:') || title.startsWith('Template:')) {
return false;
}
return true;
});
const titles = $links.map((i, el) => $(el).attr('title')).get();
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 400) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').prop('disabled', true);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`).first();
if (!$link.length) {
return;
}
let label = "";
let style = "font-size: 0.85em; margin-left: 3px; font-weight: normal;";
if (talkClass !== "Stub" && talkClass !== "Unrated" && hasStub) {
label = `${talkClass} (stub tag present) : ${predicted || "?"}`;
style += " color: #f0ad4e;";
} else if (talkClass === "Stub" && predicted && predicted !== "Stub") {
label = `Stub : ${predicted}`;
style += " color: #27ae60;";
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += " color: #36c;";
}
if (label) {
const $parent = $link.parent();
$parent.find(".comrade-audit-label").remove();
const $span = $("<span>").addClass("comrade-audit-label").attr("style", style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Lead capitalization
window.ComradeLib.checkLeadCapitalization = function(currentTitle) {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
const titleElement = document.querySelector("#mw-content-text > div > p:not([class]) b");
if (!titleElement) return;
const leadTitle = titleElement.textContent.trim();
const articleTitle = currentTitle.trim();
if (leadTitle !== articleTitle && leadTitle.toLowerCase() === articleTitle.toLowerCase()) {
ComradeLib.renderLeadCapitalizationNudge(titleElement, leadTitle, articleTitle);
}
};
window.ComradeLib.renderLeadCapitalizationNudge = function(element, leadTitle, articleTitle) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Lead Capitalization:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text(`Lead title uses incorrect casing (${leadTitle}). Correct to `), $('<code>').text(articleTitle), '?');
const $btn = $('<button>').text('Fix lead casing').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
const escaped = leadTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(`'''${escaped}'''`, 'g');
content = content.replace(searchRegex, `'''${articleTitle}'''`);
await api.postWithToken('csrf', {
action: 'edit',
title: title,
text: content,
summary: `Fix bold lead title capitalization per [[MOS:LEADTITLE]]`,
nocreate: true
});
mw.notify('Lead capitalization corrected!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update lead title capitalization.', {
type: 'error'
});
console.error(e);
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
if (oresClass.toLowerCase() === talkClass?.toLowerCase()) return;
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (talkPage.missing || needsHighQualNudge || needsPromotionNudge || lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
let message = `ORES suggests <b>${oresClass}</b>. `;
if (talkPage.missing) {
message = `Talk page missing; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
} else if (needsHighQualNudge) {
message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
} else if (lingeringStub && !needsPromotionNudge) {
message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
} else if (needsPromotionNudge) {
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
2nrh40130va6x0zpjbc3668ibx6vp8k
740139
740138
2026-05-02T10:48:56Z
Sam Sailor
26820
Test
740139
javascript
text/javascript
//<nowiki>
/* global mw, $, ComradeLib */
/**
* Comrade-Lib
* Helper functions
*/
mw.util.addCSS(`
.ambox-Orphan { display: block !important; }
.comrade-container {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 2px;
padding: 10px 15px; margin: 10px 0; display: flex;
flex-direction: column; align-items: flex-start; gap: 8px;
border: 1px solid #a2a9b1; background: #fcfcfc; line-height: 1.5;
}
.comrade-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.comrade-content {
display: flex; flex-direction: column;
gap: 5px; width: 100%;
}
.comrade-success { background-color: #d5f5e3 !important; border-left: 5px solid #27ae60 !important; color: #1b5e20 !important; }
.comrade-nudge { background: #fff9ea; border-left: 5px solid #f0ad4e; }
.comrade-info, .comrade-status { background: #eaf3ff; border-left: 5px solid #36c; }
.comrade-container button:not(.comrade-dismiss) {
background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;
padding: 2px 10px; cursor: pointer; font-weight: bold; font-size: 0.95em; margin-left: 10px;
}
.comrade-container button:not(.comrade-dismiss):hover { border-color: #36c; color: #36c; background: #fff; }
.comrade-dismiss {
background: transparent; border: none; color: #d33; font-weight: bold;
cursor: pointer; font-size: 0.9em;
}
.comrade-dismiss:hover { text-decoration: underline; }
.comrade-code-block { background: #f1f1f1; padding: 4px 8px; border-radius: 3px; font-family: monospace; display: inline-block; margin-top: 4px; }
`);
window.ComradeLib = window.ComradeLib || {};
window.ComradeLib.api = new mw.Api();
window.ComradeLib.appendDismiss = function($el) {
$('<button>').addClass('comrade-dismiss').text('Dismiss').on('click', function() {
$(this).closest('.comrade-container').fadeOut();
}).appendTo($el.find('.comrade-header'));
$("#siteSub").after($el);
};
// Category audit
window.ComradeLib.arrayChunk = function(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
window.ComradeLib.fetchOresData = async function(revIds) {
const scores = {};
const BATCH = 1;
for (let i = 0; i < revIds.length; i += BATCH) {
const rid = revIds[i];
try {
const response = await fetch('https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict', {
method: 'POST',
body: JSON.stringify({
rev_id: parseInt(rid, 10)
})
});
if (!response.ok) {
console.warn(`Comrade: Lift Wing ${response.status} for rev ${rid}`);
continue;
}
const data = await response.json();
const prediction = data?.enwiki?.scores?.[String(rid)]?.articlequality?.score?.prediction ?? data?.output?.prediction ?? null;
if (prediction) scores[String(rid)] = prediction;
} catch (e) {
console.warn(`Comrade: Lift Wing fetch error for rev ${rid}`, e);
}
}
return scores;
};
window.ComradeLib.performCategoryAudit = async function() {
let timerInterval;
const api = new mw.Api();
const $links = $('#mw-pages li a').filter(function() {
const title = $(this).attr('title') || '';
if (title.startsWith('Talk:') || title.startsWith('Template:')) {
return false;
}
return true;
});
const titles = $links.map((i, el) => $(el).attr('title')).get();
if (!titles.length) return;
const $btn = $('#comrade-audit-btn');
const totalPages = titles.length;
let remainingTime = Math.ceil((totalPages * 400) / 1000);
$btn.prop('disabled', true).text(`Auditing (${remainingTime}s)...`);
timerInterval = setInterval(() => {
remainingTime -= 1;
if (remainingTime > 0) {
$btn.text(`Auditing (${remainingTime}s)...`);
} else {
clearInterval(timerInterval);
}
}, 1000);
const chunks = ComradeLib.arrayChunk(titles, 25);
const results = {};
try {
for (const chunk of chunks) {
const talkTitles = chunk.map(t => 'Talk:' + t);
const res = await api.get({
action: 'query',
titles: chunk.concat(talkTitles).join('|'),
prop: 'revisions',
rvprop: 'content|ids',
rvslots: 'main',
formatversion: 2
});
const pages = res.query.pages;
const getContent = pg => {
if (!pg || pg.missing || !pg.revisions) return '';
const rev = pg.revisions[0];
return rev.slots?.main?.content ?? rev.content ?? '';
};
const revIds = [];
const titleToRevId = {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
if (mainPg?.revisions?.[0]?.revid) {
const rid = String(mainPg.revisions[0].revid);
revIds.push(rid);
titleToRevId[title] = rid;
}
});
const oresScores = revIds.length ? await ComradeLib.fetchOresData(revIds) : {};
chunk.forEach(title => {
const mainPg = pages.find(p => p.title.toLowerCase() === title.toLowerCase() && !p.missing);
const talkPg = pages.find(p => p.title.toLowerCase() === ('talk:' + title).toLowerCase() && !p.missing);
const talkContent = getContent(talkPg);
const mainContent = getContent(mainPg);
const classMatch = talkContent.match(/\|\s*class\s*=\s*([^|{}\n\r]+)/i);
const rawClass = classMatch ? classMatch[1].trim() : '';
const talkClass = rawClass ? rawClass.charAt(0).toUpperCase() + rawClass.slice(1).toLowerCase() : 'Unrated';
const hasStubTags = /\{\{[^}]*stub[^}]*\}\}/i.test(mainContent);
const rid = titleToRevId[title];
const predicted = rid ? (oresScores[rid] ?? null) : null;
results[title] = {
talkClass,
predicted,
hasStubTags
};
});
}
Object.entries(results).forEach(([title, {
talkClass,
predicted,
hasStubTags
}]) => {
ComradeLib.renderCategoryAuditUI(title, talkClass, predicted, hasStubTags);
});
clearInterval(timerInterval);
$btn.text('Audit complete').prop('disabled', true);
} catch (error) {
clearInterval(timerInterval);
console.error('Comrade: Audit error', error);
$btn.text('Audit failed').prop('disabled', false);
}
};
window.ComradeLib.renderCategoryAuditUI = function(title, talkClass, predicted, hasStub) {
const $link = $(`#mw-pages li a[title="${title}"]`).first();
if (!$link.length) {
return;
}
let label = "";
let style = "font-size: 0.85em; margin-left: 3px; font-weight: normal;";
if (talkClass !== "Stub" && talkClass !== "Unrated" && hasStub) {
label = `${talkClass} (stub tag present) : ${predicted || "?"}`;
style += " color: #f0ad4e;";
} else if (talkClass === "Stub" && predicted && predicted !== "Stub") {
label = `Stub : ${predicted}`;
style += " color: #27ae60;";
} else if (predicted && predicted !== talkClass) {
label = `${talkClass} : ${predicted}`;
style += " color: #36c;";
}
if (label) {
const $parent = $link.parent();
$parent.find(".comrade-audit-label").remove();
const $span = $("<span>").addClass("comrade-audit-label").attr("style", style).text(` [${label}]`);
$link.after($span);
}
};
// Category audit END
// Deorphanizer
// performDeorphan
window.ComradeLib.performDeorphan = async function() {
try {
await ComradeLib.api.edit(mw.config.get('wgPageName'), function(rev) {
let text = rev.content;
const summary = 'Article has backlinks; removed [[Template:Orphan|{{Orphan}}]]';
text = text.replace(/\{\{Orphan\s*(?:\|[^}]*)?\}\}\s*/gi, '');
text = text.replace(/(\{\{Multiple[ _]issues\s*)\|\s*\|/gi, '$1|');
text = ComradeLib.replaceMI(text);
text = text.replace(/\n{3,}/g, '\n\n').trim();
return {
text,
summary,
minor: true
};
});
mw.notify('Orphan tag removed!', {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to remove orphan tag.', {
type: 'error'
});
}
};
// replaceMI (Multiple Issues)
window.ComradeLib.replaceMI = function(text) {
const miRegex = /\{\{Multiple[ _]issues\s*\|/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = miRegex.exec(text)) !== null) {
const start = match.index;
result += text.slice(lastIndex, start);
let depth = 0;
let i = start;
while (i < text.length) {
if (text[i] === '{' && text[i + 1] === '{') {
depth++;
i += 2;
} else if (text[i] === '}' && text[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
const end = i;
const full = text.slice(start, end);
const pipeIdx = full.indexOf('|');
const inner = full.slice(pipeIdx + 1, full.length - 2).trim();
const topLevel = ComradeLib.extractTopLevelTemplates(inner);
if (topLevel.length === 0) {
result += '';
} else if (topLevel.length === 1) {
result += topLevel[0].trim();
} else {
result += full;
}
lastIndex = end;
}
result += text.slice(lastIndex);
return result;
};
// extractTopLevelTemplates
window.ComradeLib.extractTopLevelTemplates = function(inner) {
const templates = [];
let i = 0;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
let depth = 0;
const start = i;
while (i < inner.length) {
if (inner[i] === '{' && inner[i + 1] === '{') {
depth++;
i += 2;
} else if (inner[i] === '}' && inner[i + 1] === '}') {
depth--;
i += 2;
if (depth === 0) break;
} else {
i++;
}
}
templates.push(inner.slice(start, i));
} else {
i++;
}
}
return templates;
};
// Lead capitalization
window.ComradeLib.checkLeadCapitalization = function(currentTitle) {
if (mw.config.get("wgNamespaceNumber") !== 0) return;
const titleElement = document.querySelector("#mw-content-text > div > p:not([class]) b");
if (!titleElement) return;
const leadTitle = titleElement.textContent.trim();
const articleTitle = currentTitle.trim();
if (leadTitle !== articleTitle && leadTitle.toLowerCase() === articleTitle.toLowerCase()) {
ComradeLib.renderLeadCapitalizationNudge(titleElement, leadTitle, articleTitle);
}
};
window.ComradeLib.renderLeadCapitalizationNudge = function(element, leadTitle, articleTitle) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Lead Capitalization:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text(`Lead title uses incorrect casing (${leadTitle}). Correct to `), $('<code>').text(articleTitle), '?');
const $btn = $('<button>').text('Fix lead casing').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
const escaped = leadTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const searchRegex = new RegExp(`'''${escaped}'''`, 'g');
content = content.replace(searchRegex, `'''${articleTitle}'''`);
await api.postWithToken('csrf', {
action: 'edit',
title: title,
text: content,
summary: `Fix bold lead title capitalization per [[MOS:LEADTITLE]]`,
nocreate: true
});
mw.notify('Lead capitalization corrected!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update lead title capitalization.', {
type: 'error'
});
console.error(e);
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
// Quality suite
// getOresPrediction
window.ComradeLib.getOresPrediction = async function() {
const pageName = mw.config.get('wgPageName');
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'ids',
formatversion: 2
});
const page = pageData.query.pages[0];
const revId = page?.revisions?.[0]?.revid;
if (!revId) return null;
const scores = await ComradeLib.fetchOresData([String(revId)]);
const pred = scores[String(revId)];
if (!pred) return null;
return pred.charAt(0).toUpperCase() + pred.slice(1);
} catch (e) {
console.warn("Comrade: getOresPrediction failed to fetch via LiftWing", e);
return null;
}
};
// checkQualityConsistency
window.ComradeLib.checkQualityConsistency = async function() {
const oresClass = await ComradeLib.getOresPrediction();
if (!oresClass || oresClass === 'Stub') return;
const talkTitle = 'Talk:' + mw.config.get('wgPageName');
try {
const [talkRes, pageRes] = await Promise.all([
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkTitle,
rvprop: 'content',
formatversion: 2
}),
ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
})
]);
const talkPage = talkRes.query.pages[0];
const mainWikitext = pageRes.query.pages[0].revisions[0].content;
const hasStubTag = /\{\{[^}|]*stub[^}]*\}\}/i.test(mainWikitext);
let talkClass = null;
if (!talkPage.missing && talkPage.revisions) {
const classMatch = talkRes.query.pages[0].revisions[0].content.match(/\|\s*\n?\s*class\s*=\s*([^|}\s]+)/i);
if (classMatch) {
const rawClass = classMatch[1].trim().toLowerCase();
if (rawClass === 'ga') talkClass = 'Ga';
else if (rawClass === 'fa') talkClass = 'Fa';
else talkClass = rawClass.charAt(0).toUpperCase() + rawClass.slice(1);
}
}
console.log("Comrade Quality Check ORES vs Talk:", oresClass, talkClass);
const isStubRated = talkClass === 'Stub';
const isStartRated = talkClass === 'Start';
const needsPromotionNudge = (isStubRated && (oresClass === 'Start' || oresClass === 'C')) || (isStartRated && oresClass === 'C');
const needsHighQualNudge = (oresClass !== talkClass) && !needsPromotionNudge;
const lingeringStub = (talkClass && !isStubRated && hasStubTag);
if (oresClass.toLowerCase() === talkClass?.toLowerCase() && !lingeringStub) return;
if (lingeringStub) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
const message = `Article is ${talkClass}-class, but stub tags remain. `;
const $btnRemove = $('<button>').text('Remove stub tags').on('click', () => ComradeLib.performStubRemoval());
$content.append(message, $btnRemove);
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
} else if (talkPage.missing || !talkClass) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
const message = talkPage.missing ? `Talk page missing; ORES suggests <b>${oresClass}</b>. ` : `Talk page exists but is unassessed; ORES suggests <b>${oresClass}</b>. `;
const $btnRater = $('<button>').text('Open Rater').on('click', () => $('#ca-rater, #t-rater').find('a')[0].click());
$content.append(message, $btnRater);
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
} else if (needsPromotionNudge) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
const message = `ORES suggests <b>${oresClass}</b>. `;
const $btnTarget = $('<button>').text(`Promote to ${oresClass}`).on('click', () => ComradeLib.performPromotion(oresClass));
$content.append(message, $btnTarget);
if (isStubRated && oresClass === 'C') {
const $btnStart = $('<button>').text(`Promote to Start`).on('click', () => ComradeLib.performPromotion('Start'));
$content.append("\u00A0or\u00A0", $btnStart);
}
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
} else if (needsHighQualNudge) {
const $container = $('<div>').addClass('comrade-container comrade-info');
const $content = $('<div>').append($('<strong>').text('Quality:'), " ");
const message = `ORES predicts <b>${oresClass}</b>. Please assess manually. `;
const $btnOkay = $('<button>').text('OK').on('click', () => {
window.location.hash = "#content";
mw.notify("Review the criteria for " + oresClass + "-class.");
$container.find('.comrade-dismiss').click();
});
$content.append(message, $btnOkay);
$container.append($('<div>').addClass('comrade-header').append($content));
$('#content').prepend($container);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.error("Comrade quality check failed", e);
}
};
// performStubRemoval
window.ComradeLib.performStubRemoval = async function() {
try {
const pageData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
});
let wikitext = pageData.query.pages[0].revisions[0].content;
// 'g' flag is appropriate here for replacement
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const newWikitext = wikitext.replace(stubRegex, '').trim();
if (wikitext === newWikitext) {
mw.notify("No stub tags found to remove.");
return;
}
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: mw.config.get("wgPageName"),
text: newWikitext,
summary: `Removing lingering stub tags per ORES/Talk assessment`,
nocreate: true
});
location.reload();
} catch (err) {
console.error("Comrade failed to remove stub tags:", err);
mw.notify("Error removing stub tags.", {
type: 'error'
});
}
};
// performPromotion
window.ComradeLib.performPromotion = async function(newClass) {
const classLinks = {
'Start': '[[WP:STARTCLASS|Start-Class]]',
'C': '[[WP:CCLASS|C-Class]]',
'B': '[[WP:BCLASS|B-Class]]'
};
const linkedClass = classLinks[newClass] || newClass;
const pageName = mw.config.get('wgPageName');
const talkName = 'Talk:' + pageName;
try {
const talkData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: talkName,
rvprop: 'content',
formatversion: 2
});
let talkWikitext = talkData.query.pages[0].revisions[0].content;
const newTalkWikitext = talkWikitext.replace(/(\|class\s*=\s*)([^|}\s]+)/i, `$1${newClass}`);
if (talkWikitext !== newTalkWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: talkName,
text: newTalkWikitext,
summary: `Promoting article to ${linkedClass} per review and [[WP:ORES]]`
});
}
const artData = await ComradeLib.api.get({
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
formatversion: 2
});
let artWikitext = artData.query.pages[0].revisions[0].content;
const stubRegex = /\{\{[^}|]*stub[^}]*\}\}\s*\n?/gi;
const cleanArtWikitext = artWikitext.replace(stubRegex, '');
if (artWikitext !== cleanArtWikitext) {
await ComradeLib.api.postWithToken('csrf', {
action: 'edit',
title: pageName,
text: cleanArtWikitext,
summary: `Removing stub tags; article promoted to ${linkedClass}`,
nocreate: true
});
}
location.reload();
} catch (e) {
console.error("Promotion failed:", e);
mw.notify("Promotion failed during multi-step edit.", {
type: 'error'
});
}
};
// Quality suite END
// Redirects
// checkAndRenderRedirect
window.ComradeLib.checkAndRenderRedirect = async function(redirTitle, targetTitle, rCategory, labelText, customWikitext) {
const res = await ComradeLib.api.get({
action: 'query',
titles: redirTitle,
formatversion: 2
});
if (res.query.pages[0].missing) {
const safeId = "comrade-redir-" + redirTitle.replace(/[^a-z0-9]/gi, '-');
const $container = $('<div>').addClass('comrade-container comrade-info').attr('id', safeId);
const $header = $('<div>').addClass('comrade-header');
const summary = `Creating redirect to [[${targetTitle}]] (${labelText.toLowerCase()})`;
const $btn = $('<button>').text('Create redirect').on('click', () => ComradeLib.createModernRedirect(redirTitle, targetTitle, rCategory, summary, safeId, customWikitext));
$header.append($('<div>').append($('<strong>').text(`${labelText}: `), `Create redirect from `, $('<code>').text(redirTitle), $btn));
$container.append($header);
ComradeLib.appendDismiss($container);
}
};
// createModernRedirect
window.ComradeLib.createModernRedirect = async function(redirTitle, targetTitle, rCategory, customSummary, elementId, customWikitext) {
const api = ComradeLib.api;
const content = customWikitext || `#REDIRECT [[${targetTitle}]]\n\n{{Redirect category shell|\n{{${rCategory}}}\n}}`;
const summary = customSummary || `Creating redirect to [[${targetTitle}]]`;
try {
await api.postWithEditToken({
action: 'edit',
title: redirTitle,
text: content,
summary: summary,
createonly: true
});
mw.notify("Redirect created!", {
type: 'success',
tag: 'comrade',
classes: ['comrade-success']
});
$(`#${elementId}`).fadeOut();
} catch (e) {
mw.notify("Failed to create redirect.", {
type: 'error'
});
}
};
// Domain name
window.ComradeLib.checkDomainRedirect = async function(qid, currentTitle) {
let domainCandidate = "";
if (qid) {
try {
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetclaims',
entity: qid,
property: 'P856',
format: 'json',
origin: '*'
});
const res = await fetch(url).then(r => r.json());
if (res.claims?.P856) {
domainCandidate = res.claims.P856[0].mainsnak.datavalue.value;
}
} catch (e) {
console.warn("Comrade: Wikidata API call failed.");
}
}
if (!domainCandidate) {
domainCandidate = $(".infobox .url a").last().attr("href") || $(".official-website a").first().attr("href");
}
if (domainCandidate) {
try {
const url = new URL(domainCandidate);
let domain = url.hostname.replace(/^www\d*\./, "");
if (domain.includes(".") && url.pathname === "/") {
const citationURL = '/api/rest_v1/data/citation/mediawiki/' + encodeURIComponent(domainCandidate);
try {
const response = await fetch(citationURL);
if (response.ok) {
await ComradeLib.checkAndRenderRedirect(domain, currentTitle, "R from domain name", "Domain");
}
} catch (e) {
console.warn("Comrade: Citation API check failed.");
}
}
} catch (e) {
console.warn("Comrade: Error parsing URL object.");
}
}
};
// Long name
window.ComradeLib.getLongNameFromWikitext = function(wikitext) {
const firstLine = wikitext.split('\n').find(l => l.includes("'''"));
if (!firstLine) return null;
const boldMatch = firstLine.match(/'''(.+?)'''/);
if (!boldMatch) return null;
return boldMatch[1].replace(/\s*([“"«《„‹「『'].+?[”"»》”›」』']|\([^)\n]*\))\s*/g, ' ').replace(/\s+/g, ' ').trim();
};
// Expanded normalization
window.ComradeLib.getNormalizedTitles = function(currentTitle) {
const maps = {
all: {
'æ': 'ae',
'Æ': 'Ae',
'œ': 'oe',
'Œ': 'Oe',
'ij': 'ij',
'IJ': 'Ij',
'ß': 'ss',
'ẞ': 'SS',
'ð': 'd',
'Ð': 'D',
'ø': 'o',
'Ø': 'O',
'ł': 'l',
'Ł': 'L',
'þ': 'th',
'Þ': 'Th',
'ŋ': 'ng',
'Ŋ': 'Ng'
}
};
let cleanAscii = currentTitle.split('').map((char, index) => {
let rep = maps.all[char];
if (rep) {
return (index > 0 && rep.length > 1) ? rep.toLowerCase() : rep;
}
return char;
}).join('');
cleanAscii = cleanAscii.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return {
ascii: (cleanAscii.toLowerCase() !== currentTitle.toLowerCase()) ? cleanAscii : null
};
};
// Redirects END
// SD
window.ComradeLib.checkShortDescDates = async function(wikitext, entity) {
const sdMatch = wikitext.match(/\{\{[Ss]hort description\|([^|{}]+)(?:\|[^}]*)?\}\}/);
if (!sdMatch) return;
const currentSD = sdMatch[1].trim();
const dateRegex = /\((?:born |died |fl\. )?\d{3,4}(?:–\d{3,4}|s)?\)/i;
const hasDate = dateRegex.test(currentSD);
if (hasDate && currentSD.length > 15) return;
const getYear = (val) => {
if (!val || !val.time) return null;
const yearMatch = val.time.match(/[+-](\d{4,})/);
if (!yearMatch) return null;
let year = parseInt(yearMatch[1]);
if (val.time.startsWith('-')) return year + " BC";
if (val.precision === 8) return year + "s";
return year;
};
let dateStr = "";
const bYear = getYear(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value);
const dYear = getYear(entity.claims?.P570?.[0]?.mainsnak?.datavalue?.value);
if (bYear && dYear) dateStr = `(${bYear}–${dYear})`;
else if (bYear) dateStr = `(born ${bYear})`;
else if (dYear) dateStr = `(died ${dYear})`;
else {
const flYear = getYear(entity.claims?.P1317?.[0]?.mainsnak?.datavalue?.value);
if (flYear) dateStr = `(fl. ${flYear})`;
}
if (!dateStr) return;
let finalSD = currentSD;
const words = currentSD.split(' ');
if (words.length === 1 && !hasDate) {
const occupationId = entity.claims?.P106?.[0]?.mainsnak?.datavalue?.value?.id;
if (occupationId) {
const occRes = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${occupationId}&props=labels&languages=en&format=json&origin=*`).then(r => r.json());
const occLabel = occRes.entities[occupationId]?.labels?.en?.value;
if (occLabel) {
finalSD = `${occLabel.charAt(0).toLowerCase() + occLabel.slice(1)} ${dateStr}`;
}
} else {
finalSD = `${currentSD} ${dateStr}`;
}
} else if (!hasDate) {
finalSD = `${currentSD} ${dateStr}`;
}
if (finalSD !== currentSD) {
ComradeLib.renderSDNudge(currentSD, finalSD);
}
};
window.ComradeLib.renderSDNudge = function(oldSD, newSD) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text('Short Description:'));
const $content = $('<div>').addClass('comrade-content').css({
'flex-direction': 'row',
'align-items': 'center',
'flex-wrap': 'wrap',
'gap': '10px'
});
$content.append($('<span>').text('Dates missing: '), $('<code>').text(newSD));
const $btn = $('<button>').text('Update').on('click', async function() {
const api = new mw.Api();
try {
const title = mw.config.get("wgPageName");
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = res.query.pages[0].revisions[0].content;
content = content.replace(/(\{\{[Ss]hort description\|)[^|}]+/, `$1${newSD}`);
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Updating short description: +dates/role per [[WP:SDFORMAT]]`,
nocreate: true
});
mw.notify('Short description updated!');
setTimeout(() => location.reload(), 700);
} catch (e) {
mw.notify('Failed to update SD.', {
type: 'error'
});
}
});
$content.append($btn);
$container.append($header, $content);
ComradeLib.appendDismiss($container);
};
//</nowiki>
cj7281t7na53i76oavhh2k0ch8k0vhd
Odetari
0
175070
739995
739897
2026-05-01T13:34:18Z
Cryptocurrency777
73698
739995
wikitext
text/x-wiki
#REDIRECT [[Narcistic Personality Disorder (song)]]
[[File:Odetari picture.webp]]
0dhed0dte83viohtiomhi1sw9182ayu
739996
739995
2026-05-01T13:43:13Z
Cryptocurrency777
73698
739996
wikitext
text/x-wiki
#REDIRECT [[Narcistic Personality Disorder (song)]]
[[File:Odetari picture.webp|150px]]
hyvaec8j7x0ao5x4sfr3sln8c0hmr6p
DaniDV
0
175071
739980
739899
2026-05-01T12:42:43Z
Cryptocurrency777
73698
739980
wikitext
text/x-wiki
''{{InfoboxTest|DaniDV|image=TestDaniDV.jpg|alias=Darkphobia|years active=2024+|agent=-}}''
'''DaniDV''' (also known as Darkphobia), is a producer who made ''Decay'', duh
{{Did you mean/box}}
{{Welcome-copyright}}
di85xyjcjq6eri4dx76mdj7kh4s80a8
739997
739980
2026-05-01T13:44:24Z
Cryptocurrency777
73698
739997
wikitext
text/x-wiki
''{{InfoboxTest|DaniDV|image=TestDaniDV.jpg|alias=Darkphobia|years active=2024+|agent=-}}''
'''DaniDV''' (also known as Darkphobia), is a producer who made a song called ''Decay''. He is known for EDM music.
huh
{{Did you mean/box}}
{{Welcome-copyright}}
s1pstqexji4ixvqna5j1cd92iyj7ln9
740000
739997
2026-05-01T13:56:07Z
Cryptocurrency777
73698
740000
wikitext
text/x-wiki
''{{InfoboxTest|DaniDV|image=TestDaniDV.jpg|alias=Darkphobia|years active=2024+|agent=-}}''
'''DaniDV''' (also known as Darkphobia), is a producer who made a song called ''[[Decay (song)|Decay]]''. He is known for EDM music.
huh
{{Did you mean/box}}
{{Welcome-copyright}}
tkmxvnkf1tt03mrotpn433q6bqhximt
Anna Kournikova
0
175078
739974
2026-05-01T12:24:07Z
Cryptocurrency777
73698
Created page with "{{Infobox person |name = Anna Kournikova |image = Anna Kournikova-Bagram Airfield 2009.jpg |caption = Kournikova at [[Bagram Air Base]] during a 2009 [[United Service Organization|USO]] tour |native_name = {{nobold|Анна Курникова}} |native_name_lang = ru |birth_date = {{birth date and age|df=y|1981|06|07}} |birth_place = Moscow, [[Russian Soviet Federative Socialist Republic|Russian SFSR]], Soviet Union |height = {{convert|1.73|m|ftin|abbr=on}} |partner = ..."
739974
wikitext
text/x-wiki
{{Infobox person
|name = Anna Kournikova
|image = Anna Kournikova-Bagram Airfield 2009.jpg
|caption = Kournikova at [[Bagram Air Base]] during a 2009 [[United Service Organization|USO]] tour
|native_name = {{nobold|Анна Курникова}}
|native_name_lang = ru
|birth_date = {{birth date and age|df=y|1981|06|07}}
|birth_place = Moscow, [[Russian Soviet Federative Socialist Republic|Russian SFSR]], Soviet Union
|height = {{convert|1.73|m|ftin|abbr=on}}
|partner = [[Enrique Iglesias]] (2001–present)
|module =
{{Infobox tennis biography | embed=yes
|fullname =
|country = Russia
|turnedpro = October 1995
|retired = May 2003
|plays = Right-handed (two-handed backhand)
|careerprizemoney = US$3,584,662
|singlesrecord = {{tennis record|won=209|lost=129}}
|singlestitles = 0
|highestsinglesranking = No. 8 (20 November 2000)
|AustralianOpenresult = QF ([[2001 Australian Open – Women's singles|2001]])
|FrenchOpenresult = 4R ([[1998 French Open – Women's singles|1998]], [[1999 French Open – Women's singles|1999]])
|Wimbledonresult = SF ([[1997 Wimbledon Championships – Women's singles|1997]])
|USOpenresult = 4R ([[1996 US Open – Women's singles|1996]], [[1998 US Open – Women's singles|1998]])
|Othertournaments = yes
|WTAChampionshipsresult = SF ([[2000 WTA Tour Championships|2000]])
|Olympicsresult = 1R ([[Tennis at the 1996 Summer Olympics – Women's singles|1996]])
|doublesrecord = 200–71
|doublestitles = 16
|highestdoublesranking = [[List of WTA number 1 ranked doubles tennis players|No. '''1''']] (22 November 1999)
|AustralianOpenDoublesresult = '''W''' ([[1999 Australian Open – Women's doubles|1999]], [[2002 Australian Open – Women's doubles|2002]])
|FrenchOpenDoublesresult = F ([[1999 French Open – Women's doubles|1999]])
|WimbledonDoublesresult = SF ([[2000 Wimbledon Championships – Women's doubles|2000]], [[2002 Wimbledon Championships – Women's doubles|2002]])
|USOpenDoublesresult = QF ([[1996 US Open – Women's doubles|1996]], [[2002 US Open – Women's doubles|2002]])
|OthertournamentsDoubles = yes
|WTAChampionshipsDoublesresult = '''W''' ([[1999 WTA Tour Championships – Doubles|1999]], [[2000 WTA Tour Championships – Doubles|2000]])
|Mixed = yes
|mixedrecord = 24–14
|mixedtitles =
|AustralianOpenMixedresult = SF ([[1997 Australian Open – Mixed doubles|1997]], [[2000 Australian Open – Mixed doubles|2000]])
|FrenchOpenMixedresult = QF ([[1997 French Open – Mixed doubles|1997]])
|WimbledonMixedresult = F ([[1999 Wimbledon Championships – Mixed doubles|1999]])
|USOpenMixedresult = F ([[2000 US Open – Mixed doubles|2000]])
}}
}}
'''Anna Sergeyevna Kournikova Iglesias''' ({{nee|'''Kournikova'''}}; {{langx|ru|Анна Сергеевна Курникова}}; {{IPA|ru|ˈanːə sʲɪrˈɡʲejɪvnə ˈkurnʲɪkəvə|lang|Anna_kournikova.ogg}}; born 7 June 1981) is a Russian model and television personality, and former professional tennis player. Her appearance <!-- Per WP:NPOV, do not label her appearance as "beauty" or anything else --> and celebrity status made her one of the best known tennis stars worldwide. At the peak of her fame, fans looking for images of Kournikova made her name one of the most common search strings on [[Google Search]].<ref name=2001EOY>{{cite web|url=http://www.google.com/press/zeitgeist2001.html|title=2001 Year-End Google Zeitgeist: Search patterns, trends, and surprises|access-date=8 July 2009}}</ref><ref name=2002EOY>{{cite web|url=http://www.google.com/press/zeitgeist2002.html|title=2002 Year-End Google Zeitgeist: Search patterns, trends, and surprises|access-date=8 July 2009}}</ref><ref name=2003EOY>{{cite web|url=http://www.google.com/intl/en/press/zeitgeist2003.html|title=2003 Year-End Google Zeitgeist: Search patterns, trends, and surprises|access-date=9 July 2009}}</ref>
3vhqqm3p1zolnwt4elakzsmvmzsgx65
Jumpstyle
0
175079
739976
2026-05-01T12:29:09Z
Cryptocurrency777
73698
Created page with "'''Jumpstyle''' is a genre of music and dance that comes from Belgium and the Netherlands. It is considered a subcategory of EDM (Electronic Dance Music) and Eurodance. It came from the 90s and 200s but got revived in the 2020s. A good example would be Scooter’s "Jumping All Over the World,". [[Category:Music]]"
739976
wikitext
text/x-wiki
'''Jumpstyle''' is a genre of music and dance that comes from Belgium and the Netherlands. It is considered a subcategory of EDM (Electronic Dance Music) and Eurodance. It came from the 90s and 200s but got revived in the 2020s. A good example would be Scooter’s "Jumping All Over the World,".
[[Category:Music]]
rrg134b9c1nibxfvsjv0qnqq1kjz4dq
739977
739976
2026-05-01T12:30:19Z
Cryptocurrency777
73698
739977
wikitext
text/x-wiki
'''Jumpstyle''' is a genre of music and dance that comes from Belgium and the Netherlands. It is considered a subcategory of EDM (Electronic Dance Music) and Eurodance. It came from the 90s and 200s but got revived in the 2020s. A good example would be Scooter’s "Jumping All Over the World,".
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
[[Category:Music]]
ogat1az9na5rljw2rjnnnz99oj08cx0
739978
739977
2026-05-01T12:35:34Z
Cryptocurrency777
73698
739978
wikitext
text/x-wiki
'''Jumpstyle''' is a genre of music and dance that comes from Belgium and the Netherlands. It is considered a subcategory of EDM (Electronic Dance Music) and Eurodance. It came from the 90s and 200s but got revived in the 2020s. A good example would be Scooter’s "Jumping All Over the World,".
<span style="color:#000;font-family:Comic Sans MS;font-size:80%;border-radius:30px;background-color:#FCCCCF;padding:6px 7px 5px 7px">JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP
JUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMPJUMP</span>
[[Category:Music]]
sjz0ew04w9h2vsfmjcmon0g9d0bn0nw
File:TestDaniDV.jpg
6
175080
739979
2026-05-01T12:40:11Z
Cryptocurrency777
73698
wu.weee drew it
739979
wikitext
text/x-wiki
== Summary ==
wu.weee drew it
== Licensing ==
{{Fair use}}
e91s2dnfl8a6mtvmzlzrnx4on1ct77o
User:ToprakM/test.css
2
175081
739985
2026-05-01T13:13:32Z
ToprakM
35316
Created page with "background-color: red;"
739985
css
text/css
background-color: red;
9z9tdr0fszlqar9tkt102o4s3fvygh8
739987
739985
2026-05-01T13:15:18Z
ToprakM
35316
739987
css
text/css
body, #mw-page-base, #content {
background-color: red !important;
}
ei59pbcqxhprp1yq58avwa1olv3cgn4
740025
739987
2026-05-01T16:08:19Z
ToprakM
35316
740025
css
text/css
body, #mw-page-base, #content {
background-color: red !important;
}
/* dark mode */
@media screen {
html.skin-theme-clientpref-os,
html.skin-theme-clientpref-night {
background-color: purple !important;
}
3otzvfrt0vaswxo7jh6qpbdtwn6vjs5
740026
740025
2026-05-01T16:08:56Z
ToprakM
35316
740026
css
text/css
body, #mw-page-base, #content {
background-color: red !important;
}
/* dark mode */
@media screen {
html.skin-theme-clientpref-os,
html.skin-theme-clientpref-night {
background-color: purple !important;
}
}
nvabzcvvkvityceetlstvxg2i04xfhp
740027
740026
2026-05-01T16:09:12Z
ToprakM
35316
740027
css
text/css
/* dark mode */
@media screen {
html.skin-theme-clientpref-os,
html.skin-theme-clientpref-night {
background-color: purple !important;
}
}
pl0ew2hjyiqspuytot1ukyi6f5zwafa
740028
740027
2026-05-01T16:09:42Z
ToprakM
35316
740028
css
text/css
@media screen {
html.skin-theme-clientpref-night {
background-color: purple !important;
}
html.skin-theme-clientpref-night {
background-color: purple !important;
}
}
ed96o9bfg5he234qj8wgrk9g4z0mlu6
740029
740028
2026-05-01T16:12:03Z
ToprakM
35316
740029
css
text/css
body, #mw-page-base, #content {
background-color: red !important;
}
@media screen {
html.skin-theme-clientpref-night,
html.skin-theme-clientpref-os {
body, #mw-page-base, #content {
background-color: purple !important;
}
}
}
jj76tmplib65a21873iit4ay7kd3g3a
740031
740029
2026-05-01T16:20:52Z
ToprakM
35316
740031
css
text/css
/* 1. Varsayılan Durum (Light Mode) */
body,
#mw-page-base,
#content {
background-color: red !important;
}
/* 2. Kullanıcı MANUEL olarak Gece Modunu (Night Mode) seçtiğinde */
html.skin-theme-clientpref-night body,
html.skin-theme-clientpref-night #mw-page-base,
html.skin-theme-clientpref-night #content {
background-color: purple !important;
}
/* 3. Kullanıcı "OS Tercihi"ni seçtiğinde VE işletim sistemi gerçekten Gece Modunda olduğunda */
@media (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os body,
html.skin-theme-clientpref-os #mw-page-base,
html.skin-theme-clientpref-os #content {
background-color: purple !important;
}
}
cqr95imdga3m2ub39lw69zaexiggpne
740034
740031
2026-05-01T16:24:26Z
ToprakM
35316
740034
css
text/css
body,
#mw-page-base,
#content {
background-color: red !important;
}
html.skin-theme-clientpref-night body,
html.skin-theme-clientpref-night #mw-page-base,
html.skin-theme-clientpref-night #content {
background-color: purple !important;
}
@media (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os body,
html.skin-theme-clientpref-os #mw-page-base,
html.skin-theme-clientpref-os #content {
background-color: purple !important;
}
}
i1iejd96ku3f5vvgoti19zaqvz28lzs
File:TestDecayCover.jpeg
6
175082
739998
2026-05-01T13:49:06Z
Cryptocurrency777
73698
fair use test
739998
wikitext
text/x-wiki
== Summary ==
fair use test
== Licensing ==
{{Fair use}}
sg92p7hcmrtlxviqbtwt49d20ckqsgt
Decay (song)
0
175083
739999
2026-05-01T13:55:23Z
Cryptocurrency777
73698
Created page with "{{Infobox song|Name=Decay|cover=TestDecayCover.jpeg|cover size=250px|Artist=DaniDV, AfterDRK, Xaduma, Xyle, Anti-Light|Released=March 2025|Length=2:36|Writer=AfterDRK, Xaduma, Xyle, Anti-Light}} '''Decay''' is a song by AfterDRK, Xaduma, Xyle, and Anti-Light released in March 2025. It was produced by [[DaniDV]]. It has a BPM of 145<ref>{{Citation | title=DECAY - AfterDRK, Anti-Light, DaniDV, Xyle, xaduma | url=https://tunebat.com/Info/DECAY-AfterDRK-Anti-Light-DaniDV-X..."
739999
wikitext
text/x-wiki
{{Infobox song|Name=Decay|cover=TestDecayCover.jpeg|cover size=250px|Artist=DaniDV, AfterDRK, Xaduma, Xyle, Anti-Light|Released=March 2025|Length=2:36|Writer=AfterDRK, Xaduma, Xyle, Anti-Light}}
'''Decay''' is a song by AfterDRK, Xaduma, Xyle, and Anti-Light released in March 2025. It was produced by [[DaniDV]]. It has a BPM of 145<ref>{{Citation | title=DECAY - AfterDRK, Anti-Light, DaniDV, Xyle, xaduma | url=https://tunebat.com/Info/DECAY-AfterDRK-Anti-Light-DaniDV-Xyle-xaduma/6bBm0o4a7bkTtvohndLh6A}}</ref>
8bepcccrmtuo59bu57iitm75l979vt8
740006
739999
2026-05-01T14:20:43Z
Cryptocurrency777
73698
740006
wikitext
text/x-wiki
{{Infobox song|Name=Decay|cover=TestDecayCover.jpeg|cover size=250px|Artist=DaniDV, AfterDRK, Xaduma, Xyle, Anti-Light|Released=March 2025|Length=2:36|Writer=AfterDRK, Xaduma, Xyle, Anti-Light}}
'''Decay''' is a song by AfterDRK, Xaduma, Xyle, and Anti-Light released in March 2025. It was produced by [[DaniDV]]. It has a BPM of 145<ref>{{Citation | title=DECAY - AfterDRK, Anti-Light, DaniDV, Xyle, xaduma | url=https://tunebat.com/Info/DECAY-AfterDRK-Anti-Light-DaniDV-Xyle-xaduma/6bBm0o4a7bkTtvohndLh6A}}</ref>. It contains elements of EDM and discusses topics like [[relationships]] and self harm.
5l3bknl7miatjp195qmyd4r9g5gyxri
740039
740006
2026-05-01T17:07:32Z
Cryptocurrency777
73698
740039
wikitext
text/x-wiki
{{Infobox song|Name=Decay|cover=TestDecayCover.jpeg|cover size=250px|Artist=DaniDV, AfterDRK, Xaduma, Xyle, Anti-Light|Released=March 2025|Length=2:36|Writer=AfterDRK, Xaduma, Xyle, Anti-Light}}
'''Decay''' is a song by AfterDRK, Xaduma, Xyle, and Anti-Light released in March 2025. It was produced by [[DaniDV]]. It has a BPM of 145<ref>{{Citation | title=DECAY - AfterDRK, Anti-Light, DaniDV, Xyle, xaduma | url=https://tunebat.com/Info/DECAY-AfterDRK-Anti-Light-DaniDV-Xyle-xaduma/6bBm0o4a7bkTtvohndLh6A}}</ref>. It contains elements of EDM and discusses topics like [[relationships]] and self harm. Rox3s was meant to be a feature on this song, but he never showed up for the open.
ioz98seke45b892utsxiqdy84vwooux
740042
740039
2026-05-01T17:14:38Z
Cryptocurrency777
73698
740042
wikitext
text/x-wiki
{{Infobox song|Name=Decay|cover=TestDecayCover.jpeg|cover size=250px|Artist=DaniDV, AfterDRK, Xaduma, Xyle, Anti-Light|Released=March 2025|Length=2:36|Writer=AfterDRK, Xaduma, Xyle, Anti-Light}}
'''Decay''' is a song by AfterDRK, Xaduma, Xyle, and Anti-Light released in March 2025. It was produced by [[DaniDV]]. It has a BPM of 145<ref>{{Citation | title=DECAY - AfterDRK, Anti-Light, DaniDV, Xyle, xaduma | url=https://tunebat.com/Info/DECAY-AfterDRK-Anti-Light-DaniDV-Xyle-xaduma/6bBm0o4a7bkTtvohndLh6A}}</ref>. It contains elements of EDM and discusses topics like [[relationships]] and self harm. Rox3s was meant to be a feature on this song, but he never showed up for the open. The song has at least 150,000 plays on Spotify.
gkbn3vtwzbrhgea7mvzroloeoitebm0
740043
740042
2026-05-01T17:15:30Z
Cryptocurrency777
73698
740043
wikitext
text/x-wiki
{{Infobox song|Name=Decay|cover=TestDecayCover.jpeg|cover size=250px|Artist=DaniDV, AfterDRK, Xaduma, Xyle, Anti-Light|Released=March 2025|Length=2:36|Writer=AfterDRK, Xaduma, Xyle, Anti-Light}}
'''Decay''' is a single by AfterDRK, Xaduma, Xyle, and Anti-Light released in March 7, 2025. It was produced by [[DaniDV]]. It has a BPM of 145<ref>{{Citation | title=DECAY - AfterDRK, Anti-Light, DaniDV, Xyle, xaduma | url=https://tunebat.com/Info/DECAY-AfterDRK-Anti-Light-DaniDV-Xyle-xaduma/6bBm0o4a7bkTtvohndLh6A}}</ref>. It contains elements of EDM and discusses topics like [[relationships]] and self harm. Rox3s was meant to be a feature on this song, but he never showed up for the open. The song has at least 150,000 plays on Spotify.
s756943b06xnnyknjzyf4m3412qc21i
740056
740043
2026-05-01T18:37:11Z
Cryptocurrency777
73698
740056
wikitext
text/x-wiki
{{Infobox song|Name=Decay|cover=TestDecayCover.jpeg|cover size=250px|Artist=[[DaniDV]], AfterDRK, Xaduma, Xyle, Anti-Light|Released=March 2025|Length=2:36|Writer=AfterDRK, Xaduma, Xyle, Anti-Light}}
'''Decay''' is a single by AfterDRK, Xaduma, Xyle, and Anti-Light released in March 7, 2025. It was produced by [[DaniDV]]. It has a BPM of 145<ref>{{Citation | title=DECAY - AfterDRK, Anti-Light, DaniDV, Xyle, xaduma | url=https://tunebat.com/Info/DECAY-AfterDRK-Anti-Light-DaniDV-Xyle-xaduma/6bBm0o4a7bkTtvohndLh6A}}</ref>. It contains elements of EDM and discusses topics like [[relationships]] and self harm. Rox3s was meant to be a feature on this song, but he never showed up for the open. The song has at least 150,000 plays on Spotify.
guig8u2f0fetgjk2nlihv13cbgpvx5d
We Can't Be Friends (Wait for Your Love)
0
175084
740001
2026-05-01T14:01:04Z
Cryptocurrency777
73698
Created page with "{{Infobox song|Name=We Can't Be Friends (Wait For Your Love)|Artist=Ariana Grande|Released=2024|Writer=* Ariana Grande * [[Max Martin]] * [[Ilya Salmanzadeh]]}} "'''We Can't Be Friends (Wait for Your Love)'''"<!-- do not add stylization here, refer to [[MOS: MUSICCAPS]] --> is a song by American singer-songwriter [[Ariana Grande]]. It was released through [[Republic Records]] on March 8, 2024, as the second single from her seventh studio album, ''Eternal Sunshine (a..."
740001
wikitext
text/x-wiki
{{Infobox song|Name=We Can't Be Friends (Wait For Your Love)|Artist=Ariana Grande|Released=2024|Writer=* Ariana Grande
* [[Max Martin]]
* [[Ilya Salmanzadeh]]}}
"'''We Can't Be Friends (Wait for Your Love)'''"<!-- do not add stylization here, refer to [[MOS: MUSICCAPS]] --> is a song by American singer-songwriter [[Ariana Grande]]. It was released through [[Republic Records]] on March 8, 2024, as the second single from her seventh studio album, ''[[Eternal Sunshine (album)|Eternal Sunshine]]'' (2024). It is written and produced by Grande, [[Max Martin]] and [[Ilya Salmanzadeh]]. Built on an [[electropop]] beat, "We Can't Be Friends (Wait for Your Love)" is a [[Europop]], [[power pop]], [[synth-pop]], and [[synthwave]] song. The song's lyrics dually refer to the demise of a romantic relationship as well as Grande's relationship with the press.
i2tig22buolyxhu95n5j6mo7gnzyuw1
740002
740001
2026-05-01T14:01:20Z
Cryptocurrency777
73698
740002
wikitext
text/x-wiki
{{Infobox song|Name=We Can't Be Friends (Wait For Your Love)|Artist=Ariana Grande|Released=2024|Writer=* Ariana Grande
* [[Max Martin]]
* [[Ilya Salmanzadeh]]}}
"'''We Can't Be Friends (Wait for Your Love)'''"<!-- do not add stylization here, refer to [[MOS: MUSICCAPS]] --> is a song by American singer-songwriter [[Ariana Grande]]. It was released through [[Republic Records]] on March 8, 2024, as the second single from her seventh studio album, ''[[Eternal Sunshine (album)|Eternal Sunshine]]'' (2024). It is written and produced by Grande, [[Max Martin]] and [[Ilya Salmanzadeh]]. Built on an [[electropop]] beat, "We Can't Be Friends (Wait for Your Love)" is a [[Europop]], [[power pop]], [[synth-pop]], and [[synthwave]] song. The song's lyrics dually refer to the demise of a romantic relationship as well as Grande's relationship with the press.
[[Category:Music]]
ibdoheknudx3uquwdz5nwuqgcs40j3p
File:DiggyTest.jpg
6
175085
740003
2026-05-01T14:05:46Z
Cryptocurrency777
73698
fair use
740003
wikitext
text/x-wiki
== Summary ==
fair use
== Licensing ==
{{Fair use}}
1wgt6bicqvnlasoz382mqruylvek2pc
Diggy
0
175086
740004
2026-05-01T14:18:22Z
Cryptocurrency777
73698
Created page with "{{Infobox song|Name=Diggy|cover=DiggyTest.jpg|cover size=250px|Artist=Spencer Ludwig|Released=2016|Recorded=2016|Genre=Alternative|Writer=}} '''Diggy''' is a single released by Spencer Ludwig of Capital Cities. It was written by Spencer Ludwig. It was recorded and released by Ludwig in 2016 and contains trumpet elements. Its lyrics often reference dancing and showing off moves. The song was later featured in the video game ''Just Dance 2017''."
740004
wikitext
text/x-wiki
{{Infobox song|Name=Diggy|cover=DiggyTest.jpg|cover size=250px|Artist=Spencer Ludwig|Released=2016|Recorded=2016|Genre=Alternative|Writer=}}
'''Diggy''' is a single released by Spencer Ludwig of Capital Cities. It was written by Spencer Ludwig. It was recorded and released by Ludwig in 2016 and contains trumpet elements. Its lyrics often reference dancing and showing off moves. The song was later featured in the video game ''Just Dance 2017''.
fy9pptjok1es9xz7hmtr17511ubk9nh
740005
740004
2026-05-01T14:18:39Z
Cryptocurrency777
73698
740005
wikitext
text/x-wiki
{{Infobox song|Name=Diggy|cover=DiggyTest.jpg|cover size=250px|Artist=Spencer Ludwig|Released=2016|Recorded=2016|Genre=Alternative|Writer=}}
'''Diggy''' is a single released by [[Spencer Ludwig]] of Capital Cities. It was written by Spencer Ludwig. It was recorded and released by Ludwig in 2016 and contains trumpet elements. Its lyrics often reference dancing and showing off moves. The song was later featured in the video game ''Just Dance 2017''.
s8s74te01pjam9h5br8y5zqals9bztr
740017
740005
2026-05-01T15:17:32Z
Cryptocurrency777
73698
740017
wikitext
text/x-wiki
{{Infobox song|Name=Diggy|cover=DiggyTest.jpg|cover size=280px|Artist=Spencer Ludwig|Released=2016|Recorded=2016|Genre=Alternative|Writer=}}
'''Diggy''' is a single released by [[Spencer Ludwig]] of Capital Cities. It was written by Spencer Ludwig. It was recorded and released by Ludwig in 2016 and contains trumpet elements. Its lyrics often reference dancing and showing off moves. The song was later featured in the video game ''Just Dance 2017''.
8u6edfamfp2bo884rdlgrjioosgbivc
Kirby
0
175087
740009
2026-05-01T14:43:01Z
Cryptocurrency777
73698
Created page with "{{Infobox video game|release=1992+|image=Kirby-Logo-New.svg|image size=150px|developer=Nintendo}} Kirby is a video game platformer series made by Nintendo in 1992. The main character is Kirby, and he is a pink, round creature who can copy abilities"
740009
wikitext
text/x-wiki
{{Infobox video game|release=1992+|image=Kirby-Logo-New.svg|image size=150px|developer=Nintendo}}
Kirby is a video game platformer series made by Nintendo in 1992. The main character is Kirby, and he is a pink, round creature who can copy abilities
jbzjisol6wut8b7r0pg3ks6yr7jc2cw
Yak
0
175088
740010
2026-05-01T14:47:19Z
Cryptocurrency777
73698
Created page with "A domestic bovine animal ...................... [[File:Yak in Khövsgöl 01.jpg|thumb]]"
740010
wikitext
text/x-wiki
A domestic bovine animal ......................
[[File:Yak in Khövsgöl 01.jpg|thumb]]
q9calygvtm8lg1x58nczhjb6mt8nj2l
Oregon
0
175089
740011
2026-05-01T14:49:47Z
Cryptocurrency777
73698
Created page with "Like [[Alabama]], Oregon is one of the 50 states in [[USA]]"
740011
wikitext
text/x-wiki
Like [[Alabama]], Oregon is one of the 50 states in [[USA]]
1oujf6xl93ye171u17xb79zjnslwum5
Tesseract
0
175090
740012
2026-05-01T14:58:06Z
Cryptocurrency777
73698
Created page with "[[File:8-cell-simple.gif]] a Tesseract is a 4D cube that has 8 cubic cells, 24 square faces, 32 edges, and 16 vertices {{Did you know}}"
740012
wikitext
text/x-wiki
[[File:8-cell-simple.gif]]
a Tesseract is a 4D cube that has
8 cubic cells, 24 square faces, 32 edges, and 16 vertices
{{Did you know}}
2rvz3pxmdxo5y2vmdjjimn2fa9lyw5g
67
0
175091
740013
2026-05-01T15:10:04Z
Cryptocurrency777
73698
Created page with "67 ''67'' '''67''' == 67 == === 67 === = 67 = <span style="font-family:Comic Sans MS;>67</span> <span style="font-family:Tahoma;>67</span> <span style="font-family:Roboto;>67</span> is aa 2025 meme"
740013
wikitext
text/x-wiki
67
''67''
'''67'''
== 67 ==
=== 67 ===
= 67 =
<span style="font-family:Comic Sans MS;>67</span>
<span style="font-family:Tahoma;>67</span>
<span style="font-family:Roboto;>67</span>
is aa 2025 meme
oknm71rlahryrbnfd9lb4lhqe8uuo18
740014
740013
2026-05-01T15:10:12Z
Cryptocurrency777
73698
740014
wikitext
text/x-wiki
67
''67''
'''67'''
== 67 ==
=== 67 ===
= 67 =
<span style="font-family:Comic Sans MS;>67</span>
<span style="font-family:Tahoma;>67</span>
<span style="font-family:Roboto;>67</span>
is a 2025 meme
518otr9p9xdp0wpudt5nxvgei9hp057
All Night Radio
0
175092
740015
2026-05-01T15:13:28Z
Cryptocurrency777
73698
Created page with "{{nihongo|"'''All Night Radio'''"|オールナイトレディオ|Ōru Naito Redio}} is a song recorded by Japanese singer [[Ado (singer)|Ado]], released on October 14, 2023 by [[Virgin Records|Virgin Music]]. The song was written and produced by [[Vocaloid]] producer Mitchie M.<ref name=":1">{{Cite web |trans-title=Ado's New Song "All Night Radio" Theme Song for Live-Streamed Stage Play Drama "Ano Yoru de Aetara" |title=Adoの新曲「オールナイトレディオ」生..."
740015
wikitext
text/x-wiki
{{nihongo|"'''All Night Radio'''"|オールナイトレディオ|Ōru Naito Redio}} is a song recorded by Japanese singer [[Ado (singer)|Ado]], released on October 14, 2023 by [[Virgin Records|Virgin Music]]. The song was written and produced by [[Vocaloid]] producer Mitchie M.<ref name=":1">{{Cite web |trans-title=Ado's New Song "All Night Radio" Theme Song for Live-Streamed Stage Play Drama "Ano Yoru de Aetara" |title=Adoの新曲「オールナイトレディオ」生配信舞台演劇ドラマ「あの夜であえたら」主題歌に |url=https://www.fashion-press.net/news/110057 |date=October 10, 2023 |access-date=March 22, 2024 |website=Fashion Press |language=ja |archive-date=March 22, 2024 |archive-url=https://web.archive.org/web/20240322143701/https://www.fashion-press.net/news/110057 |url-status=live }}</ref>
43i44aahre6nzjdx2yoqflf18x5a9gh
Ado (musician)
0
175093
740016
2026-05-01T15:15:35Z
Cryptocurrency777
73698
Redirected page to [[All Night Radio]]
740016
wikitext
text/x-wiki
#REDIRECT [[All Night Radio]]
olkafuhrhs3u6s24v435opxghcswk02
User:Sam Sailor/test.js/comrade-options.js
2
175094
740019
2026-05-01T15:32:00Z
Sam Sailor
26820
Test
740019
javascript
text/javascript
//<nowiki>
window.ComradeOptions = Object.assign({
// Default Excluded Templates
excludedTemplates: [
"Template:Dmbox",
"Template:Set index article",
"Template:Animal common name",
"Template:Chemistry index",
"Template:Enzyme index",
"Template:Fungus common name",
"Template:Given name",
"Template:Greek mythology index",
"Template:Lake index",
"Template:Locomotive index",
"Template:Media set index",
"Template:Molecular formula index",
"Template:Mountain index",
"Template:Nickname",
"Template:Painting index",
"Template:Plant common name",
"Template:River index",
"Template:Road index",
"Template:Ship index",
"Template:Sport index",
"Template:Storm index",
"Template:Surname"
],
// UI Behaviors and Timers
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
//</nowiki>
06159b470wa47m5we1ycsahktlbt2wx
Wikipedia:Requests/Bot status/Alachuckthebuck
4
175095
740045
2026-05-01T17:18:07Z
Alachuckthebuck
49745
requesting bot acess for chuckbot.
740045
wikitext
text/x-wiki
<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.-->
=== [[User:Chuckbot|Chuckbot]] ===
* {{User3|Alachuckthebuck}}, [[Special:CentralAuth/Alachuckthebuck|global contribs]] 17:17, 1 May 2026 (UTC)
*{{user3|Chuckbot}},[[Special:CentralAuth/Chuckbot|global contribs]]
* '''Motive for request:''' Testing code for chuckbot's 4-award helper module [https://github.com/chuckthebuck/module4awardhelper] before going live.
* '''Requested rights:'''bot
* '''Comments:''' <!-- Comments of other users -->
<!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! -->
[[Category:!Requests]]
[[Category:Really big category]]
__NOINDEX__
f4af0ytjfo1pmz874mqui1x8zbg8whe
740058
740045
2026-05-01T19:09:22Z
Alachuckthebuck
49745
/* Chuckbot */ adding mainpage
740058
wikitext
text/x-wiki
<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.-->
=== [[User:Chuckbot|Chuckbot]] ===
* {{User3|Alachuckthebuck}}, [[Special:CentralAuth/Alachuckthebuck|global contribs]] 17:17, 1 May 2026 (UTC)
*{{user3|Chuckbot}},[[Special:CentralAuth/Chuckbot|global contribs]]
* '''Motive for request:''' Testing code for chuckbot's 4-award helper module [https://github.com/chuckthebuck/module4awardhelper] before going live. my main page is [[c:user:Alachuckthebuck|user:Alachuckthebuck]]
* '''Requested rights:'''bot
* '''Comments:''' <!-- Comments of other users -->
<!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! -->
[[Category:!Requests]]
[[Category:Really big category]]
__NOINDEX__
71oc5xlvmqp7sewd95xggnn9bki30id
Wikipedia:Requests/Permissions/Namoroka
4
175096
740050
2026-05-01T17:47:29Z
Namoroka
19627
Created page with "<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.--> === [[User:{{subst:REVISIONUSER}}|{{subst:REVISIONUSER}}]] === * {{User3|{{subst:REVISIONUSER}}}}, [[Special:CentralAuth/{{subst:REVISIONUSER}}|global contribs]] ~~~~~ * '''Motive for request:''' <!-- Important! Plea..."
740050
wikitext
text/x-wiki
<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.-->
=== [[User:Namoroka|Namoroka]] ===
* {{User3|Namoroka}}, [[Special:CentralAuth/Namoroka|global contribs]] 17:47, 1 May 2026 (UTC)
* '''Motive for request:''' <!-- Important! Please include a good motive for your request here. --> To test changes to gadgets
* '''Requested rights:''' <!-- Please include the rights requested (example: Administrator, Bureaucrat, etc.) --> Interface administrator
* '''Comments:''' <!-- Comments of other users -->Hi, I'm an admin and interface administrator on kowikisource, and I want to test some gadgets before they go live.
<!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! -->
[[Category:!Requests]]
[[Category:Really big category]]
__NOINDEX__
cnuuza95ftnzswfffkm31f41eun2sk2
Halsey
0
175097
740120
2026-05-01T21:43:46Z
Cryptocurrency777
73698
Created page with "[[G-Eazy]] ex"
740120
wikitext
text/x-wiki
[[G-Eazy]] ex
sv7qrmjjsqy8yewt2yjq2ottwdrd6ut
User talk:TheAuroraBorealis
3
175098
740121
2026-05-01T21:48:08Z
TheAuroraBorealis
73590
Bot: Adding Hello world from PyWikiBot! ~~~~
740121
wikitext
text/x-wiki
Hello world from PyWikiBot! [[User:TheAuroraBorealis|TheAuroraBorealis]] ([[User talk:TheAuroraBorealis|talk]]) 21:48, 1 May 2026 (UTC)
kv77p5s7fez22an7m80krpqncfhugte
740122
740121
2026-05-01T21:49:17Z
TheAuroraBorealis
73590
Bot: Adding PyWikiBot has taken over the world! /j
740122
wikitext
text/x-wiki
PyWikiBot has taken over the world! /j
Hello world from PyWikiBot! [[User:TheAuroraBorealis|TheAuroraBorealis]] ([[User talk:TheAuroraBorealis|talk]]) 21:48, 1 May 2026 (UTC)
pdr4r16e2rwi9jv5z1qwoa0nw5wh9pm
User:Namoroka/ohi.css
2
175099
740124
2026-05-02T02:17:56Z
Namoroka
19627
Created page with "/* CSS */ #ohi_wrap { font-size: 13px; font-family: sans-serif; } #keyboardLayout { clear: both; text-align: center; } #keyboardLayoutInfo { font-size: 12px; margin-bottom: 5px; text-align: left; } #keyboardLayoutInfo a { display: none; } #keyboardLayoutTable { display: inline-block; } #keyboardLayoutTable .row { margin: 0; white-space: nowrap; } #ohi_wrap div.e1, #ohi_wrap div.e2, #ohi_wrap div.e3, #ohi_wrap div.h1, #ohi_wrap div.h2, #ohi_wrap div.h3 {..."
740124
css
text/css
/* CSS */
#ohi_wrap {
font-size: 13px;
font-family: sans-serif;
}
#keyboardLayout { clear: both; text-align: center; }
#keyboardLayoutInfo { font-size: 12px; margin-bottom: 5px; text-align: left; }
#keyboardLayoutInfo a { display: none; }
#keyboardLayoutTable { display: inline-block; }
#keyboardLayoutTable .row { margin: 0; white-space: nowrap; }
#ohi_wrap div.e1, #ohi_wrap div.e2, #ohi_wrap div.e3,
#ohi_wrap div.h1, #ohi_wrap div.h2, #ohi_wrap div.h3 {
display: inline-block;
font-size: 12px;
vertical-align: bottom;
height: 38px;
cursor: pointer;
border: 1px solid #c8ccd1;
background-color: #fff;
box-sizing: border-box;
border-radius: 2px;
margin: 1px;
}
#ohi_wrap div.e1 { background-color: #fff; }
#ohi_wrap div.e2 { background-color: #eaecf0; }
#ohi_wrap div.e3 { background-color: #f8f9fa; }
#ohi_wrap div.h1 { background-color: #eaf3ff; } /* 초성 */
#ohi_wrap div.h2 { background-color: #fce7e7; } /* 중성 */
#ohi_wrap div.h3 { background-color: #e3f5e1; } /* 종성 */
#ohi_wrap div.gin-hol { background-color: #f1e1ff; } /* 타자기 장모음 */
#ohi_wrap div.e1:hover, #ohi_wrap div.e2:hover, #ohi_wrap div.e3:hover,
#ohi_wrap div.h1:hover, #ohi_wrap div.h2:hover, #ohi_wrap div.h3:hover {
background-color: #c8ccd1;
}
#ohi_wrap div.pressed { background-color: #fc3; }
/* 자판 내부 텍스트 */
#ohi_wrap div.up, #ohi_wrap div.down { height: 18px; line-height: 18px; }
#ohi_wrap div.up { margin: 0 2px; }
#ohi_wrap div.down { margin: 0 2px; }
#ohi_wrap div.ue, #ohi_wrap div.de { float: left; color: #72777d; font-family: monospace; }
#ohi_wrap div.e2 div.ue { color: #202122; }
#ohi_wrap div.dh, #ohi_wrap div.uh { float: right; color: #202122; font-weight: bold; }
/* 체크박스 옵션 영역 */
#top_options .option, #middle_options .option { display: inline-block; margin-right: 15px; float: none; }
#ohi_wrap input.checkbox { margin-left: 0; margin-right: 3px; vertical-align: middle; }
#ohi_wrap label { font-size: 12px; color: #202122; cursor: pointer; }
kbd.status { font-family: monospace; color: #fff; background-color: #36c; padding: 2px 4px; border-radius: 2px; }
mgunsxaud16ybcgfbt4whhxfgslz8yz
User:Namoroka/additional-layouts.js
2
175100
740125
2026-05-02T02:20:00Z
Namoroka
19627
https://github.com/pat-al/Online-HanGeul-IME/blob/master/additional_layouts.js / GPLv3
740125
javascript
text/javascript
/*
※ additional_layouts.js : 이제는 쓰이지 않거나 개선판이 나왔거나 연구 중인 자판 배열들을 모음
※ keyboard_layouts.js : 널리 쓰이거나 대표성이 있거나 기능·배열에 주목할 면이 있는 자판 배열들을 모음
※ 첫가끝 방식으로 옛한글을 조합하는 자판은 type_name 끝에 '-y'를 붙인다.
※ 신세벌식 자판은 type_name 앞에 'Sin3-'를 붙인다.
※ 갈마들이 방식을 쓰는 공병우식 자판은 type_name 끝에 '_gm'을 붙인다.
※ 모아치기 방식으로 쓰는 세벌식 자판은 type_name 앞에 '3m-'를 붙인다.
※ 타자기 자판은 벌 수 다음에 't-'를 붙인다. ('3t-', 4t-')
※ 모아치기 자판의 보조 배열(sublayout)은 입력에 반영되지 않고 배열표에만 나타남
*/
input_additional_keyboard_layout_info();
input_additional_combination_table_info();
var additional_layouts = [];
additional_layouts[0] = new keyboard_layout_info();
additional_layouts.push({KE: 'Ko', type_name: '2-GJS', full_name: '김준성 풀어쓰기 수동 타자기 (1940년대)', layout: K2_GimJunseong_typewriter_layout, link: 'https://pat.im/1024'});
additional_layouts.push({KE: 'Ko', type_name: '2-HGS_1952', full_name: '한당욱·김철수·신한종 풀어쓰기 전신 타자기 (1952)', layout: K2_HGS_1952_teletypewriter_layout, link: 'https://pat.im/1025'});
additional_layouts.push({KE: 'Ko', type_name: '2-Jang_1953', full_name: '장봉선 풀어쓰기 전신 타자기 (1953) (체신부 통신용)', layout: K2_Jang_1953_teletypewriter_layout, link: 'https://pat.im/1025'});
additional_layouts.push({KE: 'Ko', type_name: '2-Bag_Song_1968', full_name: '박영효·송계범 전신 타자기 자판 설계안 (1968)', layout: K2_Bag_Song_1968_layout, link: 'https://pat.im/1025'});
additional_layouts.push({KE: 'Ko', type_name: '2-Gaon26KM', full_name: '가온한글26KM', layout: K2_Gaon_KSX5002_layout, link: 'http://cafe.daum.net/kbd-p/8OTK/10'});
additional_layouts.push({KE: 'Ko', type_name: '4t-1969', full_name: '1969 네벌식 타자기 표준 (과학기술처)', layout: K4_1969_Typewriter_layout, hangeul_combination_table: K4_1969_Typewriter_combination_table, link: 'https://pat.im/965'});
additional_layouts.push({KE: 'Ko', type_name: '3t-Oesol', full_name: '외솔 타자기 (1981, 최동식·김광성)', layout: K3_Oesol_Typewriter_layout, hangeul_combination_table: K3_Oesol_Typewriter_combination_table, link: 'https://pat.im/1026'});
additional_layouts.push({KE: 'Ko', type_name: '4t-1985', full_name: '1985 두벌식 배열 호환형 타자기 (과학기술처)', layout: K4_1985_Typewriter_layout, link: 'https://pat.im/1026'});
additional_layouts.push({KE: 'Ko', type_name: '3t-Gong_Munjang', full_name: '공병우 문장용 타자기', layout: K3_Gong_Munjang_layout, link: 'https://pat.im/960#5'});
additional_layouts.push({KE: 'Ko', type_name: '3-87', full_name: '3-87', layout: K3_87_layout, extended_sign_layout: K3_87_extended_layout, link: 'https://pat.im/1145'});
additional_layouts.push({KE: 'Ko', type_name: '3-89', full_name: '3-89', layout: K3_89_layout, link: ''});
additional_layouts.push({KE: 'Ko', type_name: '3-891', full_name: '3-891', layout: K3_891_layout, extended_sign_layout: K3_891_extended_layout, link: 'https://pat.im/1188'});
additional_layouts.push({KE: 'Ko', type_name: '3-95', full_name: '3-95 자판안 (김창용)', layout: K3_95_proposal_layout, extended_sign_layout: K3_95_extended_layout, link: 'https://bbs.pat.im/viewtopic.php?f=15&t=931'});
additional_layouts.push({KE: 'Ko', type_name: '3-sun1990', full_name: '안종혁 순아래 1990 (한 손 자판, no-shift) (3-90 응용)', layout: K3_sun1990_layout, link: ''});
additional_layouts.push({KE: 'Ko', type_name: '3-Gaon38A', full_name: '김국 38A (가온세벌38A)', layout: K3_Gaon38A_layout, hangeul_combination_table: Gaon38A_combination_table, link: 'http://cafe.daum.net/kbd-p/8k2B/1'});
additional_layouts.push({KE: 'Ko', type_name: '3-2011', full_name: '3-2011 (문장 입력용)', layout: K3_2011_layout, extended_sign_layout: K3_2011_extended_sign_layout, old_hangeul_layout_type_name: '3-2011-y', link: 'https://pat.im/855'});
additional_layouts.push({KE: 'Ko', type_name: '3-2011-y', full_name: '3-2011 옛한글', layout: K3_2011_layout, extended_sign_layout: K3_2011y_extended_sign_layout, extended_hangeul_layout: K3_2012y_extended_hangeul_layout, link: 'https://pat.im/908'});
additional_layouts.push({KE: 'Ko', type_name: '3-2012', full_name: '3-2012', layout: K3_2012_layout, extended_sign_layout: K3_2012_extended_sign_layout, old_hangeul_layout_type_name: '3-2012-y', link: 'https://pat.im/938'});
additional_layouts.push({KE: 'Ko', type_name: '3-2012-y', full_name: '3-2012 옛한글', layout: K3_2012_layout, extended_sign_layout: K3_2012y_extended_sign_layout, extended_hangeul_layout: K3_2012y_extended_hangeul_layout, link: 'https://pat.im/938#4-2'});
additional_layouts.push({KE: 'Ko', type_name: '314', full_name: '한글문화원 314 자판안', layout: K3_14_proposal_layout, link: 'http://cafe.daum.net/3bulsik/JMKX/4'});
additional_layouts.push({KE: 'Ko', type_name: '314_gm', full_name: '한글문화원 314 자판안 (+ 갈마들이)', layout: K3_14_proposal_layout, link: 'http://cafe.daum.net/3bulsik/JMKX/4'});
additional_layouts.push({KE: 'Ko', type_name: '3-2014', full_name: '3-2014', layout: K3_2014_layout, extended_sign_layout: K3_2012y_extended_sign_layout, hangeul_combination_table: K3_Galmadeuli_Gong3_combination_table, old_hangeul_layout_type_name: '3-2014-y', link: 'https://pat.im/1088'});
additional_layouts.push({KE: 'Ko', type_name: '3-2014-y', full_name: '3-2014 옛한글', layout: K3_2014_layout, extended_sign_layout: K3_2012y_extended_sign_layout, extended_hangeul_layout: K3_2012y_extended_hangeul_layout, link: 'https://pat.im/1090'});
additional_layouts.push({KE: 'Ko', type_name: '3-2015', full_name: '3-2015', layout: K3_2015_layout, hangeul_combination_table: K3_2015_combination_table, hangeul_convenience_combination_table: K3_2015_additional_combination_table, old_hangeul_layout_type_name: '3-2015-y', link: 'http://cafe.daum.net/3bulsik/JMKX/34'});
additional_layouts.push({KE: 'Ko', type_name: '3-2015-y', full_name: '3-2015 옛한글', layout: K3_2015y_layout, hangeul_combination_table: K3_2015y_combination_table, link: 'http://cafe.daum.net/3bulsik/JMKX/36'});
additional_layouts.push({KE: 'Ko', type_name: '3-2015M', full_name: '3-2015M', layout: K3_2015M_layout, hangeul_combination_table: K3_2015M_combination_table, hangeul_combination_table: K3_Galmadeuli_Gong3_combination_table, link: 'http://cafe.daum.net/3bulsik/JMKX/46'});
additional_layouts.push({KE: 'Ko', type_name: '3-2015P', full_name: '3-2015P', layout: K3_2015P_layout, extended_sign_layout: K3_2012y_extended_sign_layout, hangeul_combination_table: K3_Galmadeuli_Gong3_combination_table, old_hangeul_layout_type_name: '3-2015P-y', link: 'https://pat.im/1090'});
additional_layouts.push({KE: 'Ko', type_name: '3-2015P-y', full_name: '3-2015P 옛한글', layout: K3_2015P_layout, extended_sign_layout: K3_2012y_extended_sign_layout, extended_hangeul_layout: K3_2012y_extended_hangeul_layout, link: 'https://pat.im/1090'});
additional_layouts.push({KE: 'Ko', type_name: '3-P3', full_name: '3-P3', layout: K3_P3_layout, extended_sign_layout: K3_P3_extended_sign_layout, hangeul_combination_table: K3_Galmadeuli_Gong3_combination_table, link: 'https://pat.im/1128'});
additional_layouts.push({KE: 'Ko', type_name: '3-P2', full_name: '3-P2', layout: K3_P2_layout, extended_sign_layout: K3_2012y_extended_sign_layout, hangeul_combination_table: K3_Galmadeuli_Gong3_combination_table, link: 'https://pat.im/1128'});
additional_layouts.push({KE: 'Ko', type_name: '3-D1', full_name: '3-D1', layout: K3_D1_layout, capslock_layout: K3_D1_capslock_layout, old_hangeul_layout_type_name: '3-D1-y', hangeul_combination_table: K3_D1_combination_table, hangeul_convenience_combination_table: K3_D1_additional_combination_table, link: 'https://cafe.daum.net/3bulsik/JMKX/180', link: 'https://ds1tpt.tistory.com/10'});
additional_layouts.push({KE: 'Ko', type_name: '3-D1-y', full_name: '3-D1 옛한글', layout: K3_D1_y_layout, hangeul_combination_table: K3_D1_y_combination_table, link: 'https://cafe.daum.net/3bulsik/JMKX/180', link: 'https://ds1tpt.tistory.com/10'});
additional_layouts.push({KE: 'Ko', type_name: '3-D2', full_name: '3-D2', layout: K3_D2_layout, capslock_layout: K3_D2_capslock_layout, old_hangeul_layout_type_name: '3-D2-y', hangeul_combination_table: K3_Galmadeuli_Gong3_combination_table, link: 'https://cafe.daum.net/3bulsik/JMKX/201', link: 'https://ds1tpt.tistory.com/16'});
additional_layouts.push({KE: 'Ko', type_name: '3-D2-y', full_name: '3-D2 옛한글', layout: K3_D2_y_layout, capslock_layout: K3_D2_capslock_layout, link: 'https://cafe.daum.net/3bulsik/JMKX/201', link: 'https://ds1tpt.tistory.com/16'});
additional_layouts.push({KE: 'Ko', type_name: '3-18Na', full_name: '3-18Na', layout: K3_18Na_layout, hangeul_combination_table: K3_18Na_combination_table, link: 'https://kldp.org/node/160815'});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-1995', full_name: '신세벌식 1995 (신광조 원안)', layout: K3_Sin3_1995_layout, link:'https://pat.im/1104'});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-BGN', full_name: '박경남 신세벌식', layout: K3_Sin3_BGN_layout});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-2003', full_name: '박경남 수정 신세벌식 (2003)', layout: K3_Sin3_2003_layout});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-2012', full_name: '신세벌식 2012', layout: K3_Sin3_2012_layout, link: 'https://pat.im/978'});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-2015', full_name: '신세벌식 2015', layout: K3_Sin3_2015_layout, hangeul_combination_table: K3_Sin3_2015_combination_table, link: 'http://sebeol.org/gnuboard/bbs/board.php?bo_table=lab&wr_id=28'});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-M', full_name: '신세벌식 M', layout: K3_Sin3_M_layout, link: 'http://cafe.daum.net/3bulsik/JMKX/77'});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-P', full_name: '신세벌식 P', layout: K3_Sin3_P_layout, extended_hangeul_layout: K3_Sin3_P_y_layout, extended_sign_layout: K3_Sin3_extended_sign_layout, /*extended_hangeul_combination_table: K3_Sin3_P_extended_combination_table,*/ old_hangeul_layout_type_name: 'Sin3-P-y', link: 'https://pat.im/1110'});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-P-y', full_name: '신세벌식 P 옛한글 조합', layout: K3_Sin3_P_y_layout, capslock_layout: K3_Sin3_P_y_capslock_layout, extended_sign_layout: K3_Sin3_extended_sign_layout, capslock_extended_sign_layout: null, hangeul_combination_table: K3_Sin3_P2_yeshangeul_combination_table, link: 'https://pat.im/1136#2-4'});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-Gongdong', full_name: '신세벌식 공동개발안 (연구)', layout: K3_Sin3_Gongdong_layout, ieochigi_hangeul_abbreviation_table: K3_Sin3_Gongdong_abbreviation_table, hangeul_convenience_combination_table: K3_Sin3_Gongdong_additional_combination_table, link: 'http://cafe.daum.net/3bulsikmini0A0/JYgd/31'});
additional_layouts.push({KE: 'Ko', type_name: 'Sin3-Cham', full_name: '참신세벌식 v18', layout: K3_Sin3_Cham_layout, hangeul_combination_table: K3_Sin3_Cham_combination_table, hangeul_convenience_combination_table: K3_Sin3_Cham_additional_combination_table, link: 'https://doc9107.tistory.com/67'});
additional_layouts.push({KE: 'Ko', type_name: 'LGG3-OH-R', full_name: '이건구 한 손 세벌식 오른손', layout: K3_LGG_OH_r, capslock_layout: K3_LGG_OH_r_capslock_layout, hangeul_combination_table: K3_LGG_OH_combination_table, link: 'https://ds1tpt.tistory.com/32'});
additional_layouts.push({KE: 'Ko', type_name: 'LGG3-OH-L', full_name: '이건구 한 손 세벌식 왼손', layout: K3_LGG_OH_l, capslock_layout: K3_LGG_OH_l_capslock_layout, hangeul_combination_table: K3_LGG_OH_combination_table, link: 'https://ds1tpt.tistory.com/32'});
additional_layouts.push({KE: 'Ko', type_name: 'LGG3-OH-OHK', full_name: '이건구 한 손 세벌식 한손글쇠판용', layout: K3_LGG_OH_ohk, capslock_layout: K3_LGG_OH_ohk_capslock_layout, hangeul_combination_table: K3_LGG_OH_combination_table, link: 'https://ds1tpt.tistory.com/32'});
additional_layouts.push({KE: 'Ko', type_name: '3m-Anmatae', full_name: '안마태 소리 글판', layout: K3_Anmatae_layout, moachigi_hangeul_combination_table: K3_Anmatae_combination_table, link: ''});
additional_layouts.push({KE: 'Ko', type_name: '3m-Semoe2014', full_name: '세모이 2014 (옛 배열)', layout: K3_Semoe_2014_layout, sublayout: K3_Semoe_2014_sublayout, moachigi_hangeul_combination_table: K3_Semoe_2014_combination_table});
additional_layouts.push({KE: 'Ko', type_name: '3m-Semoe2015', full_name: '세모이 2015 (옛 배열)', layout: K3_Semoe_2015_layout, sublayout: K3_Semoe_2015_sublayout, moachigi_hangeul_combination_table: K3_Semoe_2015_combination_table});
additional_layouts.push({KE: 'Ko', type_name: '3m-Semoe2016', full_name: '세모이 2016 (옛 배열)', layout: K3_Semoe_2016_layout, sublayout: K3_Semoe_2016_sublayout, moachigi_hangeul_combination_table: K3_Semoe_2016_combination_table, extended_sign_layout: K3_Semoe_extended_sign_layout});
additional_layouts.push({KE: 'Ko', type_name: '3m-Semoe2017', full_name: '세모이 2017 (옛 배열)', layout: K3_Semoe_2017_layout, sublayout: K3_Semoe_2017_sublayout, extended_sign_layout: K3_Semoe_extended_sign_layout, moachigi_hangeul_combination_table: K3_Semoe_2017_combination_table, moachigi_multikey_abbreviation_table: K3_Semoe_2017_moachigi_multikey_abbreviation_table, link: 'http://ssg.wo.tc/220526834927'});
additional_layouts.push({KE: 'Ko', type_name: '3m-test', full_name: '모아치기 시험', layout: K3_Semoe_2017_layout, sublayout: K3_Semoe_2017_sublayout, extended_sign_layout: K3_Semoe_extended_sign_layout, moachigi_hangeul_combination_table: K3_Semoe_2017_combination_table, moachigi_multikey_abbreviation_table: K3_test_multikey_abbreviation_table, link: ''});
function input_additional_keyboard_layout_info() {
var i,j;
// 김준성 두벌식 타자기 (1940년대 미군정 때)
K2_GimJunseong_typewriter_layout = [
0x0000, /* 0x21 exclam: exclamation mark */
0x003A, /* 0x22 quotedbl: colon */
0x0033, /* 0x23 numbersign: 3 */
0x0034, /* 0x24 dollar: 4 */
0x0035, /* 0x25 percent: 5 */
0x0037, /* 0x26 ampersand: 7 */
0x116C, /* 0x27 apostrophe: jungseong oe */
0x0039, /* 0x28 parenleft: 9 */
0x0028, /* 0x29 parenright: left parenthesis */
0x0038, /* 0x2A asterisk: 8 */
0x002B, /* 0x2B plus: plus sign */
0x116E, /* 0x2C comma: jungseong u */
0x1170, /* 0x2D minus: jungseong we */
0x1172, /* 0x2E period: jungseong yu */
0x1171, /* 0x2F slash: jungseong wi */
0x116F, /* 0x30 0: jungseong weo */
0x0000, /* 0x31 1: */
0x0022, /* 0x32 2: quotation mark */
0x0040, /* 0x33 3: commertial at */
0x0027, /* 0x34 4: apostrophe */
0x0025, /* 0x35 5: percent */
0x002E, /* 0x36 6: period */
0x002C, /* 0x37 7: comma */
0x116A, /* 0x38 8: jungseong wa */
0x116B, /* 0x39 9: jungseong wae */
0x003F, /* 0x3A colon: question mark */
0x1168, /* 0x3B semicolon: jungseong ye */
0x007E, /* 0x3C less: tilde */
0x003D, /* 0x3D equal: equals sign */
0x002F, /* 0x3E greater: slash */
0x005F, /* 0x3F question: underscore */
0x0032, /* 0x40 at: 2 */
0x0041, /* 0x41 A */
0x0042, /* 0x42 B */
0x0043, /* 0x43 C */
0x0044, /* 0x44 D */
0x0045, /* 0x45 E */
0x0046, /* 0x46 F */
0x0047, /* 0x47 G */
0x0048, /* 0x48 H */
0x0049, /* 0x49 I */
0x004A, /* 0x4A J */
0x004B, /* 0x4B K */
0x004C, /* 0x4C L */
0x004D, /* 0x4D M */
0x004E, /* 0x4E N */
0x004F, /* 0x4F O */
0x0050, /* 0x50 P */
0x0051, /* 0x51 Q */
0x0052, /* 0x52 R */
0x0053, /* 0x53 S */
0x0054, /* 0x54 T */
0x0055, /* 0x55 U */
0x0056, /* 0x56 V */
0x0057, /* 0x57 W */
0x0058, /* 0x58 X */
0x0059, /* 0x59 Y */
0x005A, /* 0x5A Z */
0x1164, /* 0x5B bracketleft: jungseong yae */
0x005C, /* 0x5C backslash: backslash */
0x0000, /* 0x5D bracketright: */
0x0036, /* 0x5E asciicircum: 6 */
0x0029, /* 0x5F underscore: right parenthesis */
0x0060, /* 0x60 quoteleft: grave accent */
0x1105, /* 0x61 a: choseong lieul */
0x110C, /* 0x62 b: choseong jieuj */
0x110F, /* 0x63 c: choseong kieuk */
0x1102, /* 0x64 d: choseong nieun */
0x1107, /* 0x65 e: choseong bieub */
0x1100, /* 0x66 f: choseong gieug */
0x1112, /* 0x67 g: choseong hieuh */
0x1163, /* 0x68 h: jungseong ya */
0x1175, /* 0x69 i: jungseong i */
0x1161, /* 0x6A j: jungseong a */
0x1165, /* 0x6B k: jungseong eo */
0x1167, /* 0x6C l: jungseong yeo */
0x1169, /* 0x6D m: jungseong o */
0x116D, /* 0x6E n: jungseong yo */
0x1166, /* 0x6F o: jungseong e */
0x1162, /* 0x70 p: jungseong ae */
0x00B0, /* 0x71 q: degree sign */
0x1109, /* 0x72 r: choseong sieus */
0x1103, /* 0x73 s: choseong dieud */
0x110B, /* 0x74 t: choseong ieung */
0x1173, /* 0x75 u: jungseong eu */
0x110E, /* 0x76 v: choseong chieuch */
0x1106, /* 0x77 w: choseong mieum */
0x1110, /* 0x78 x: choseong tieut */
0x1174, /* 0x79 y: jungseong eui */
0x1111, /* 0x7A z: choseong pieup */
0x002D, /* 0x7B braceleft: minus sign */
0x007C, /* 0x7C bar: vertical line(bar) */
0x0000, /* 0x7D braceright: */
0x0000 /* 0x7E asciitilde: */
];
// 한당욱·김철수·신한종 전신 타자기 (1952년)
K2_HGS_1952_teletypewriter_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x0027, /* 0x27 apostrophe: apostrophe */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x002F, /* 0x2F slash: slash */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x003A, /* 0x3A colon: colon */
0x003B, /* 0x3B semicolon: semicolon */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: equals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x002D, /* 0x41 A: minus sign */
0x003F, /* 0x42 B: question mark */
0x003A, /* 0x43 C: colon */
0x0024, /* 0x44 D: dollar sign */
0x0033, /* 0x45 E: 3 */
0x0021, /* 0x46 F: exclamation mark */
0x0026, /* 0x47 G: ampersand */
0x23F9, /* 0x48 H: stop */
0x0038, /* 0x49 I: 8 */
0x0022, /* 0x4A J: quotatioin mark */
0x0028, /* 0x4B K: left parenthesis */
0x0029, /* 0x4C L: right parenthesis */
0x002E, /* 0x4D M: period */
0x002C, /* 0x4E N: comma */
0x0039, /* 0x4F O: 9 */
0x0030, /* 0x50 P: 0 */
0x0031, /* 0x51 Q: 1 */
0x0034, /* 0x52 R: 4 */
0x237E, /* 0x53 S: bell */
0x0035, /* 0x54 T: 5 */
0x0037, /* 0x55 U: 7 */
0x003B, /* 0x56 V: semicolon */
0x0032, /* 0x57 W: 2 */
0x002F, /* 0x58 X: slash */
0x0036, /* 0x59 Y: 6 */
0x3003, /* 0x5A Z: ditto mark */
0x005B, /* 0x5B bracketleft: left bracket */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x1100, /* 0x61 a: choseong gieug */
0x116D, /* 0x62 b: jungseong yo */
0x1111, /* 0x63 c: choseong pieup */
0x1109, /* 0x64 d: choseong sieus */
0x1106, /* 0x65 e: choseong mieum */
0x1102, /* 0x66 f: choseong nieun */
0x110B, /* 0x67 g: choseong ieung */
0x1169, /* 0x68 h: jungseong o */
0x116E, /* 0x69 i: jungseong u */
0x1161, /* 0x6A j: jungseong a */
0x1165, /* 0x6B k: jungseong eo */
0x1175, /* 0x6C l: jungseong i */
0x1173, /* 0x6D m: jungseong yu */
0x1172, /* 0x6E n: jungseong eu */
0x1162, /* 0x6F o: jungseong ae */
0x1166, /* 0x70 p: jungseong e */
0x1107, /* 0x71 q: choseong bieub */
0x110C, /* 0x72 r: choseong jieuj */
0x1105, /* 0x73 s: choseong lieul */
0x1112, /* 0x74 t: choseong hieuh */
0x1167, /* 0x75 u: jungseong yeo */
0x1110, /* 0x76 v: choseong tieut */
0x1103, /* 0x77 w: choseong dieud */
0x110E, /* 0x78 x: choseong chieuch */
0x1163, /* 0x79 y: jungseong ya */
0x110F, /* 0x7A z: choseong kieuk */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
// 장봉선 두벌식 (1953년 체신부 전신용)
K2_Jang_1953_teletypewriter_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x0027, /* 0x27 apostrophe: apostrophe */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x002F, /* 0x2F slash: slash */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x003A, /* 0x3A colon: colon */
0x003B, /* 0x3B semicolon: semicolon */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: equals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x002D, /* 0x41 A: minus sign */
0x003F, /* 0x42 B: question mark */
0x003A, /* 0x43 C: colon */
0x271C, /* 0x44 D: Who are you? (ENQ, WRU) */
0x0033, /* 0x45 E: 3 */
0x0025, /* 0x46 F: percent sign */
0x0022, /* 0x47 G: quotatioin mark */
0x0021, /* 0x48 H: exclamation mark */
0x0038, /* 0x49 I: 8 */
0x237E, /* 0x4A J: bell */
0x0028, /* 0x4B K: left parenthesis */
0x0029, /* 0x4C L: right parenthesis */
0x002E, /* 0x4D M: period */
0x002C, /* 0x4E N: comma */
0x0039, /* 0x4F O: 9 */
0x0030, /* 0x50 P: 0 */
0x0031, /* 0x51 Q: 1 */
0x0034, /* 0x52 R: 4 */
0x0027, /* 0x53 S: apostrophe */
0x0035, /* 0x54 T: 5 */
0x0037, /* 0x55 U: 7 */
0x003D, /* 0x56 V: equals sign */
0x0032, /* 0x57 W: 2 */
0x002F, /* 0x58 X: slash */
0x0036, /* 0x59 Y: 6 */
0x002B, /* 0x5A Z: plus sign */
0x005B, /* 0x5B bracketleft: left bracket */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x1112, /* 0x61 a: choseong hieuh */
0x1111, /* 0x62 b: choseong pieup */
0x110E, /* 0x63 c: choseong chieuch */
0x1100, /* 0x64 d: choseong gieug */
0x1109, /* 0x65 e: choseong sieus */
0x1102, /* 0x66 f: choseong nieun */
0x110B, /* 0x67 g: choseong ieung */
0x1173, /* 0x68 h: jungseong eu */
0x1169, /* 0x69 i: jungseong o */
0x1161, /* 0x6A j: jungseong a */
0x1175, /* 0x6B k: jungseong i */
0x1165, /* 0x6C l: jungseong eo */
0x116D, /* 0x6D m: jungseong yo */
0x1172, /* 0x6E n: jungseong yu */
0x1163, /* 0x6F o: jungseong ya */
0x1166, /* 0x70 p: jungseong e */
0x1110, /* 0x71 q: choseong tieut */
0x1103, /* 0x72 r: choseong dieud */
0x1105, /* 0x73 s: choseong lieul */
0x1106, /* 0x74 t: choseong mieum */
0x116E, /* 0x75 u: jungseong u */
0x1107, /* 0x76 v: choseong bieub */
0x110C, /* 0x77 w: choseong jieuj */
0x110F, /* 0x78 x: choseong kieuk */
0x1167, /* 0x79 y: jungseong yeo */
0x1162, /* 0x7A z: jungseong ae */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
// 박영효·송계범 두벌식 (1968년 전신 타자기용)
K2_Bag_Song_1968_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x0027, /* 0x27 apostrophe: apostrophe */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x002F, /* 0x2F slash: slash */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x003A, /* 0x3A colon: colon */
0x003B, /* 0x3B semicolon: semicolon */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: equals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x1104, /* 0x41 A: choseong ssang_dieud */
0x1163, /* 0x42 B: jungseong ya*/
0x1112, /* 0x43 C: choseong hieuh*/
0x110B, /* 0x44 D: choseong ieung*/
0x1108, /* 0x45 E: choseong ssang_bieub */
0x1102, /* 0x46 F: choseong nieun */
0x1105, /* 0x47 G: choseong lieul */
0x1169, /* 0x48 H: jungseong o */
0x1168, /* 0x49 I: jungseong ye */
0x1173, /* 0x4A J: jungseong eu */
0x1161, /* 0x4B K: jungseong a */
0x1175, /* 0x4C L: jungseong i */
0x1172, /* 0x4D M: jungseong yu */
0x116D, /* 0x4E N: jungseong yo */
0x1165, /* 0x4F O: jungseong eo */
0x1164, /* 0x50 P: jungseong yae */
0x110E, /* 0x51 Q: choseong chieuch */
0x110A, /* 0x52 R: choseong ssang_sieus */
0x1101, /* 0x53 S: choseong ssang_gieug */
0x1106, /* 0x54 T: choseong mieum*/
0x116E, /* 0x55 U: jungseong u */
0x1110, /* 0x56 V: choseong tieut */
0x110D, /* 0x57 W: choseong ssang_jieuj */
0x1111, /* 0x58 X: choseong pieup */
0x1167, /* 0x59 Y: jungseong yeo */
0x110F, /* 0x5A Z: choseong kieuk */
0x005B, /* 0x5B bracketleft: left bracket */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x1103, /* 0x61 a: choseong dieud */
0x1163, /* 0x62 b: jungseong ya */
0x1112, /* 0x63 c: choseong hieuh */
0x110B, /* 0x64 d: choseong ieung */
0x1107, /* 0x65 e: choseong bieub */
0x1102, /* 0x66 f: choseong nieun */
0x1105, /* 0x67 g: choseong lieul */
0x1169, /* 0x68 h: jungseong o */
0x1166, /* 0x69 i: jungseong e */
0x1173, /* 0x6A j: jungseong eu */
0x1161, /* 0x6B k: jungseong a */
0x1175, /* 0x6C l: jungseong i */
0x1172, /* 0x6D m: jungseong yu */
0x116D, /* 0x6E n: jungseong yo */
0x1165, /* 0x6F o: jungseong eo */
0x1162, /* 0x70 p: jungseong ae */
0x110E, /* 0x71 q: choseong chieuch */
0x1109, /* 0x72 r: choseong sieus */
0x1100, /* 0x73 s: choseong gieug */
0x1106, /* 0x74 t: choseong mieum */
0x116E, /* 0x75 u: jungseong u */
0x1110, /* 0x76 v: choseong tieut */
0x110C, /* 0x77 w: choseong jieuj */
0x1111, /* 0x78 x: choseong pieup */
0x1167, /* 0x79 y: jungseong yeo */
0x110F, /* 0x7A z: choseong kieuk */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
// 가온한글 두벌식
K2_Gaon_KSX5002_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x0027, /* 0x27 apostrophe: apostrophe */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x002F, /* 0x2F slash: slash */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x003A, /* 0x3A colon: colon */
0x003B, /* 0x3B semicolon: semicolon */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: equals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x0041, /* 0x41 A */
0x0042, /* 0x42 B */
0x0043, /* 0x43 C */
0x0044, /* 0x44 D */
0x0045, /* 0x45 E */
0x0046, /* 0x46 F */
0x0047, /* 0x47 G */
0x0048, /* 0x48 H */
0x0049, /* 0x49 I */
0x004A, /* 0x4A J */
0x004B, /* 0x4B K */
0x004C, /* 0x4C L */
0x004D, /* 0x4D M */
0x004E, /* 0x4E N */
0x004F, /* 0x4F O */
0x0050, /* 0x50 P */
0x0051, /* 0x51 Q */
0x0052, /* 0x52 R */
0x0053, /* 0x53 S */
0x0054, /* 0x54 T */
0x0055, /* 0x55 U */
0x0056, /* 0x56 V */
0x0057, /* 0x57 W */
0x0058, /* 0x58 X */
0x0059, /* 0x59 Y */
0x005A, /* 0x5A Z */
0x005B, /* 0x5B bracketleft: left bracket */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x1106, /* 0x61 a: choseong mieum */
0x1172, /* 0x62 b: jungseong yu */
0x110E, /* 0x63 c: choseong cieuc */
0x110B, /* 0x64 d: choseong ieung */
0x1103, /* 0x65 e: choseong dieud */
0x1105, /* 0x66 f: choseong lieul */
0x1112, /* 0x67 g: choseong hieuh */
0x1169, /* 0x68 h: jungseong o */
0x1163, /* 0x69 i: jungseong ya */
0x1165, /* 0x6A j: jungseong eo */
0x1161, /* 0x6B k: jungseong a */
0x1175, /* 0x6C l: jungseong i */
0x1173, /* 0x6D m: jungseong eu */
0x116E, /* 0x6E n: jungseong u */
0x1162, /* 0x6F o: jungseong ae */
0x1166, /* 0x70 p: jungseong e */
0x1107, /* 0x71 q: choseong bieub */
0x1100, /* 0x72 r: choseong gieug */
0x1102, /* 0x73 s: choseong nieun */
0x1109, /* 0x74 t: choseong sieus */
0x1167, /* 0x75 u: jungseong yeo */
0x1111, /* 0x76 v: choseong pieup */
0x110C, /* 0x77 w: choseong jieuj */
0x1110, /* 0x78 x: choseong tieut */
0x116D, /* 0x79 y: jungseong yo */
0x110F, /* 0x7A z: choseong kieuk */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
// 1969 옛 표준 네벌식 타자기
K4_1969_Typewriter_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x11AE, /* 0x22 quotedbl: jongseong dieud */
0xFFE6, /* 0x23 numbersign: won sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x003A, /* 0x26 ampersand: colon */
0x11B8, /* 0x27 apostrophe: jongseong bieub */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x0027, /* 0x2A asterisk: apostrophe */
0x002B, /* 0x2B plus: plus sign */
0x3161, /* 0x2C comma: hangeul letter eu */
0x002D, /* 0x2D minus: minus sign */
0x11BA, /* 0x2E period: jongseong sieus */
0x11BB, /* 0x2F slash: jongseong ssang_sieus */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x3156, /* 0x3A colon: hangeul letter ye */
0x3154, /* 0x3B semicolon: hangeul letter e */
0x002C, /* 0x3C less: comma */
0x003D, /* 0x3D equal: equals sign */
0x11B9, /* 0x3E greater: jongseong bieubsieus */
0x11AD, /* 0x3F question: jongseong nieunhieuh */
0x0022, /* 0x40 at: quotation mark */
0x110F, /* 0x41 A: choseong kieuk */
0x3160, /* 0x42 B: hangeul letter yu */
0x11BE, /* 0x43 C: jongseong chieuch */
0x110E, /* 0x44 D: choseong chieuch */
0x1104, /* 0x45 E: choseong ssang_dieud */
0x1111, /* 0x46 F: choseong pieup */
0x002F, /* 0x47 G: slash */
0x315B, /* 0x48 H: hangeul letter yo */
0x1163, /* 0x49 I: jungseong ya */
0x3155, /* 0x4A J: hangeul letter yeo */
0x3151, /* 0x4B K: hangeul letter ya */
0x3150, /* 0x4C L: hangeul lteer ae */
0x002E, /* 0x4D M: period */
0x1172, /* 0x4E N: jungseong yu */
0x1162, /* 0x4F O: jungseong ae */
0x003F, /* 0x50 P: question mark */
0x1108, /* 0x51 Q: choseong ssang_bieub */
0x1101, /* 0x52 R: choseong ssang_gieug */
0x1110, /* 0x53 S: choseong tieut */
0x110A, /* 0x54 T: choseong ssang_sieus*/
0x1167, /* 0x55 U: jungseong yeo */
0x11C1, /* 0x56 V: jongseong pieup */
0x110D, /* 0x57 W: choseong ssang_jieuj */
0x11C0, /* 0x58 X: jongseong tieut */
0x116D, /* 0x59 Y: jungseong yo */
0x11C2, /* 0x5A Z: jongseong hieuh */
0x11A8, /* 0x5B bracketleft: jongseong gieug */
0x0000, /* 0x5C backslash: */
0x0000, /* 0x5D bracketright: */
0x005F, /* 0x5E asciicircum: underscore */
0x00D7, /* 0x5F underscore: multiplication sign */
0x0000, /* 0x60 quoteleft: */
0x1106, /* 0x61 a: choseong mieum */
0x315C, /* 0x62 b: hangeul letter u */
0x11BC, /* 0x63 c: jongseong ieung */
0x110B, /* 0x64 d: choseong ieung */
0x1103, /* 0x65 e: choseong dieud */
0x1105, /* 0x66 f: choseong lieul*/
0x1112, /* 0x67 g: choseong hieuh */
0x3157, /* 0x68 h: hangeul letter o */
0x1161, /* 0x69 i: jungseong a */
0x3153, /* 0x6A j: hangeul letter eo */
0x314F, /* 0x6B k: hangeul letter a */
0x3163, /* 0x6C l: hangeul letter i */
0x1173, /* 0x6D m: jungseong eu */
0x116E, /* 0x6E n: jungseong u */
0x1175, /* 0x6F o: jungseong i */
0x1166, /* 0x70 p: jungseong e */
0x1107, /* 0x71 q: choseong bieub */
0x1100, /* 0x72 r: choseong gieug */
0x1102, /* 0x73 s: choseong nieun */
0x1109, /* 0x74 t: choseong sieus */
0x1165, /* 0x75 u: jungseong u */
0x11AF, /* 0x76 v: jongseong lieul */
0x110C, /* 0x77 w: choseong jieuj */
0x11AB, /* 0x78 x: jongseong nieun */
0x1169, /* 0x79 y: jungseong o */
0x11B7, /* 0x7A z: jongseong mieum */
0x11BD, /* 0x7B braceleft: jongseong jieuj */
0x0000, /* 0x7C bar: */
0x0000, /* 0x7D braceright: */
0x0000, /* 0x7E asciitilde: */
];
// 외솔 타자기 (두벌식 호환형 세벌식)
K3_Oesol_Typewriter_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x00B0, /* 0x22 quotedbl: degree sign */
0xFFE6, /* 0x23 numbersign: won sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x003A, /* 0x26 ampersand: colon */
0x116E, /* 0x27 apostrophe: jungseong u */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x0027, /* 0x2A asterisk: apostrophe */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x1169, /* 0x2F slash: jungseong o */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x11AC, /* 0x3A colon: jongseong nieun-jieuj */
0x3162, /* 0x3B semicolon: hangeul letter eui */
0x002F, /* 0x3C less: slash */
0x003D, /* 0x3D equal: equals sign */
0x003F, /* 0x3E greater: question mark */
0x0040, /* 0x3F question: commertial at */
0x0022, /* 0x40 at: quotatioin mark */
0x11B7, /* 0x41 A: jongseong mieum */
0x11AA, /* 0x42 B: jongseong gieug-sieus */
0x11BE, /* 0x43 C: jongseong chieuch */
0x11BC, /* 0x44 D: jongseong ieung */
0x11AE, /* 0x45 E: jongseong dieud */
0x11AF, /* 0x46 F: jongseong lieul */
0x11C2, /* 0x47 G: jongseong hieuh */
0x11AD, /* 0x48 H: jongseong nieun-hieuh */
0x11B0, /* 0x49 I: jongseong lieul-gieug */
0x11B9, /* 0x4A J: jongseong bieub-sieus */
0x11BB, /* 0x4B K: jongseong ssang_sieus */
0x11A9, /* 0x4C L: jongseong ssang_gieug */
0x11B2, /* 0x4D M: jongseong lieul-bieub */
0x11B3, /* 0x4E N: jongseong lieul-sieus */
0x11B4, /* 0x4F O: jongseong lieul-tieut */
0x11B5, /* 0x50 P: jongseong lieul-pieup */
0x11B8, /* 0x51 Q: jongseong bieub */
0x11A8, /* 0x52 R: jongseong gieug */
0x11AB, /* 0x53 S: jongseong nieun */
0x11BA, /* 0x54 T: jongseong sieus */
0x11B1, /* 0x55 U: jongseong lieul-mieum */
0x11C1, /* 0x56 V: jongseong pieup */
0x11BD, /* 0x57 W: jongseong jieuj */
0x11C0, /* 0x58 X: jongseong tieut */
0x11B6, /* 0x59 Y: jongseong lieul-hieuh */
0x110F, /* 0x5A Z: choseong kieuk */
0x3156, /* 0x5B bracketleft: hangeul letter ye */
0x0000, /* 0x5C backslash: */
0x0000, /* 0x5D bracketright: */
0x005F, /* 0x5E asciicircum: underscore */
0x00D7, /* 0x5F underscore: multiplication sign */
0x0060, /* 0x60 quoteleft: grave accent */
0x1106, /* 0x61 a: choseong mieum */
0x3160, /* 0x62 b: hangeul letter yu */
0x110E, /* 0x63 c: choseong chieu */
0x110B, /* 0x64 d: choseong ieung */
0x1103, /* 0x65 e: choseong dieud */
0x1105, /* 0x66 f: choseong lieul */
0x1112, /* 0x67 g: chchoseong hieuh */
0x3157, /* 0x68 h: hangeul letter o */
0x3151, /* 0x69 i: hangeul letter ya */
0x3153, /* 0x6A j: hangeul letter eo */
0x314F, /* 0x6B k: hangeul letter a */
0x3163, /* 0x6C l: hangeul letter i */
0x3161, /* 0x6D m: hangeul letter eu */
0x315C, /* 0x6E n: hangeul letter u */
0x3150, /* 0x6F o: hangeul lteer ae */
0x3154, /* 0x70 p: hangeul letter e */
0x1107, /* 0x71 q: choseong bieub */
0x1100, /* 0x72 r: choseong gieug */
0x1102, /* 0x73 s: choseong nieun */
0x1109, /* 0x74 t: choseong sieus */
0x3155, /* 0x75 u: hangeul letter yeo */
0x1111, /* 0x76 v: choseong pieup */
0x110C, /* 0x77 w: choseong jieuj */
0x1110, /* 0x78 x: choseong tieut */
0x315B, /* 0x79 y: hangeul letter yo */
0x110F, /* 0x7A z: choseong kieuk */
0x3152, /* 0x7B braceleft: hangeul letter yae */
0x0000, /* 0x7C bar: */
0x0000, /* 0x7D braceright: */
0x007E /* 0x7E asciitilde: tilde */
];
// 1985 표준 타자기 (2벌식 호환형 4벌식)
K4_1985_Typewriter_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x11BB, /* 0x22 quotedbl: jongseong ssang_sieus */
0xFFE6, /* 0x23 numbersign: won sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x003A, /* 0x26 ampersand: colon */
0x1174, /* 0x27 apostrophe: jungseong eui */
0x0029, /* 0x28 parenleft: right parenthesis */
0x002D, /* 0x29 parenright: minus sign */
0x0028, /* 0x2A asterisk: left parenthesis *///0x0027 apostrophe
0x11B5, /* 0x2B plus: jongseong lieul-pieup */
0x002C, /* 0x2C comma: comma */
0x00D7, /* 0x2D minus: multiplication sign */
0x116E, /* 0x2E period: jungseong u */
0x1169, /* 0x2F slash: jungseong o */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x11AD, /* 0x3A colon: jongseong nieun-jieuj */
0x1168, /* 0x3B semicolon: jungseong ye */
0x11B9, /* 0x3C less: jongseong bieub-sieus */
0x002B, /* 0x3D equal: plus sign */
0x002E, /* 0x3E greater: period */
0x003F, /* 0x3F question: question mark */
0x0022, /* 0x40 at: quotatioin mark */
0x11B7, /* 0x41 A: jongseong mieum */
0x1172, /* 0x42 B: jungseong yu */
0x11BE, /* 0x43 C: jongseong chieuch */
0x11BC, /* 0x44 D: jongseong ieung */
0x11AE, /* 0x45 E: jongseong dieud */
0x11AF, /* 0x46 F: jongseong lieul */
0x11C2, /* 0x47 G: jongseong hieuh */
0x1169, /* 0x48 H: jungseong o */
0x1163, /* 0x49 I: jungseong ya */
0x1165, /* 0x4A J: jungseong eo */
0x1161, /* 0x4B K: jungseong a */
0x1175, /* 0x4C L: jungseong i */
0x1173, /* 0x4D M: jungseong eu */
0x116E, /* 0x4E N: jungseong u */
0x1162, /* 0x4F O: jungseong ae */
0x1166, /* 0x50 P: jungseong e */
0x11B8, /* 0x51 Q: jongseong bieub */
0x11A8, /* 0x52 R: jongseong gieug */
0x11AB, /* 0x53 S: jongseong nieun */
0x11BA, /* 0x54 T: jongseong sieus */
0x1167, /* 0x55 U: jungseong yeo */
0x11C1, /* 0x56 V: jongseong pieup */
0x11BD, /* 0x57 W: jongseong jieuj */
0x11C0, /* 0x58 X: jongseong tieut */
0x116D, /* 0x59 Y: jungseong yo */
0x110F, /* 0x5A Z: choseong kieuk */
0x1164, /* 0x5B bracketleft: jungseong yae */
0x0000, /* 0x5C backslash: */
0x0000, /* 0x5D bracketright: */
0x002F, /* 0x5E asciicircum: slash */
0x11A9, /* 0x5F underscore: jongseong ssang_gieug */
0x0060, /* 0x60 quoteleft: grave accent */
0x1106, /* 0x61 a: choseong mieum */
0x3160, /* 0x62 b: hangeul letter yu */
0x110E, /* 0x63 c: choseong chieu */
0x110B, /* 0x64 d: choseong ieung */
0x1103, /* 0x65 e: choseong dieud */
0x1105, /* 0x66 f: choseong lieul */
0x1112, /* 0x67 g: chchoseong hieuh */
0x3157, /* 0x68 h: hangeul letter o */
0x3151, /* 0x69 i: hangeul letter ya */
0x3153, /* 0x6A j: hangeul letter eo */
0x314F, /* 0x6B k: hangeul letter a */
0x3163, /* 0x6C l: hangeul letter i */
0x3161, /* 0x6D m: hangeul letter eu */
0x315C, /* 0x6E n: hangeul letter u */
0x3150, /* 0x6F o: hangeul lteer ae */
0x3154, /* 0x70 p: hangeul letter e */
0x1107, /* 0x71 q: choseong bieub */
0x1100, /* 0x72 r: choseong gieug */
0x1102, /* 0x73 s: choseong nieun */
0x1109, /* 0x74 t: choseong sieus */
0x3155, /* 0x75 u: hangeul letter yeo */
0x1111, /* 0x76 v: choseong pieup */
0x110C, /* 0x77 w: choseong jieuj */
0x1110, /* 0x78 x: choseong tieut */
0x315B, /* 0x79 y: hangeul letter yo */
0x110F, /* 0x7A z: choseong kieuk */
0x11B0, /* 0x7B braceleft: jongseong lieul-gieug */
0x0000, /* 0x7C bar: */
0x0000, /* 0x7D braceright: */
0x007E /* 0x7E asciitilde: tilde */
];
// 공병우 문장용 타자기
K3_Gong_Munjang_layout = [
0x11BE, /* 0x21 exclam: jongseong chieuch */
0x0021, /* 0x22 quotedbl: exclam */
0x11C1, /* 0x23 numbersign: jongseong pieup */
0x11BD, /* 0x24 dollar: jongseong jieuj */
0x3152, /* 0x25 percent: hangeul letter yae */
0x00B7, /* 0x26 ampersand: middle dot */
0x3162, /* 0x27 apostrophe: hangeul letter eui */
0x004B, /* 0x28 parenleft: K */
0x004D, /* 0x29 parenright: M */
0x002F, /* 0x2A asterisk: slash */
0x004C, /* 0x2B plus: L */
0x002C, /* 0x2C comma */
0x3156, /* 0x2D minus: hangeul letter ye */
0x002E, /* 0x2E period */
0x1169, /* 0x2F slash: jungseong o */
0x3151, /* 0x30 0: hangeul letter ya */
0x11AD, /* 0x31 1: jongseong nieun-hieuh */
0x11C2, /* 0x32 2: jongseong hieuh */
0x11BB, /* 0x33 3: jongseong ssang_sieus */
0x11B9, /* 0x34 4: jongseong bieub-sieus */
0x3150, /* 0x35 5: hangeul lteer ae */
0x3155, /* 0x36 6: hangeul letter yeo */
0x1111, /* 0x37 7: choseong pieup */
0x1110, /* 0x38 8: choseong tieuh */
0x110F, /* 0x39 9: choseong kieuk */
0x0034, /* 0x3A colon: 4 */
0x1107, /* 0x3B semicolon: choseong bieub */
0x003C, /* 0x3C less: less-than sign */
0x3160, /* 0x3D equal: hangeul letter yu */
0x003E, /* 0x3E greater: greater-than sign */
0x0023, /* 0x3F question: number sign */
0x11AE, /* 0x40 at: jongseong dieud */
0x11B2, /* 0x41 A: jongseong lieul-bieub */
0x300D, /* 0x42 B: right corner bracket 」 */
0x002A, /* 0x43 C: aterisk */
0x11B0, /* 0x44 D: jongseong lieul-gieug */
0x11A9, /* 0x45 E: jongseong ssang_gieug */
0x0022, /* 0x46 F: quotatioin mark */
0x003D, /* 0x47 G: equals sign */
0x0030, /* 0x48 H: 0 */
0x0037, /* 0x49 I: 7 */
0x0031, /* 0x4A J: 1 */
0x0032, /* 0x4B K: 2 */
0x0033, /* 0x4C L: 3 */
0x0029, /* 0x4D M: parenright */
0x0028, /* 0x4E N: parenleft */
0x0038, /* 0x4F O: 8 */
0x0039, /* 0x50 P: 9 */
0x11AA, /* 0x51 Q: jongseong gieug-sieus */
0x003F, /* 0x52 R: question mark */
0x11B6, /* 0x53 S: jongseong lieul-hieuh */
0x00D7, /* 0x54 T: multiplication sign × */
0x0036, /* 0x55 U: 6 */
0x300C, /* 0x56 V: left corner bracket 「 */
0x11B1, /* 0x57 W: jongseong lieul-mieum */
0x11AC, /* 0x58 X: jongseong nieun-jieuj */
0x0035, /* 0x59 Y: 5 */
0x11BF, /* 0x5A Z: jongseong kieuk */
0x116E, /* 0x5B bracketleft: jungseong u */
0x0000, /* 0x5C backslash: */
0x0000, /* 0x5D bracketright: */
0x003A, /* 0x5E asciicircum: middle colon */
0x0047, /* 0x5F underscore: G */
0x0000, /* 0x60 quoteleft: */
0x11BC, /* 0x61 a: jongseong ieung */
0x315C, /* 0x62 b: hangeul letter u */
0x3154, /* 0x63 c: hangeul letter e */
0x11AB, /* 0x64 d: jongseong nieun */
0x11A8, /* 0x65 e: jongseong gieug */
0x314F, /* 0x66 f: hangeul letter a */
0x3161, /* 0x67 g: hangeul letter eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1103, /* 0x6B k: choseong dieud */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x315B, /* 0x70 p: hangeul letter yo */
0x11C0, /* 0x71 q: jongseong tieut */
0x3163, /* 0x72 r: hangeul letter i */
0x11AF, /* 0x73 s: jongseong lieul */
0x3153, /* 0x74 t: hangeul letter eo */
0x1100, /* 0x75 u: choseong gieug */
0x3157, /* 0x76 v: hangeul letter o */
0x11B7, /* 0x77 w: jongseong mieum */
0x11BA, /* 0x78 x: jongseong sieus */
0x1105, /* 0x79 y: choseong lieul */
0x11B8, /* 0x7A z: jongseong bieub */
0x002D, /* 0x7B braceleft: minus sign */
0x0000, /* 0x7C bar: */
0x0000, /* 0x7D braceright: */
0x0000 /* 0x7E asciitilde: */
];
// 3-87 자판
K3_87_layout = [
0x11BF, /* 0x21 exclam: jongseong kieuk */
0x201D, /* 0x22 quotedbl: right double quotation mark ” */
0x11AC, /* 0x23 numbersign: jongseong nieun-jieuj */
0x11B1, /* 0x24 dollar: jongseong lieul-mieum */
0x11B4, /* 0x25 percent: jongseong lieul-tieut */
0x0037, /* 0x26 ampersand: 7 */
0x1110, /* 0x27 apostrophe: choseong tieuh */
0x0039, /* 0x28 parenleft: 9 */
0x002F, /* 0x29 parenright: slash */
0x0038, /* 0x2A asterisk: 8 */
0x002B, /* 0x2B plus */
0x002C, /* 0x2C comma */
0x300C, /* 0x2D minus: left corner bracket 「 */
0x002E, /* 0x2E period */
0x1169, /* 0x2F slash: jungseong o */
0x0029, /* 0x30 0: parenright */
0x11B8, /* 0x31 1: jongseong bieub */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x116D, /* 0x33 3: jungseong yo */
0x1168, /* 0x34 4: jungseong ye */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x0028, /* 0x37 7: parenleft */
0x1174, /* 0x38 8: jungseong eui */
0x116E, /* 0x39 9: jungseong u */
0x201C, /* 0x3A colon : left double quotation mark “ */
0x1107, /* 0x3B semicolon: choseong bieub */
0x002C, /* 0x3C less: comma */
0x300D, /* 0x3D equal: right corner bracket 」 */
0x002E, /* 0x3E greater: period */
0x00B7, /* 0x3F question: middle dot */
0x11BD, /* 0x40 at: jongseong jieuj */
0x11AE, /* 0x41 A: jongseong dieud */
0x11B2, /* 0x42 B: jongseong lieul-bieub */
0x203B, /* 0x43 C: reference mark */
0x1164, /* 0x44 D: jungseong yae */
0x11B8, /* 0x45 E: jongseong bieub */
0x11A9, /* 0x46 F: jongseong ssang_gieug */
0x11B0, /* 0x47 G: jongseong lieul-gieug */
0x0022, /* 0x48 H: quotatioin mark */
0x0035, /* 0x49 I: 5 */
0x0031, /* 0x4A J: 1 */
0x0032, /* 0x4B K: 2 */
0x0033, /* 0x4C L: 3 */
0x0030, /* 0x4D M: 0 */
0x002D, /* 0x4E N: minus sign */
0x0036, /* 0x4F O: 6 */
0x003C, /* 0x50 P: less-than sign */
0x11C1, /* 0x51 Q: jongseong pieup */
0x11C2, /* 0x52 R: jongseong hieuh */
0x11AD, /* 0x53 S: jongseong nieun-hieuh */
0x11B6, /* 0x54 T: jongseong lieul-hieuh */
0x0034, /* 0x55 U: 4 */
0x0021, /* 0x56 V: exclam */
0x11C0, /* 0x57 W: jongseong tieut */
0x11B9, /* 0x58 X: jongseong bieub-sieus */
0x0027, /* 0x59 Y: apostrophe */
0x11BE, /* 0x5A Z: jongseong chieuch */
0x110F, /* 0x5B bracketleft: choseong kieuk */
0x003A, /* 0x5C backslash: colon */
0x003F, /* 0x5D bracketright: question mark */
0x11B5, /* 0x5E asciicircum: jongseong lieul-pieup */
0x0025, /* 0x5F underscore: percent */
0x11B3, /* 0x60 quoteleft: jongseong lieul-sieus */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1162, /* 0x64 d: jungseong ae */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1103, /* 0x6B k: choseong dieud */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1175, /* 0x72 r: jungseong i */
0x11AB, /* 0x73 s: jongseong nieun */
0x1165, /* 0x74 t: jungseong eo */
0x1100, /* 0x75 u: choseong gieug */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x003E, /* 0x7B braceleft: greater-than sign */
0x003B, /* 0x7C bar: semicolon */
0x005C, /* 0x7D braceright : backslash */
0x11AA /* 0x7E asciitilde: jongseong gieug-sieus */
];
// 3-87 자판 확장 배열
K3_87_extended_layout = [
[[0x11B5],[0x0000]], /* 0x21 exclam: jongseong lieul-pieup */
[[0x2193],[0x0000]], /* 0x22 quotedbl: downwards arrow ↓ */
[[0x0023],[0x0000]], /* 0x23 numbersign: number sign */
[[0x0024],[0x0000]], /* 0x24 dollar: dollar */
[[0x00A9],[0x0000]], /* 0x25 percent: copyright sign © */
[[0x2122],[0x0000]], /* 0x26 ampersand: trademark ™ */
[[0x2019],[0x0000]], /* 0x27 apostrophe: right single quotation mark ’ */
[[0x25B2],[0x0000]], /* 0x28 parenleft: black up-pointing ▲ */
[[0x00B0],[0x0000]], /* 0x29 parenright: degree sign ° */
[[0x002A],[0x0000]], /* 0x2A asterisk: aterisk */
[[0x003D],[0x0000]], /* 0x2B plus: equal sign */
[[0x2192],[0x0000]], /* 0x2C comma: rightwards arrow → */
[[0x2610],[0x0000]], /* 0x2D minus: ballot box ☐ */
[[0x2190],[0x0000]], /* 0x2E period: leftwards arrow ← */
[[0x25FE],[0x0000]], /* 0x2F slash: black medium small square ◾ */
[[0x2070],[0x0000]], /* 0x30 0: superscript zero ⁰ */
[[0x00B9],[0x0000]], /* 0x31 1: superscript one ¹ */
[[0x00B2],[0x0000]], /* 0x32 2: superscript two ² */
[[0x00B3],[0x0000]], /* 0x33 3: superscript three ³ */
[[0x2074],[0x0000]], /* 0x34 4: superscript four ⁴ */
[[0x2075],[0x0000]], /* 0x35 5: superscript five ⁵ */
[[0x2076],[0x0000]], /* 0x36 6: superscript six ⁶ */
[[0x2077],[0x0000]], /* 0x37 7: superscript seven ⁷ */
[[0x2078],[0x0000]], /* 0x38 8: superscript eight ⁸ */
[[0x2079],[0x0000]], /* 0x39 9: superscript nine ⁹ */
[[0x2191],[0x0000]], /* 0x3A colon: upwards arrow ↑ */
[[0x2018],[0x0000]], /* 0x3B semicolon: left single quotation mark ‘ */
[[0x2198],[0x0000]], /* 0x3C less: south-east arrow ↘ */
[[0x2713],[0x0000]], /* 0x3D equal: check mark ✓ */
[[0x2015],[0x0000]], /* 0x3E greater: horizontal bar ― */
[[0x00B1],[0x0000]], /* 0x3F question: plus minus sign ± */
[[0x0040],[0x0000]], /* 0x40 at: at */
[[0x0041],[0x0000]], /* 0x41 A: */
[[0x0042],[0x0000]], /* 0x42 B: */
[[0x0043],[0x0000]], /* 0x43 C: */
[[0x0044],[0x0000]], /* 0x44 D: */
[[0x0045],[0x0000]], /* 0x45 E: */
[[0x0046],[0x0000]], /* 0x46 F: */
[[0x0047],[0x0000]], /* 0x47 G: */
[[0x0048],[0x0000]], /* 0x48 H: */
[[0x0049],[0x0000]], /* 0x49 I: */
[[0x004A],[0x0000]], /* 0x4A J: */
[[0x004B],[0x0000]], /* 0x4B K: */
[[0x004C],[0x0000]], /* 0x4C L: */
[[0x004D],[0x0000]], /* 0x4D M: */
[[0x004E],[0x0000]], /* 0x4E N: */
[[0x004F],[0x0000]], /* 0x4F O: */
[[0x0050],[0x0000]], /* 0x50 P: */
[[0x0051],[0x0000]], /* 0x51 Q: */
[[0x0052],[0x0000]], /* 0x52 R: */
[[0x0053],[0x0000]], /* 0x53 S: */
[[0x0054],[0x0000]], /* 0x54 T: */
[[0x0055],[0x0000]], /* 0x55 U: */
[[0x0056],[0x0000]], /* 0x56 V: */
[[0x0057],[0x0000]], /* 0x57 W: */
[[0x0058],[0x0000]], /* 0x58 X: */
[[0x0059],[0x0000]], /* 0x59 Y: */
[[0x005A],[0x0000]], /* 0x5A Z: */
[[0x005B],[0x0000]], /* 0x5B bracketleft: bracketleft */
[[0x005C],[0x0000]], /* 0x5C backslash: backslash */
[[0x005D],[0x0000]], /* 0x5D bracketright: bracketright */
[[0x00AE],[0x0000]], /* 0x5E asciicircum: registerd sign ® */
[[0x007E],[0x0000]], /* 0x5F underscore: tilde */
[[0x11B3],[0x0000]], /* 0x60 quoteleft: jongseong lieul-sieus */
[[0x0061],[0x0000]], /* 0x61 a: */
[[0x0062],[0x0000]], /* 0x62 b: */
[[0x0063],[0x0000]], /* 0x63 c: */
[[0x0064],[0x0000]], /* 0x64 d: */
[[0x0065],[0x0000]], /* 0x65 e: */
[[0x0066],[0x0000]], /* 0x66 f: */
[[0x0067],[0x0000]], /* 0x67 g: */
[[0x0068],[0x0000]], /* 0x68 h: */
[[0x0069],[0x0000]], /* 0x69 i: */
[[0x006A],[0x0000]], /* 0x6A j: */
[[0x006B],[0x0000]], /* 0x6B k: */
[[0x006C],[0x0000]], /* 0x6C l: */
[[0x006D],[0x0000]], /* 0x6D m: */
[[0x006E],[0x0000]], /* 0x6E n: */
[[0x006F],[0x0000]], /* 0x6F o: */
[[0x0070],[0x0000]], /* 0x70 p: */
[[0x0071],[0x0000]], /* 0x71 q: */
[[0x0072],[0x0000]], /* 0x72 r: */
[[0x0073],[0x0000]], /* 0x73 s: */
[[0x0074],[0x0000]], /* 0x74 t: */
[[0x0075],[0x0000]], /* 0x75 u: */
[[0x0076],[0x0000]], /* 0x76 v: */
[[0x0077],[0x0000]], /* 0x77 w: */
[[0x0078],[0x0000]], /* 0x78 x: */
[[0x0079],[0x0000]], /* 0x79 y: */
[[0x007A],[0x0000]], /* 0x7A z: */
[[0x007B],[0x0000]], /* 0x7B braceleft: braceleft */
[[0x007C],[0x0000]], /* 0x7C bar: vertical bar */
[[0x007D],[0x0000]], /* 0x7D braceright: braceright */
[[0x11AA],[0x0000]] /* 0x7E asciitilde: jongseong gieug-sieus */
];
// 3-89 자판
K3_89_layout = [
0x11C2, /* 0x21 exclam: jongseong hieuh */
0x0023, /* 0x22 quotedbl: number sign */
0x11BF, /* 0x23 numbersign: jongseong kieuk */
0x11B5, /* 0x24 dollar: jongseong lieul-pieup */
0x11B4, /* 0x25 percent: jongseong lieul-tieut */
0x0037, /* 0x26 ampersand: 7 */
0x1110, /* 0x27 apostrophe: choseong tieuh */
0x0039, /* 0x28 parenleft: 9 */
0x0025, /* 0x29 parenright: percent */
0x0038, /* 0x2A asterisk: 8 */
0x002B, /* 0x2B plus */
0x002C, /* 0x2C comma */
0x002A, /* 0x2D minus: aterisk */
0x002E, /* 0x2E period */
0x1169, /* 0x2F slash: jungseong o */
0x0029, /* 0x30 0: parenright */
0x11B8, /* 0x31 1: jongseong bieub */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x116D, /* 0x33 3: jungseong yo */
0x1168, /* 0x34 4: jungseong ye */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x0028, /* 0x37 7: parenleft */
0x1174, /* 0x38 8: jungseong eui */
0x116E, /* 0x39 9: jungseong u */
0x003A, /* 0x3A colon */
0x1107, /* 0x3B semicolon: choseong bieub */
0x002C, /* 0x3C less: comma */
0x003D, /* 0x3D equal */
0x002E, /* 0x3E greater: period */
0x0021, /* 0x3F question: exclam */
0x11BD, /* 0x40 at: jongseong jieuj */
0x11AE, /* 0x41 A: jongseong dieud */
0x003F, /* 0x42 B: question mark */
0x11B1, /* 0x43 C: jongseong lieul-mieum */
0x11B0, /* 0x44 D: jongseong lieul-gieug */
0x11AC, /* 0x45 E: jongseong nieun-jieuj */
0x11A9, /* 0x46 F: jongseong ssang_gieug */
0x11AA, /* 0x47 G: jongseong gieug-sieus */
0x0022, /* 0x48 H: quotatioin mark */
0x0035, /* 0x49 I: 5 */
0x0031, /* 0x4A J: 1 */
0x0032, /* 0x4B K: 2 */
0x0033, /* 0x4C L: 3 */
0x0030, /* 0x4D M: 0 */
0x002D, /* 0x4E N: minus sign */
0x0036, /* 0x4F O: 6 */
0x007E, /* 0x50 P: tilde */
0x11C1, /* 0x51 Q: jongseong pieup */
0x11B6, /* 0x52 R: jongseong lieul-hieuh */
0x11AD, /* 0x53 S: jongseong nieun-hieuh */
0x11B3, /* 0x54 T: jongseong lieul-sieus */
0x0034, /* 0x55 U: 4 */
0x11B2, /* 0x56 V: jongseong lieul-bieub */
0x11C0, /* 0x57 W: jongseong tieut */
0x11B9, /* 0x58 X: jongseong bieub-sieus */
0x0027, /* 0x59 Y: apostrophe */
0x11BE, /* 0x5A Z: jongseong chieuch */
0x110F, /* 0x5B bracketleft: choseong kieuk */
0x005C, /* 0x5C backslash */
0x003B, /* 0x5D bracketright: semicolon */
0x1164, /* 0x5E asciicircum: jungseong yae */
0x002F, /* 0x5F underscore: slash */
0x005B, /* 0x60 quoteleft: bracketleft */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1162, /* 0x72 r: jungseong ae */
0x11AB, /* 0x73 s: jongseong nieun */
0x1165, /* 0x74 t: jungseong eo */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft */
0x0040, /* 0x7C bar: commertial at */
0x007D, /* 0x7D braceright */
0x005D /* 0x7E asciitilde: bracketright */
];
// 3-891 자판
K3_891_layout = [
0x11BD, /* 0x21 exclam: jongseong jieuj */
0x00B7, /* 0x22 quotedbl: middle dot */
0x11BF, /* 0x23 numbersign: jongseong kieuk */
0x11B5, /* 0x24 dollar: jongseong lieul-pieup */
0x11B4, /* 0x25 percent: jongseong lieul-tieut */
0x201C, /* 0x26 ampersand: left double quotation mark */
0x1110, /* 0x27 apostrophe: choseong tieuh */
0x2018, /* 0x28 parenleft: left single quotation mark */
0x2019, /* 0x29 parenright: right single quotation mark */
0x201D, /* 0x2A asterisk: right double quotation mark */
0x002B, /* 0x2B plus */
0x002C, /* 0x2C comma */
0x0029, /* 0x2D minus: parenright */
0x002E, /* 0x2E period */
0x1169, /* 0x2F slash: jungseong o */
0x110F, /* 0x30 0: choseong kieuk */
0x11C2, /* 0x31 1: jongseong hieuh */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x11B8, /* 0x33 3: jongseong bieub */
0x116D, /* 0x34 4: jungseong yo */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x1168, /* 0x37 7: jungseong ye */
0x1174, /* 0x38 8: jungseong eui */
0x116E, /* 0x39 9: jungseong u */
0x0034, /* 0x3A colon: 4 */
0x1107, /* 0x3B semicolon: choseong bieub */
0x002C, /* 0x3C less: comma */
0x003A, /* 0x3D equal: colon */
0x002E, /* 0x3E greater: period */
0x0021, /* 0x3F question: exclam */
0x203B, /* 0x40 at: reference mark */
0x11AE, /* 0x41 A: jongseong dieud */
0x003F, /* 0x42 B: question mark */
0x11B1, /* 0x43 C: jongseong lieul-mieum */
0x11B0, /* 0x44 D: jongseong lieul-gieug */
0x11AC, /* 0x45 E: jongseong nieun-jieuj */
0x11A9, /* 0x46 F: jongseong ssang_gieug */
0x11AA, /* 0x47 G: jongseong gieug-sieus */
0x0030, /* 0x48 H: 0 */
0x0037, /* 0x49 I: 7 */
0x0031, /* 0x4A J: 1 */
0x0032, /* 0x4B K: 2 */
0x0033, /* 0x4C L: 3 */
0x0022, /* 0x4D M: quotatioin mark */
0x002D, /* 0x4E N: minus sign */
0x0038, /* 0x4F O: 8 */
0x0039, /* 0x50 P: 9 */
0x11C1, /* 0x51 Q: jongseong pieup */
0x11B6, /* 0x52 R: jongseong lieul-hieuh */
0x11AD, /* 0x53 S: jongseong nieun-hieuh */
0x11B3, /* 0x54 T: jongseong lieul-sieus */
0x0036, /* 0x55 U: 6 */
0x11B2, /* 0x56 V: jongseong lieul-bieub */
0x11C0, /* 0x57 W: jongseong tieut */
0x11B9, /* 0x58 X: jongseong bieub-sieus */
0x0035, /* 0x59 Y: 5 */
0x11BE, /* 0x5A Z: jongseong chieuch */
0x0028, /* 0x5B bracketleft: parenleft */
0x003E, /* 0x5C backslash: greater-than sign */
0x003C, /* 0x5D bracketright: less-than sign */
0x2170, /* 0x5E asciicircum: small roman numeral one */
0x007E, /* 0x5F underscore: tilde */
0x1164, /* 0x60 quoteleft: jungseong yae */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1162, /* 0x72 r: jungseong ae */
0x11AB, /* 0x73 s: jongseong nieun */
0x1165, /* 0x74 t: jungseong eo */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x0025, /* 0x7B braceleft: percent */
0x005C, /* 0x7C bar: backslash */
0x002F, /* 0x7D braceright: slash */
0x0000 /* 0x7E asciitilde */
];
// 3-891 자판 확장 배열
K3_891_extended_layout = [
[[0xFFE5],[0x0000]], /* 0x21 exclam: fullwidth yen sign */
[[0x2193],[0x0000]], /* 0x22 quotedbl: downwards arrow ↓ */
[[0x0023],[0x0000]], /* 0x23 numbersign: number sign */
[[0x0024],[0x0000]], /* 0x24 dollar: dollar */
[[0x00B1],[0x0000]], /* 0x25 percent: plus minus sign ± */
[[0x2026],[0x0000]], /* 0x26 ampersand: horizontal ellipsis … */
[[0x110A],[0x0000]], /* 0x27 apostrophe: choseong ssang-sieus */
[[0x25B2],[0x0000]], /* 0x28 parenleft: black up-pointing ▲ */
[[0x00B0],[0x0000]], /* 0x29 parenright: degree sign ° */
[[0x002A],[0x0000]], /* 0x2A asterisk: asterisk */
[[0x003D],[0x0000]], /* 0x2B plus: equal sign */
[[0x1101],[0x0000]], /* 0x2C comma: choseong ssang-gieug */
[[0x2610],[0x0000]], /* 0x2D minus: ballot box ☐ */
[[0x110D],[0x0000]], /* 0x2E period: choseong ssang-jieuj */
[[0x1108],[0x0000]], /* 0x2F slash: choseong ssang-bieub */
[[0x2122],[0x0000]], /* 0x30 0: trademark ™ */
[[0x00B9],[0x0000]], /* 0x31 1: superscript one ¹ */
[[0x00B2],[0x0000]], /* 0x32 2: superscript two ² */
[[0xACF5],[0x0000]], /* 0x33 3: 공 */
[[0xACF5],[0x0000]], /* 0x34 4: 공 */
[[0x327E],[0x0000]], /* 0x35 5: circled hangeul ieung u */
[[0x2076],[0x0000]], /* 0x36 6: superscript six ⁶, */
[[0x2015],[0x0000]], /* 0x37 7: horizontal bar ―, */
[[0x00AE],[0x0000]], /* 0x38 8: registerd sign ® */
[[0x00A9],[0x0000]], /* 0x39 9: copyright sign © */
[[0x2191],[0x0000]], /* 0x3A colon: upwards arrow ↑ */
[[0x1104],[0x0000]], /* 0x3B semicolon: choseong ssang-dieud, */
[[0x2198],[0x0000]], /* 0x3C less: south-east arrow ↘ */
[[0x2713],[0x0000]], /* 0x3D equal: check mark ✓ */
[[0x2190],[0x0000]], /* 0x3E greater: leftwards arrow ← */
[[0x25FE],[0x0000]], /* 0x3F question: black medium small square ◾ */
[[0x0040],[0x0000]], /* 0x40 at: at */
[[0x0041],[0x0000]], /* 0x41 A: */
[[0x0042],[0x0000]], /* 0x42 B: */
[[0x0043],[0x0000]], /* 0x43 C: */
[[0x0044],[0x0000]], /* 0x44 D: */
[[0x0045],[0x0000]], /* 0x45 E: */
[[0x0046],[0x0000]], /* 0x46 F: */
[[0x0047],[0x0000]], /* 0x47 G: */
[[0x0048],[0x0000]], /* 0x48 H: */
[[0x0049],[0x0000]], /* 0x49 I: */
[[0x004A],[0x0000]], /* 0x4A J: */
[[0x004B],[0x0000]], /* 0x4B K: */
[[0x004C],[0x0000]], /* 0x4C L: */
[[0x004D],[0x0000]], /* 0x4D M: */
[[0x004E],[0x0000]], /* 0x4E N: */
[[0x004F],[0x0000]], /* 0x4F O: */
[[0x0050],[0x0000]], /* 0x50 P: */
[[0x0051],[0x0000]], /* 0x51 Q: */
[[0x0052],[0x0000]], /* 0x52 R: */
[[0x0053],[0x0000]], /* 0x53 S: */
[[0x0054],[0x0000]], /* 0x54 T: */
[[0x0055],[0x0000]], /* 0x55 U: */
[[0x0056],[0x0000]], /* 0x56 V: */
[[0x0057],[0x0000]], /* 0x57 W: */
[[0x0058],[0x0000]], /* 0x58 X: */
[[0x0059],[0x0000]], /* 0x59 Y: */
[[0x005A],[0x0000]], /* 0x5A Z: */
[[0x005B],[0x0000]], /* 0x5B bracketleft: bracketleft */
[[0x005C],[0x0000]], /* 0x5C backslash: backslash, */
[[0x005D],[0x0000]], /* 0x5D bracketright: bracketright */
[[0x0000],[0x0000]], /* 0x5E asciicircum: */
[[0x25A1],[0x0000]], /* 0x5F underscore: white squre */
[[0x0000],[0x0000]], /* 0x60 quoteleft: */
[[0x0061],[0x0000]], /* 0x61 a: */
[[0x0062],[0x0000]], /* 0x62 b: */
[[0x0063],[0x0000]], /* 0x63 c: */
[[0x0064],[0x0000]], /* 0x64 d: */
[[0x0065],[0x0000]], /* 0x65 e: */
[[0x0066],[0x0000]], /* 0x66 f: */
[[0x0067],[0x0000]], /* 0x67 g: */
[[0x0068],[0x0000]], /* 0x68 h: */
[[0x0069],[0x0000]], /* 0x69 i: */
[[0x006A],[0x0000]], /* 0x6A j: */
[[0x006B],[0x0000]], /* 0x6B k: */
[[0x006C],[0x0000]], /* 0x6C l: */
[[0x006D],[0x0000]], /* 0x6D m: */
[[0x006E],[0x0000]], /* 0x6E n: */
[[0x006F],[0x0000]], /* 0x6F o: */
[[0x0070],[0x0000]], /* 0x70 p: */
[[0x0071],[0x0000]], /* 0x71 q: */
[[0x0072],[0x0000]], /* 0x72 r: */
[[0x0073],[0x0000]], /* 0x73 s: */
[[0x0074],[0x0000]], /* 0x74 t: */
[[0x0075],[0x0000]], /* 0x75 u: */
[[0x0076],[0x0000]], /* 0x76 v: */
[[0x0077],[0x0000]], /* 0x77 w: */
[[0x0078],[0x0000]], /* 0x78 x: */
[[0x0079],[0x0000]], /* 0x79 y: */
[[0x007A],[0x0000]], /* 0x7A z: */
[[0x300C],[0x0000]], /* 0x7B braceleft: left corner bracker 「 */
[[0x007C],[0x0000]], /* 0x7C bar: vertical bar */
[[0x300D],[0x0000]], /* 0x7D braceright: right corner bracker 」 */
[[0x003B],[0x0000]] /* 0x7E asciitilde: semicolon */
];
// 3-95 자판안 (김창용)
K3_95_proposal_layout = [
0x11A9, /* 0x21 exclam: jongseong ssang_gieug */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x11BD, /* 0x23 numbersign: jognseong jieuj */
0x11B5, /* 0x24 dollar: jongseong lieul-pieup */
0x11B4, /* 0x25 percent: jongseong lieul-tieut */
0x0026, /* 0x26 ampersand: ampersand */
0x1110, /* 0x27 apostrophe: choseong tieut */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x1169, /* 0x2F slash: jungseong o */
0x110F, /* 0x30 0: choseong kieuk */
0x11C2, /* 0x31 1: jongseong hieuh */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x11B8, /* 0x33 3: jongseong bieub */
0x116D, /* 0x34 4: jungseong yo */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x1168, /* 0x37 7: jungseong ye */
0x1174, /* 0x38 8: jungseong yi */
0x116E, /* 0x39 9: jungseong u */
0x003A, /* 0x3A colon: colon */
0x1107, /* 0x3B semicolon: choseong bieub */
0x0032, /* 0x3C less: 2 */
0x003D, /* 0x3D equal: euals sign */
0x0033, /* 0x3E greater: 3 */
0x003F, /* 0x3F question: question mark */
0x11B0, /* 0x40 at: jongseong lieul-gieug */
0x11AE, /* 0x41 A: jongseong dieud */
0x0021, /* 0x42 B: exclamation mark */
0x11BF, /* 0x43 C: jongseong kieuk */
0x11B2, /* 0x44 D: jongseong lieul-bieub */
0x11AC, /* 0x45 E: jongseong nieun-jieuj */
0x11B1, /* 0x46 F: jongseong lieul-mieum */
0x1164, /* 0x47 G: jungseong yae */
0x0027, /* 0x48 H: apostrophe */
0x0038, /* 0x49 I: 8 */
0x0034, /* 0x4A J: 4 */
0x0035, /* 0x4B K: 5 */
0x0036, /* 0x4C L: 6 */
0x0031, /* 0x4D M: 1 */
0x0030, /* 0x4E N: 0 */
0x0039, /* 0x4F O: 9 */
0x003E, /* 0x50 P: greater-than sign */
0x11C1, /* 0x51 Q: jongseong pieup */
0x11B6, /* 0x52 R: jongseong lieul-hieuh */
0x11AD, /* 0x53 S: jongseong nieun-hieuh */
0x11B3, /* 0x54 T: jongseong lieul-sieus */
0x0037, /* 0x55 U: 7 */
0x11AA, /* 0x56 V: jongseong gieug-sieus */
0x11C0, /* 0x57 W: jongseong tieut */
0x11B9, /* 0x58 X: jongseong bieub-sieus */
0x003C, /* 0x59 Y: less-than sign */
0x11BE, /* 0x5A Z: jongseong chieuch */
0x005B, /* 0x5B bracketleft: left bracket */
0x003B, /* 0x5C backslash: semicolon */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1162, /* 0x72 r: jungseong ae */
0x11AB, /* 0x73 s: jongseong nieun */
0x1165, /* 0x74 t: jungseong eo */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft: left brace */
0x00B7, /* 0x7C bar: middle dot */
0x007D, /* 0x7D braceright: right brace */
0x007E, /* 0x7E asciitilde: tilde */
];
K3_95_extended_layout = [ // 3-95 김창용 자판안 확장 배열
[[0x0000],[0x0000]], /* 0x21 exclam: */
[[0x0000],[0x0000]], /* 0x22 quotedbl: */
[[0x0000],[0x0000]], /* 0x23 numbersign */
[[0x0000],[0x0000]], /* 0x24 dollar */
[[0x0000],[0x0000]], /* 0x25 percent: */
[[0x0000],[0x0000]], /* 0x26 ampersand: */
[[0x0000],[0x0000]], /* 0x27 apostrophe: */
[[0x0000],[0x0000]], /* 0x28 parenleft: */
[[0x0000],[0x0000]], /* 0x29 parenright: */
[[0x0000],[0x0000]], /* 0x2A asterisk */
[[0x0000],[0x0000]], /* 0x2B plus: */
[[0x2026],[0x0000]], /* 0x2C comma: horizontal epllipsis … */
[[0x00F7],[0x0000]], /* 0x2D minus: division sign ÷ */
[[0x00B7],[0x0000]], /* 0x2E period: middle dot · */
[[0x0000],[0x0000]], /* 0x2F slash: */
[[0x300B],[0x0000]], /* 0x30 0: right double angle bracket 》 */
[[0xF8FF],[0x0000]], /* 0x31 1: apple logo (PUA) */
[[0x0040],[0x0000]], /* 0x32 2: commercial at */
[[0x0023],[0x0000]], /* 0x33 3: number sign # */
[[0x0024],[0x0000]], /* 0x34 4: dollar sign $ */
[[0x0025],[0x0000]], /* 0x35 5: percent sign % */
[[0x005E],[0x0000]], /* 0x36 6: circumflex accent ^ */
[[0x26AB],[0x0000]], /* 0x37 7: medium black circle ⚫ */
[[0x203B],[0x0000]], /* 0x38 8: reference mark ※ */
[[0x300A],[0x0000]], /* 0x39 9: left double angle bracket 《 */
[[0x0000],[0x0000]], /* 0x3A colon: */
[[0x1108],[0x0000]], /* 0x3B semicolon: choseong ssang_bieup */
[[0x0000],[0x0000]], /* 0x3C less: */
[[0x2260],[0x0000]], /* 0x3D equal: not equal to */
[[0x0000],[0x0000]], /* 0x3E greater: */
[[0x0000],[0x0000]], /* 0x3F question: */
[[0x0000],[0x0000]], /* 0x40 at */
[[0x0000],[0x0000]], /* 0x41 A: */
[[0x0000],[0x0000]], /* 0x42 B: */
[[0x0000],[0x0000]], /* 0x43 C: */
[[0x0000],[0x0000]], /* 0x44 D: */
[[0x0000],[0x0000]], /* 0x45 E: */
[[0x0000],[0x0000]], /* 0x46 F: */
[[0x0000],[0x0000]], /* 0x47 G: */
[[0x0000],[0x0000]], /* 0x48 H: */
[[0x0000],[0x0000]], /* 0x49 I: */
[[0x0000],[0x0000]], /* 0x4A J: */
[[0x0000],[0x0000]], /* 0x4B K: */
[[0x0000],[0x0000]], /* 0x4C L: */
[[0x0000],[0x0000]], /* 0x4D M: */
[[0x0000],[0x0000]], /* 0x4E N: */
[[0x0000],[0x0000]], /* 0x4F O: */
[[0x0000],[0x0000]], /* 0x50 P: */
[[0x0000],[0x0000]], /* 0x51 Q: */
[[0x0000],[0x0000]], /* 0x52 R: */
[[0x0000],[0x0000]], /* 0x53 S: */
[[0x0000],[0x0000]], /* 0x54 T: */
[[0x0000],[0x0000]], /* 0x55 U: */
[[0x0000],[0x0000]], /* 0x56 V: */
[[0x0000],[0x0000]], /* 0x57 W: */
[[0x0000],[0x0000]], /* 0x58 X: */
[[0x0000],[0x0000]], /* 0x59 Y: */
[[0x0000],[0x0000]], /* 0x5A Z: */
[[0x201C],[0x0000]], /* 0x5B bracketleft: left double quotation mark “ */
[[0x003A],[0x0000]], /* 0x5C backslash: colon */
[[0x201D],[0x0000]], /* 0x5D bracketright: right double quotation mark ” */
[[0x0000],[0x0000]], /* 0x5E asciicircum: */
[[0x0000],[0x0000]], /* 0x5F underscore: */
[[0x007E],[0x0000]], /* 0x60 quoteleft: tilde */
[[0x11BC],[0x0000]], /* 0x61 a: jongseong ieung */
[[0x116E],[0x0000]], /* 0x62 b: jungseong u */
[[0x1166],[0x0000]], /* 0x63 c: jungseong e */
[[0x1175],[0x0000]], /* 0x64 d: jungseong i */
[[0x1167],[0x0000]], /* 0x65 e: jungseong yeo */
[[0x1161],[0x0000]], /* 0x66 f: jungseong a */
[[0x1173],[0x0000]], /* 0x67 g: jungseong eu */
[[0x1102],[0x0000]], /* 0x68 h: choseong nieun */
[[0x1106],[0x0000]], /* 0x69 i: choseong mieum */
[[0x110B],[0x0000]], /* 0x6A j: choseong ieung */
[[0x1101],[0x0000]], /* 0x6B k: choseong ssang_gieug */
[[0x110D],[0x0000]], /* 0x6C l: choseong ssang_jieuj */
[[0x1112],[0x0000]], /* 0x6D m: choseong hieuh */
[[0x110A],[0x0000]], /* 0x6E n: choseong ssang_sieus */
[[0x2018],[0x0000]], /* 0x6F o: left single quotation mark ‘ */
[[0x2019],[0x0000]], /* 0x70 p: right single quotation mark ’ */
[[0x11BA],[0x0000]], /* 0x71 q: jongseong sieus */
[[0x1162],[0x0000]], /* 0x72 r: jungseong ae */
[[0x11AB],[0x0000]], /* 0x73 s: jongseong nieun */
[[0x1165],[0x0000]], /* 0x74 t: jungseong eo */
[[0x1104],[0x0000]], /* 0x75 u: choseong ssang_dieud */
[[0x1169],[0x0000]], /* 0x76 v: jungseong o */
[[0x11AF],[0x0000]], /* 0x77 w: jongseong lieul */
[[0x11A8],[0x0000]], /* 0x78 x: jongseong gieug */
[[0x1105],[0x0000]], /* 0x79 y: choseong lieul */
[[0x11B7],[0x0000]], /* 0x7A z: jongseong mieum */
[[0x0000],[0x0000]], /* 0x7B braceleft: */
[[0x0000],[0x0000]], /* 0x7C bar: */
[[0x0000],[0x0000]], /* 0x7D braceright: */
[[0x0000],[0x0000]] /* 0x7E asciitilde: */
];
// 순아래 자판
K3_sun1990_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x1110, /* 0x27 apostrophe: choseong tieut */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x11BD, /* 0x2D minus: jongseong jieuj */
0x002E, /* 0x2E period: period */
0x11AE, /* 0x2F slash: jongseong dieud */
0x1164, /* 0x30 0: choseong yae */
0x11C2, /* 0x31 1: jongseong hieuh */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x11B8, /* 0x33 3: jongseong bieub */
0x116D, /* 0x34 4: jungseong yo */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x1168, /* 0x37 7: jungseong ye */
0x1174, /* 0x38 8: jungseong yi */
0x110F, /* 0x39 9: choseong kieuk */
0x003A, /* 0x3A colon: colon */
0x1107, /* 0x3B semicolon: choseong bieub */
0x0032, /* 0x3C less: 2 */
0x11BE, /* 0x3D equal: jongseong chieuch */
0x0033, /* 0x3E greater: 3 */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x11BC, /* 0x41 A: jongseong ieung */
0x0021, /* 0x42 B: exclamation mark */
0x005C, /* 0x43 C: backslash */
0x005D, /* 0x44 D: right bracket */
0x1167, /* 0x45 E: jungseong yeo */
0x1161, /* 0x46 F: jungseong a */
0x002F, /* 0x47 G: slash */
0x0027, /* 0x48 H: apostrophe */
0x0038, /* 0x49 I: 8 */
0x0034, /* 0x4A J: 4 */
0x0035, /* 0x4B K: 5 */
0x0036, /* 0x4C L: 6 */
0x0031, /* 0x4D M: 1 */
0x0030, /* 0x4E N: 0 */
0x0039, /* 0x4F O: 9 */
0x003E, /* 0x50 P: greater-than sign */
0x11BA, /* 0x51 Q: jongseong sieus */
0x1162, /* 0x52 R: jungseong ae */
0x005B, /* 0x53 S: left bracket */
0x003B, /* 0x54 T: semicolon */
0x0037, /* 0x55 U: 7 */
0x1169, /* 0x56 V: jungseong o */
0x11AF, /* 0x57 W: jongseong lieul */
0x003D, /* 0x58 X: equals sign */
0x003C, /* 0x59 Y: less-than sign */
0x002D, /* 0x5A Z: minus sign */
0x11C0, /* 0x5B bracketleft: jongseong tieut */
0x11BF, /* 0x5C backslash: jongseong kieuk */
0x11C1, /* 0x5D bracketright: jongseong pieup */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1162, /* 0x72 r: jungseong ae */
0x11AB, /* 0x73 s: jongseong nieun */
0x1165, /* 0x74 t: jungseong eo */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E, /* 0x7E asciitilde: tilde */
];
K3_Gaon38A_layout = [
0x0021, /* 0x21 exclam: */
0x0022, /* 0x22 quotedbl: */
0x0023, /* 0x23 numbersign: */
0x0024, /* 0x24 dollar: */
0x0025, /* 0x25 percent: */
0x0026, /* 0x26 ampersand: */
0x0027, /* 0x27 apostrophe: */
0x0028, /* 0x28 parenleft: */
0x0029, /* 0x29 parenright: */
0x002A, /* 0x2A asterisk: */
0x002B, /* 0x2B plus: */
0x002C, /* 0x2C comma: */
0x002D, /* 0x2D minus: */
0x002E, /* 0x2E period: */
0x1110, /* 0x2F slash: */
0x110F, /* 0x30 0: */
0x11BF, /* 0x31 1: */
0x11BE, /* 0x32 2: */
0x11C1, /* 0x33 3: */
0x116D, /* 0x34 4: */
0x1172, /* 0x35 5: */
0x1163, /* 0x36 6: */
0x11C0, /* 0x37 7: */
0x11BD, /* 0x38 8: */
0x11AE, /* 0x39 9: */
0x003A, /* 0x3A colon: */
0x1107, /* 0x3B semicolon: */
0x003C, /* 0x3C less: */
0x003D, /* 0x3D equal: */
0x003E, /* 0x3E greater: */
0x003F, /* 0x3F question: */
0x0040, /* 0x40 at: */
0x0000, /* 0x41 A: */
0x2019, /* 0x42 B: */
0x0000, /* 0x43 C: */
0x20A9, /* 0x44 D: */
0x0033, /* 0x45 E: */
0x002E, /* 0x46 F: */
0x002F, /* 0x47 G: */
0x0000, /* 0x48 H: */
0x0038, /* 0x49 I: */
0x0000, /* 0x4A J: */
0x0000, /* 0x4B K: */
0x003B, /* 0x4C L: */
0x0000, /* 0x4D M: */
0x0000, /* 0x4E N: */
0x0039, /* 0x4F O: */
0x0030, /* 0x50 P: */
0x0031, /* 0x51 Q: */
0x0034, /* 0x52 R: */
0x203B, /* 0x53 S: */
0x0035, /* 0x54 T: */
0x0037, /* 0x55 U: */
0x2018, /* 0x56 V: */
0x0032, /* 0x57 W: */
0x0000, /* 0x58 X: */
0x0036, /* 0x59 Y: */
0x0000, /* 0x5A Z: */
0x005B, /* 0x5B bracketleft: */
0x005C, /* 0x5C backslash: */
0x005D, /* 0x5D bracketright: */
0x005E, /* 0x5E asciicircum: */
0x005F, /* 0x5F underscore: */
0x0060, /* 0x60 quoteleft: */
0x11BC, /* 0x61 a: */
0x116E, /* 0x62 b: */
0x11A8, /* 0x63 c: */
0x1175, /* 0x64 d: */
0x11AF, /* 0x65 e: */
0x1161, /* 0x66 f: */
0x1173, /* 0x67 g: */
0x1102, /* 0x68 h: */
0x1106, /* 0x69 i: */
0x110B, /* 0x6A j: */
0x1100, /* 0x6B k: */
0x110C, /* 0x6C l: */
0x1112, /* 0x6D m: */
0x1109, /* 0x6E n: */
0x110E, /* 0x6F o: */
0x1111, /* 0x70 p: */
0x11B8, /* 0x71 q: */
0x1167, /* 0x72 r: */
0x11AB, /* 0x73 s: */
0x1165, /* 0x74 t: */
0x1103, /* 0x75 u: */
0x1169, /* 0x76 v: */
0x11BA, /* 0x77 w: */
0x11B7, /* 0x78 x: */
0x1105, /* 0x79 y: */
0x11C2, /* 0x7A z: */
0x007B, /* 0x7B braceleft: */
0x007C, /* 0x7C bar: */
0x007D, /* 0x7D braceright: */
0x007E /* 0x7E asciitilde: */
];
// 3-2011 자판
K3_2011_layout = [
0x11A9, /* 0x21 exclam: jongseong ssang_gieug */
0x0025, /* 0x22 quotedbl: percent sign */
0x11AC, /* 0x23 numbersign: jongseong nieun-jieuj */
0x0024, /* 0x24 dollar */
0x0023, /* 0x25 percent: number sign */
0x0026, /* 0x26 ampersand */
0x1110, /* 0x27 apostrophe: choseong tieuh */
0x0028, /* 0x28 parenleft */
0x0029, /* 0x29 parenright */
0x007E, /* 0x2A asterisk: tilde */
0x002B, /* 0x2B plus */
0x002C, /* 0x2C comma */
0x005B, /* 0x2D minus: left bracket */
0x002E, /* 0x2E period */
0x1169, /* 0x2F slash: jungseong o */
0x110F, /* 0x30 0: choseong kieuk */
0x11C2, /* 0x31 1: jongseong hieuh */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x11B8, /* 0x33 3: jongseong bieub */
0x116D, /* 0x34 4: jungseong yo */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x1168, /* 0x37 7: jungseong ye */
0x1174, /* 0x38 8: jungseong eui */
0x116E, /* 0x39 9: jungseong u */
0x0034, /* 0x3A colon: 4 */
0x1107, /* 0x3B semicolon: choseong bieub */
0x003C, /* 0x3C less */
0x005D, /* 0x3D equal: right bracket */
0x003E, /* 0x3E greater */
0x003F, /* 0x3F question */
0x11B0, /* 0x40 at: jongseong lieul-gieug */
0x11AE, /* 0x41 A: jongseong dieud */
0x0040, /* 0x42 B: commertial at */
0x11BF, /* 0x43 C: jongseong kieuk */
0x11B2, /* 0x44 D: jongseong lieul-bieub */
0x11BD, /* 0x45 E: jongseong jieuj */
0x11B1, /* 0x46 F: jongseong lieul-mieum */
0x0021, /* 0x47 G: exclamation mark */
0x0030, /* 0x48 H: 0 */
0x0037, /* 0x49 I: 7 */
0x0031, /* 0x4A J: 1 */
0x0032, /* 0x4B K: 2 */
0x0033, /* 0x4C L: 3 */
0x0022, /* 0x4D M: quotatioin mark */
0x0027, /* 0x4E N: apostrophe */
0x0038, /* 0x4F O: 8 */
0x0039, /* 0x50 P: 9 */
0x11C1, /* 0x51 Q: jongseong pieup */
0x11B6, /* 0x52 R: jongseong lieul-hieuh */
0x11AD, /* 0x53 S: jongseong nieun-hieuh */
0x1164, /* 0x54 T: jungseong yae */
0x0036, /* 0x55 U: 6 */
0x11AA, /* 0x56 V: jongseong gieug-sieus */
0x11C0, /* 0x57 W: jongseong tieut */
0x11B9, /* 0x58 X: jongseong bieub-sieus */
0x0035, /* 0x59 Y: 5 */
0x11BE, /* 0x5A Z: jongseong chieuch */
0x00B7, /* 0x5B bracketleft: middle dot */
0x003D, /* 0x5C backslash: equals sign */
0x003A, /* 0x5D bracketright: colon */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x002A, /* 0x5F underscore: asterisk */
0x003B, /* 0x60 quoteleft: semicolon */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong giueg */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1165, /* 0x72 r: jungseong eo */
0x11AB, /* 0x73 s: jongseong nieun */
0x1162, /* 0x74 t: jungseong ae */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x002D, /* 0x7B braceleft: minus sign */
0x005C, /* 0x7C bar: backslash */
0x002F, /* 0x7D braceright: slash */
0x005F /* 0x7E asciitilde: underscore */
];
// 3-2012 자판
K3_2012_layout = [
0x0021, /* 0x21 exclam */
0x002F, /* 0x22 quotedbl: slash */
0x0023, /* 0x23 numbersign */
0x0024, /* 0x24 dollar */
0x0025, /* 0x25 percent */
0x0026, /* 0x26 ampersand */
0x1110, /* 0x27 apostrophe: choseong tieuh */
0x0028, /* 0x28 parenleft */
0x0029, /* 0x29 parenright */
0x002A, /* 0x2A asterisk */
0x002B, /* 0x2B plus */
0x002C, /* 0x2C comma */
0x002D, /* 0x2D minus */
0x002E, /* 0x2E period */
0x1169, /* 0x2F slash: jungseong o */
0x110F, /* 0x30 0: choseong kieuk */
0x11C2, /* 0x31 1: jongseong hieuh */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x11B8, /* 0x33 3: jongseong bieub */
0x116D, /* 0x34 4: jungseong yo */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x1168, /* 0x37 7: jungseong ye */
0x1174, /* 0x38 8: jungseong eui */
0x116E, /* 0x39 9: jungseong u */
0x0034, /* 0x3A colon: 4 */
0x1107, /* 0x3B semicolon: choseong bieub */
0x003C, /* 0x3C less */
0x003D, /* 0x3D equal */
0x003E, /* 0x3E greater */
0x003F, /* 0x3F question */
0x0040, /* 0x40 at:commertial at */
0x11AE, /* 0x41 A: jongseong dieud */
0x003B, /* 0x42 B: semicolon */
0x11BF, /* 0x43 C: jongseong kieuk */
0x11B0, /* 0x44 D: jongseong lieul-gieug */
0x11BD, /* 0x45 E: jongseong jieuj */
0x11B1, /* 0x46 F: jongseong lieul-mieum */
0x003A, /* 0x47 G: colon */
0x0030, /* 0x48 H: 0 */
0x0037, /* 0x49 I: 7 */
0x0031, /* 0x4A J: 1 */
0x0032, /* 0x4B K: 2 */
0x0033, /* 0x4C L: 3 */
0x0022, /* 0x4D M: quotatioin mark */
0x0027, /* 0x4E N: apostrophe */
0x0038, /* 0x4F O: 8 */
0x0039, /* 0x50 P: 9 */
0x11C1, /* 0x51 Q: jongseong pieup */
0x11B6, /* 0x52 R: jongseong lieul-hieuh */
0x11AD, /* 0x53 S: jongseong nieun-hieuh */
0x1164, /* 0x54 T: jungseong yae */
0x0036, /* 0x55 U: 6 */
0x11A9, /* 0x56 V: jongseong ssang_gieug */
0x11C0, /* 0x57 W: jongseong tieut */
0x11B9, /* 0x58 X: jongseong bieub-sieus */
0x0035, /* 0x59 Y: 5 */
0x11BE, /* 0x5A Z: jongseong chieuch */
0x005B, /* 0x5B bracketleft */
0x005C, /* 0x5C backslash */
0x005D, /* 0x5D bracketright */
0x005E, /* 0x5E asciicircum */
0x005F, /* 0x5F underscore */
0x0060, /* 0x60 quoteleft */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1165, /* 0x72 r: jungseong eo */
0x11AB, /* 0x73 s: jongseong nieun */
0x1162, /* 0x74 t: jungseong ae */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft */
0x007C, /* 0x7C bar */
0x007D, /* 0x7D braceright */
0x007E /* 0x7E asciitilde */
];
K3_14_proposal_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x1110, /* 0x27 apostrophe: choseong tieut */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x1169, /* 0x2F slash: jungseong o */
0x110F, /* 0x30 0: choseong kieuh */
0x11C2, /* 0x31 1: jongseong hieuh */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x11B8, /* 0x33 3: jongseong bieub */
0x116D, /* 0x34 4: jungseong yo */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x1168, /* 0x37 7: jungseong ye */
0x1174, /* 0x38 8: jungseong eui */
0x116E, /* 0x39 9: jungseong u */
0x003A, /* 0x3A colon: colon */
0x1107, /* 0x3B semicolon: choseong bieub */
0x0032, /* 0x3C less: 2 */
0x003D, /* 0x3D equal: euals sign */
0x0033, /* 0x3E greater: 3 */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x11AE, /* 0x41 A: jongseong dieud */
0x002F, /* 0x42 B: slash */
0x11B5, /* 0x43 C: jongseong lieul-pieup */
0x1164, /* 0x44 D: jungseong yae */
0x11BD, /* 0x45 E: jongseong jieuj */
0x11B4, /* 0x46 F: jongseong lieul-tiuet */
0x11B0, /* 0x47 G: jongseong lieul-gieug */
0x0027, /* 0x48 H: apostrophe */
0x0038, /* 0x49 I: 8 */
0x0034, /* 0x4A J: 4 */
0x0035, /* 0x4B K: 5 */
0x0036, /* 0x4C L: 6 */
0x0031, /* 0x4D M: 1 */
0x0030, /* 0x4E N: 0 */
0x0039, /* 0x4F O: 9 */
0x003B, /* 0x50 P: semicolon */
0x11C1, /* 0x51 Q: jongseong pieup */
0x11AC, /* 0x52 R: jongseong nieun-jieuj */
0x11AD, /* 0x53 S: jongseong nieun-hieuh */
0x003C, /* 0x54 T: less-than sign */
0x0037, /* 0x55 U: 7 */
0x11BF, /* 0x56 V: jongseong kieuk */
0x11C0, /* 0x57 W: jongseong tieut */
0x11B9, /* 0x58 X: jongseong bieub-sieus */
0x003E, /* 0x59 Y: greater-than sign */
0x11BE, /* 0x5A Z: jongseong chieuch */
0x005B, /* 0x5B bracketleft: left bracket */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1162, /* 0x72 r: jungseong ae */
0x11AB, /* 0x73 s: jongseong nieun */
0x1165, /* 0x74 t: jungseong eo */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E, /* 0x7E asciitilde: tilde */
0x0000 /* 0x7F delete */
];
K3_2014_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11C2,0x0000], /* 0x31 1: jongseong hieuh */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11B8,0x0000], /* 0x33 3: jongseong bieub */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1174,0x0000], /* 0x38 8: jungseong eui */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x0034,0x0000], /* 0x3A colon: 4 */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11B9,0x11B9], /* 0x41 A: jongseong bieub-sieus, jongseong bieub-sieuh */
[0x003B,0x0000], /* 0x42 B: semicolon */
[0x11BF,0x11AA], /* 0x43 C: jongseong kieuk, jongseong gieug-sieus */
[0x11AE,0x11B2], /* 0x44 D: jongseong dieud, jongseong lieul-bieub */
[0x11BD,0x11AC], /* 0x45 E: jongseong jieuj, jongseong nieun-jieuj */
[0x11C0,0x11B4], /* 0x46 F: jongseong tieut, jongseong lieul-tieut */
[0x003A,0x0000], /* 0x47 G: colon */
[0x0030,0x0000], /* 0x48 H: 0 */
[0x0037,0x0000], /* 0x49 I: 7 */
[0x0031,0x0000], /* 0x4A J: 1 */
[0x0032,0x0000], /* 0x4B K: 2 */
[0x0033,0x0000], /* 0x4C L: 3 */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x0027,0x0000], /* 0x4E N: apostrophe */
[0x0038,0x0000], /* 0x4F O: 8 */
[0x0039,0x0000], /* 0x50 P: 9 */
[0x11B6,0x11B6], /* 0x51 Q: jongseong lieul-hieuh, jongseong lieul-hieuh */
[0x11BE,0x11B3], /* 0x52 R: jongseong chieuch, jongseong lieul-sieus */
[0x11AD,0x11AD], /* 0x53 S: jongseong nieun-hieuh, jongseong nieun-hieuh */
[0x1164,0x0000], /* 0x54 T: jungseong yae */
[0x0036,0x0000], /* 0x55 U: 6 */
[0x11C1,0x11B5], /* 0x56 V: jongseong pieup, jongseong lieul-pieup */
[0x11B0,0x11B0], /* 0x57 W: jongseong lieul-gieug, jongseong lieul-gieug */
[0x11A9,0x11A9], /* 0x58 X: jongseong ssang_gieug, jongseong ssang_gieug */
[0x0035,0x0000], /* 0x59 Y: 5 */
[0x11B1,0x11B1], /* 0x5A Z: jongseong lieul-mieum, jongseong lieul-mieum */
[0x005B,0x005B], /* 0x5B bracketleft */
[0x005C,0x005C], /* 0x5C backslash */
[0x005D,0x005D], /* 0x5D bracketright */
[0x005E,0x005E], /* 0x5E asciicircum */
[0x005F,0x005F], /* 0x5F underscore */
[0x0060,0x0060], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1165,0x0000], /* 0x72 r: jungseong eo */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1162,0x0000], /* 0x74 t: jungseong ae */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
K3_2015_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11AE,0x0000], /* 0x31 1: jongseong dieud */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11B8,0x0000], /* 0x33 3: jongseong bieub */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1174,0x0000], /* 0x38 8: jungseong eui */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x0034,0x0000], /* 0x3A colon: 4 */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11B4,0x11B4], /* 0x41 A: jongseong lieul-tieut, jongseong lieul-tieut */
[0x003B,0x0000], /* 0x42 B: semicolon */
[0x11C0,0x11B5], /* 0x43 C: jongseong tieut, jongseong lieul-pieup */
[0x11C2,0x11B2], /* 0x44 D: jongseong hieuh, jongseong lieul-bieub */
[0x11BD,0x11AC], /* 0x45 E: jongseong jieuj, jongseong nieun-jieuj */
[0x11C1,0x11B1], /* 0x46 F: jongseong pieup, jongseong lieul-mieum */
[0x003A,0x0000], /* 0x47 G: colon */
[0x0030,0x0000], /* 0x48 H: 0 */
[0x0037,0x0000], /* 0x49 I: 7 */
[0x0031,0x0000], /* 0x4A J: 1 */
[0x0032,0x0000], /* 0x4B K: 2 */
[0x0033,0x0000], /* 0x4C L: 3 */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x0027,0x0000], /* 0x4E N: apostrophe */
[0x0038,0x0000], /* 0x4F O: 8 */
[0x0039,0x0000], /* 0x50 P: 9 */
[0x11A9,0x11A9], /* 0x51 Q: jongseong ssang_gieug, jongseong ssang_gieug */
[0x11BE,0x11B6], /* 0x52 R: jongseong chieuch, jongseong lieul-hieuh */
[0x11AD,0x11AD], /* 0x53 S: jongseong nieun-hieuh, jongseong nieun-hieuh */
[0x1164,0x0000], /* 0x54 T: jungseong yae */
[0x0036,0x0000], /* 0x55 U: 6 */
[0x11BF,0x11AA], /* 0x56 V: jongseong kieuk, jongseong gieug-sieus */
[0x11B0,0x11B0], /* 0x57 W: jongseong lieul-gieug, jongseong lieul-gieug */
[0x11B9,0x11B9], /* 0x58 X: jongseong bieub-sieuh, jongseong bieub-sieuh */
[0x0035,0x0000], /* 0x59 Y: 5 */
[0x11B3,0x11B3], /* 0x5A Z: jongseong lieul-sieus, jongseong lieul-sieus */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1165,0x0000], /* 0x72 r: jungseong eo */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1162,0x0000], /* 0x74 t: jungseong ae */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
K3_2015y_layout = [
0x11F9, /* 0x21 exclam: jongseong yeolin_hieuh */
0x002F, /* 0x22 quotedbl: slash */
0x0023, /* 0x23 numbersign */
0x0024, /* 0x24 dollar */
0x0025, /* 0x25 percent */
0x0026, /* 0x26 ampersand */
0x1110, /* 0x27 apostrophe: choseong tieuh */
0x0028, /* 0x28 parenleft */
0x0029, /* 0x29 parenright */
0x002A, /* 0x2A asterisk */
0x002B, /* 0x2B plus */
0x002C, /* 0x2C comma */
0x002D, /* 0x2D minus */
0x002E, /* 0x2E period */
0x1169, /* 0x2F slash: jungseong o */
0x110F, /* 0x30 0: choseong kieuk */
0x11C2, /* 0x31 1: jongseong hieuh */
0x11BB, /* 0x32 2: jongseong ssang_sieus */
0x11B8, /* 0x33 3: jongseong bieub */
0x116D, /* 0x34 4: jungseong yo */
0x1172, /* 0x35 5: jungseong yu */
0x1163, /* 0x36 6: jungseong ya */
0x1168, /* 0x37 7: jungseong ye */
0x1174, /* 0x38 8: jungseong eui */
0x116E, /* 0x39 9: jungseong u */
0x003A, /* 0x3A colon */
0x1107, /* 0x3B semicolon: choseong bieub */
0x113C, /* 0x3C less: choseong ap_sieus */
0x003D, /* 0x3D equal */
0x113E, /* 0x3E greater: choseong dwis_sieus */
0x003F, /* 0x3F question */
0x0040, /* 0x40 at */
0x11F0, /* 0x41 A: jongseong yes_ieung */
0x0021, /* 0x42 B: exclam */
0x11C0, /* 0x43 C: jongseong tieut */
0x11AE, /* 0x44 D: jongseong dieud */
0x11BD, /* 0x45 E: jongseong jieuj */
0x11C1, /* 0x46 F: jongseong pieup */
0X119E, /* 0x47 G: jungseong alae_a */
0x00B7, /* 0x48 H: middle dot */
0x1154, /* 0x49 I: choseong ap_chieuch */
0x114C, /* 0x4A J: choseong yes_ieung */
0x114E, /* 0x4B K: choseong ap_jieuj */
0x1150, /* 0x4C L: choseong dwis_jieuj */
0x1159, /* 0x4D M: choseong yeolin_hieuh */
0x1140, /* 0x4E N: choseong yeolin_sieus */
0x1155, /* 0x4F O: choseong dwis_chieuch */
0x003B, /* 0x50 P: semicolon */
0x11EB, /* 0x51 Q: jongseong yeolin_sieus */
0x11BE, /* 0x52 R: jongseong chieuch */
0x0000, /* 0x53 S: */
0x1164, /* 0x54 T: jungseong yae */
0x302E, /* 0x55 U: hangeul single dot tone mark */
0x11BF, /* 0x56 V: jongseong kieuk */
0x0000, /* 0x57 W: */
0x0000, /* 0x58 X: */
0X302F, /* 0x59 Y: hangeul double dot tone mark */
0x0000, /* 0x5A Z: */
0x005B, /* 0x5B bracketleft */
0x005C, /* 0x5C backslash */
0x005D, /* 0x5D bracketright */
0x005E, /* 0x5E asciicircum */
0x005F, /* 0x5F underscore */
0x0060, /* 0x60 quoteleft */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x1167, /* 0x65 e: jungseong yeo */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x110E, /* 0x6F o: choseong chieuch */
0x1111, /* 0x70 p: choseong pieup */
0x11BA, /* 0x71 q: jongseong sieus */
0x1165, /* 0x72 r: jungseong eo */
0x11AB, /* 0x73 s: jongseong nieun */
0x1162, /* 0x74 t: jungseong ae */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11AF, /* 0x77 w: jongseong lieul */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x0027, /* 0x7B braceleft: apostrophe */
0x007C, /* 0x7C bar */
0x0022, /* 0x7D braceright: quotatioin mark */
0x007E /* 0x7E asciitilde */
];
K3_2015M_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11AE,0x0000], /* 0x31 1: jongseong dieud */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11B8,0x0000], /* 0x33 3: jongseong bieub */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1174,0x0000], /* 0x38 8: jungseong eui */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x0034,0x0000], /* 0x3A colon: 4 */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11B4,0x11B4], /* 0x41 A: jongseong lieul-tieut, jongseong lieul-tieut*/
[0x003B,0x0000], /* 0x42 B: semicolon */
[0x11C0,0x11B5], /* 0x43 C: jongseong tieut, jongseong lieul-pieup */
[0x11C2,0x11B2], /* 0x44 D: jongseong hieuh, jongseong lieul-bieub */
[0x11BD,0x11AC], /* 0x45 E: jongseong jieuj, jongseong nieun-jieuj */
[0x11C1,0x11B1], /* 0x46 F: jongseong pieup, jongseong lieul-mieum */
[0x003A,0x0000], /* 0x47 G: colon */
[0x0030,0x0000], /* 0x48 H: 0 */
[0x0037,0x0000], /* 0x49 I: 7 */
[0x0031,0x0000], /* 0x4A J: 1 */
[0x0032,0x0000], /* 0x4B K: 2 */
[0x0033,0x0000], /* 0x4C L: 3 */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x0027,0x0000], /* 0x4E N: apostrophe */
[0x0038,0x0000], /* 0x4F O: 8 */
[0x0039,0x0000], /* 0x50 P: 9 */
[0x11A9,0x11A9], /* 0x51 Q: jongseong ssang_gieug, jongseong ssang_gieug */
[0x11BE,0x11B6], /* 0x52 R: jongseong chieuch, jongseong lieul-hieuh */
[0x11AD,0x11AD], /* 0x53 S: jongseong nieun-hieuh, jongseong nieun-hieuh */
[0x1164,0x0000], /* 0x54 T: jungseong yae */
[0x0036,0x0000], /* 0x55 U: 6 */
[0x11BF,0x11AA], /* 0x56 V: jongseong kieuk, jongseong gieug-sieus */
[0x11B0,0x11B0], /* 0x57 W: jongseong lieul-gieug, jongseong lieul-gieug */
[0x11B9,0x11B9], /* 0x58 X: jongseong bieub-sieuh, jongseong bieub-sieuh */
[0x0035,0x0000], /* 0x59 Y: 5 */
[0x11B3,0x11B3], /* 0x5A Z: jongseong lieul-sieus, jongseong lieul-sieus */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1162,0x0000], /* 0x72 r: jungseong ae */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1165,0x0000], /* 0x74 t: jungseong eo */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
K3_2015P_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11AE,0x0000], /* 0x31 1: jongseong dieud */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11B8,0x0000], /* 0x33 3: jongseong bieub */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1174,0x0000], /* 0x38 8: jungseong eui */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x0034,0x0000], /* 0x3A colon: 4 */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11B9,0x11B9], /* 0x41 A: jongseong bieub-sieuh, jongseong bieub-sieuh */
[0x003B,0x0000], /* 0x42 B: semicolon */
[0x11C0,0x11B4], /* 0x43 C: jongseong tieut, jongseong lieul-tieut */
[0x11C2,0x11B2], /* 0x44 D: jongseong hieuh, jongseong lieul-bieub */
[0x11BD,0x11AC], /* 0x45 E: jongseong jieuj, jongseong nieun-jieuj */
[0x11C1,0x11B5], /* 0x46 F: jongseong pieup, jongseong lieul-pieup */
[0x003A,0x0000], /* 0x47 G: colon */
[0x0030,0x0000], /* 0x48 H: 0 */
[0x0037,0x0000], /* 0x49 I: 7 */
[0x0031,0x0000], /* 0x4A J: 1 */
[0x0032,0x0000], /* 0x4B K: 2 */
[0x0033,0x0000], /* 0x4C L: 3 */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x0027,0x0000], /* 0x4E N: apostrophe */
[0x0038,0x0000], /* 0x4F O: 8 */
[0x0039,0x0000], /* 0x50 P: 9 */
[0x11B6,0x11B6], /* 0x51 Q: jongseong lieul-hieuh, jongseong lieul-hieuh */
[0x11BE,0x11B3], /* 0x52 R: jongseong chieuch, jongseong lieul-sieus */
[0x11AD,0x11AD], /* 0x53 S: jongseong nieun-hieuh, jongseong nieun-hieuh */
[0x1164,0x0000], /* 0x54 T: jungseong yae */
[0x0036,0x0000], /* 0x55 U: 6 */
[0x11BF,0x11AA], /* 0x56 V: jongseong kieuk, jongseong gieug-sieus */
[0x11B0,0x11B0], /* 0x57 W: jongseong lieul-gieug, jongseong lieul-gieug */
[0x11A9,0x11A9], /* 0x58 X: jongseong ssang_gieug, jongseong ssang_gieug */
[0x0035,0x0000], /* 0x59 Y: 5 */
[0x11B1,0x11B1], /* 0x5A Z: jongseong lieul-mieum, jongseong lieul-mieum */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong I */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1165,0x0000], /* 0x72 r: jungseong eo */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1162,0x0000], /* 0x74 t: jungseong ae */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 3-P3 자판
K3_P3_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x0022,0x0000], /* 0x22 quotedbl: quotatioin mark */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11BF,0x0000], /* 0x31 1: jongseong kieuk */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11B8,0x0000], /* 0x33 3: jongseong bieub */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1173,0x1173], /* 0x38 8: jungseong eu, jungseong eu */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x0032,0x0000], /* 0x3C less: 2 */
[0x003D,0x0000], /* 0x3D equal */
[0x0033,0x0000], /* 0x3E greater: 3 */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11B9,0x11B9], /* 0x41 A: jongseong bieub-sieuh, jongseong bieub-sieuh */
[0x003E,0x0000], /* 0x42 B: greater */
[0x11AE,0x11AA], /* 0x43 C: jongseong dieud, jongseong gieug-sieus */
[0x11C2,0x11B2], /* 0x44 D: jongseong hieuh, jongseong lieul-bieub */
[0x11C0,0x11B4], /* 0x45 E: jongseong tieut, jongseong lieul-tieut */
[0x11C1,0x11B5], /* 0x46 F: jongseong pieup, jongseong lieul-pieup */
[0x003C,0x0000], /* 0x47 G: less */
[0x0027,0x0000], /* 0x48 H: apostrophe */
[0x0038,0x0000], /* 0x49 I: 8 */
[0x0034,0x0000], /* 0x4A J: 4 */
[0x0035,0x0000], /* 0x4B K: 5 */
[0x0036,0x0000], /* 0x4C L: 6 */
[0x0031,0x0000], /* 0x4D M: 1 */
[0x0030,0x0000], /* 0x4E N: 0 */
[0x0039,0x0000], /* 0x4F O: 9 */
[0x003B,0x0000], /* 0x50 P: semicolon */
[0x11B6,0x11B6], /* 0x51 Q: jongseong lieul-hieuh, jongseong lieul-hieuh */
[0x11BE,0x11B3], /* 0x52 R: jongseong chieuch, jongseong lieul-sieus */
[0x11AD,0x11AD], /* 0x53 S: jongseong nieun-hieuh, jongseong nieun-hieuh */
[0x1164,0x0000], /* 0x54 T: jungseong yae */
[0x0037,0x0000], /* 0x55 U: 7 */
[0x11BD,0x11AC], /* 0x56 V: jongseong jieuj, jongseong nieun-jieuj */
[0x11B0,0x11B0], /* 0x57 W: jongseong lieul-gieug, jongseong lieul-gieug */
[0x11A9,0x11A9], /* 0x58 X: jongseong ssang_gieug, jongseong ssang_gieug */
[0x002F,0x0000], /* 0x59 Y: slash */
[0x11B1,0x11B1], /* 0x5A Z: jongseong lieul-mieum, jongseong lieul-mieum */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1165,0x0000], /* 0x72 r: jungseong eo */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1162,0x0000], /* 0x74 t: jungseong ae */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 3-P2 자판
K3_P2_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11BF,0x0000], /* 0x31 1: jongseong kieuk */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11B8,0x0000], /* 0x33 3: jongseong bieub */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1173,0x1173], /* 0x38 8: jungseong eu, jungseong eu */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x0034,0x0000], /* 0x3A colon: 4 */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11B9,0x11B9], /* 0x41 A: jongseong bieub-sieuh, jongseong bieub-sieuh */
[0x003B,0x0000], /* 0x42 B: semicolon */
[0x11AE,0x11AA], /* 0x43 C: jongseong dieud, jongseong gieug-sieus */
[0x11C2,0x11B2], /* 0x44 D: jongseong hieuh, jongseong lieul-bieub */
[0x11C0,0x11B4], /* 0x45 E: jongseong tieut, jongseong lieul-tieut */
[0x11C1,0x11B5], /* 0x46 F: jongseong pieup, jongseong lieul-pieup */
[0x003A,0x0000], /* 0x47 G: colon */
[0x0030,0x0000], /* 0x48 H: 0 */
[0x0037,0x0000], /* 0x49 I: 7 */
[0x0031,0x0000], /* 0x4A J: 1 */
[0x0032,0x0000], /* 0x4B K: 2 */
[0x0033,0x0000], /* 0x4C L: 3 */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x0027,0x0000], /* 0x4E N: apostrophe */
[0x0038,0x0000], /* 0x4F O: 8 */
[0x0039,0x0000], /* 0x50 P: 9 */
[0x11B6,0x11B6], /* 0x51 Q: jongseong lieul-hieuh, jongseong lieul-hieuh */
[0x11BE,0x11B3], /* 0x52 R: jongseong chieuch, jongseong lieul-sieus */
[0x11AD,0x11AD], /* 0x53 S: jongseong nieun-hieuh, jongseong nieun-hieuh */
[0x1164,0x0000], /* 0x54 T: jungseong yae */
[0x0036,0x0000], /* 0x55 U: 6 */
[0x11BD,0x11AC], /* 0x56 V: jongseong jieuj, jongseong nieun-jieuj */
[0x11B0,0x11B0], /* 0x57 W: jongseong lieul-gieug, jongseong lieul-gieug */
[0x11A9,0x11A9], /* 0x58 X: jongseong ssang_gieug, jongseong ssang_gieug */
[0x0035,0x0000], /* 0x59 Y: 5 */
[0x11B1,0x11B1], /* 0x5A Z: jongseong lieul-mieum, jongseong lieul-mieum */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1165,0x0000], /* 0x72 r: jungseong eo */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1162,0x0000], /* 0x74 t: jungseong ae */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 3-D1 자판 - 기본 배열
K3_D1_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11BF,0x0000], /* 0x31 1: jongseong kieuk */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11C0,0x0000], /* 0x33 3: jongseong tieut */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1174,0x0000], /* 0x38 8: jungseong eui */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x003A,0x0000], /* 0x3A colon: colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x0032,0x0000], /* 0x3C less: 2 */
[0x003D,0x0000], /* 0x3D equal */
[0x0033,0x0000], /* 0x3E greater: 3 */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11B9,0x0000], /* 0x41 A: jongseong bieub-sieuh */
[0x003E,0x0000], /* 0x42 B: greater */
[0x11AE,0x0000], /* 0x43 C: jongseong dieud */
[0x11C2,0x0000], /* 0x44 D: jongseong hieuh */
[0x11B8,0x0000], /* 0x45 E: jongseong bieub */
[0x11BD,0x0000], /* 0x46 F: jongseong jieuj */
[0x11C1,0x0000], /* 0x47 G: jongseong pieup */
[0x0022,0x0000], /* 0x48 H: quotatioin mark */
[0x0038,0x1174], /* 0x49 I: 8, jungseong eui */
[0x0034,0x0000], /* 0x4A J: 4 */
[0x0035,0x0000], /* 0x4B K: 5 */
[0x0036,0x0000], /* 0x4C L: 6 */
[0x0031,0x0000], /* 0x4D M: 1 */
[0x0030,0x0000], /* 0x4E N: 0 */
[0x0039,0x0000], /* 0x4F O: 9 */
[0x003B,0x0000], /* 0x50 P: semicolon */
[0x11B6,0x0000], /* 0x51 Q: jongseong lieul-hieuh */
[0x1164,0x0000], /* 0x52 R: jungseong yae */
[0x11AD,0x0000], /* 0x53 S: jongseong nieun-hieuh */
[0x003C,0x0000], /* 0x54 T: less */
[0x0037,0x0000], /* 0x55 U: 7 */
[0x11BE,0x0000], /* 0x56 V: jongseong chieuch */
[0x11B0,0x0000], /* 0x57 W: jongseong lieul-gieug */
[0x11A9,0x0000], /* 0x58 X: jongseong ssang_gieug */
[0x0027,0x0000], /* 0x59 Y: apostrophe */
[0x11B1,0x0000], /* 0x5A Z: jongseong lieul-mieum */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x00B7,0x0000], /* 0x60 quoteleft: middle dot */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x1168], /* 0x68 h: choseong nieun, jungseong ye */
[0x1106,0x1174], /* 0x69 i: choseong mieum, jungseong eui */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch */
[0x1111,0x116E], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1162,0x0000], /* 0x72 r: jungseong ae */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1165,0x0000], /* 0x74 t: jungseong eo */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x1168], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 3-D1 자판 - 기호 확장 배열 (캡스 록을 켜고 씀)
K3_D1_capslock_layout = [
0x0021, /* 0x21 exclam */
0x002F, /* 0x22 quotedbl */
0x0023, /* 0x23 numbersign */
0x0024, /* 0x24 dollar */
0x0025, /* 0x25 percent */
0x0026, /* 0x26 ampersand */
0x00AE, /* 0x27 apostrophe: registerd sign ® */
0x0028, /* 0x28 parenleft */
0x0029, /* 0x29 parenright */
0x002A, /* 0x2A asterisk */
0x002B, /* 0x2B plus */
0x3001, /* 0x2C comma: ideographic comma 、 */
0x002D, /* 0x2D minus */
0x3002, /* 0x2E period: ideographic full stop 。 */
0x00A9, /* 0x2F slash: copyright sign © */
0x3015, /* 0x30 0: right tortoise shell bracket 〕 */
0x203D, /* 0x31 1: interrobang ‽ */
0xFFE1, /* 0x32 2: fullwidth pound sign £ */
0x20AC, /* 0x33 3: euro sign € */
0xFFE0, /* 0x34 4: fullwidth cent sign ¢ */
0x2030, /* 0x35 5: per mille(per thousand) sign ‰ */
0x00B6, /* 0x36 6: pilcrow sign ¶ */
0x00A7, /* 0x37 7: section sign § */
0x203B, /* 0x38 8: reference mark */
0x3014, /* 0x39 9: left tortoise shell bracket 〔 */
0x003A, /* 0x3A colon */
0x2020, /* 0x3B semicolon: dagger † */
0x0032, /* 0x3C less: 2 */
0x003D, /* 0x3D equal */
0x0033, /* 0x3E greater: 3 */
0x003F, /* 0x3F question */
0x0040, /* 0x40 at */
0x2190, /* 0x41 A: leftwards arrow ← */
0x2198, /* 0x42 B: south-east arrow ↘ */
0x2195, /* 0x43 C: up-down arrow ↕ */
0x2194, /* 0x44 D: ↔ left right arrow */
0x2022, /* 0x45 E: bullet • */
0x2196, /* 0x46 F: north-west arrow ↖ */
0x2197, /* 0x47 G: north-east arrow ↗ */
0x003E, /* 0x48 H: greater-than sign */
0x0038, /* 0x49 I: 8 */
0x0034, /* 0x4A J: 4 */
0x0035, /* 0x4B K: 5 */
0x0036, /* 0x4C L: 6 */
0x0031, /* 0x4D M: 1 */
0x0030, /* 0x4E N: 0 */
0x0039, /* 0x4F O: 9 */
0x003B, /* 0x50 P: semicolon */
0x25B2, /* 0x51 Q: black up-pointing triangle */
0x00F7, /* 0x52 R: division sign ÷ */
0x2192, /* 0x53 S: rightwards arrow → */
0x00B1, /* 0x54 T: plus minus sign ± */
0x0037, /* 0x55 U: 7 */
0x2199, /* 0x56 V: south-west arrow ↙ */
0x25A0, /* 0x57 W: black square ■ */
0x2191, /* 0x58 X: upwards arrow ↑ */
0x003C, /* 0x59 Y: less-than sign */
0x2193, /* 0x5A Z: downwards arrow ↓ */
0x005B, /* 0x5B bracketleft */
0xFFE6, /* 0x5C backslash */
0x005D, /* 0x5D bracketright */
0x005E, /* 0x5E asciicircum */
0x005F, /* 0x5F underscore */
0x00B7, /* 0x60 quoteleft: middle dot */
0x300E, /* 0x61 a: left white corner bracket 『 */
0x2265, /* 0x62 b: greater-than or equal to ≥ */
0x300B, /* 0x63 c: right double angle bracket 》 */
0x300A, /* 0x64 d: left double angle bracket 《 */
0x25CB, /* 0x65 e: white circle ○ */
0x3008, /* 0x66 f: left angle bracket 〈 */
0x2264, /* 0x67 g: less-than or equal to ≤ */
0x0022, /* 0x68 h: quotatioin mark */
0x261E, /* 0x69 i: white right pointing index ☞ */
0x2015, /* 0x6A j: horizontal bar ― */
0x2026, /* 0x6B k: horizontal epllipsis */
0x0060, /* 0x6C l: quoteleft */
0x2122, /* 0x6D m: trademark ™ */
0x3003, /* 0x6E n: ditto mark 〃 */
0x00B0, /* 0x6F o: degree sign ° */
0x2021, /* 0x70 p: double dagger ‡ */
0x25B3, /* 0x71 q: up-pointing triangle △ */
0x00D7, /* 0x72 r: multiplication X × */
0x300C, /* 0x73 s: left corner bracket 「 */
0x2260, /* 0x74 t: not equal to ≠ */
0x261C, /* 0x75 u: white left pointing index ☜ */
0x3009, /* 0x76 v: right angle bracket 〉 */
0x25A1, /* 0x77 w: square □ */
0x300D, /* 0x78 x: right corner bracket 」 */
0x0027, /* 0x79 y: apostrophe */
0x300F, /* 0x7A z: right white corner bracket 』 */
0x007B, /* 0x7B braceleft */
0xFFE5, /* 0x7C bar: fullwidth yen sign ¥ */
0x007D, /* 0x7D braceright */
0x007E /* 0x7E asciitilde */
];
K3_D1_y_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x005B,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x005D,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11BF,0x0000], /* 0x31 1: jongseong kieuk */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11C0,0x0000], /* 0x33 3: jongseong tieut */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1174,0x0000], /* 0x38 8: jungseong eui */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x113C,0x0000], /* 0x3C less: choseong ap_sieus */
[0x003D,0x0000], /* 0x3D equal */
[0x113E,0x0000], /* 0x3E greater: choseong dwis_sieus */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11F0,0x0000], /* 0x41 A: jongseong yes_ieung */
[0x0021,0x0000], /* 0x42 B: greater */
[0x11AE,0x0000], /* 0x43 C: jongseong dieud */
[0x11C2,0x0000], /* 0x44 D: jongseong hieuh */
[0x11B8,0x0000], /* 0x45 E: jongseong bieub */
[0x11BD,0x0000], /* 0x46 F: jongseong jieuj */
[0x11C1,0x0000], /* 0x47 G: jongseong pieup */
[0x2015,0x0000], /* 0x48 H: horizontal bar */
[0x1154,0x0000], /* 0x49 I: choseong ap_chieuch */
[0x114C,0x0000], /* 0x4A J: choseong yes_ieung */
[0x114E,0x0000], /* 0x4B K: choseong ap_jieuj */
[0x1150,0x0000], /* 0x4C L: choseong dwis_jieuj */
[0x1159,0x0000], /* 0x4D M: choseong yeolin_hieuh */
[0x1140,0x0000], /* 0x4E N: choseong yeolin_sieus */
[0x1155,0x0000], /* 0x4F O: choseong dwis_chieuch */
[0x2026,0x0000], /* 0x50 P: horizontal epllipsis */
[0x11EB,0x0000], /* 0x51 Q: jongseong yeolin_sieus */
[0x1164,0x0000], /* 0x52 R: jungseong yae */
[0x11F9,0x0000], /* 0x53 S: jongseong yeolin_hieuh */
[0x119E,0x0000], /* 0x54 T: jungseong alae_a */
[0x302E,0x0000], /* 0x55 U: hangeul single dot tone mark */
[0x11BE,0x0000], /* 0x56 V: jongseong chieuch */
[0x11AF,0x0000], /* 0x57 W: jongseong lieul */
[0x11A8,0x0000], /* 0x58 X: jongseong gieug */
[0x302F,0x0000], /* 0x59 Y: hangeul double dot tone mark */
[0x11B7,0x0000], /* 0x5A Z: jongseong mieum */
[0x300C,0x0000], /* 0x5B bracketleft */
[0x3002,0x0000], /* 0x5C backslash */
[0x300D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x00B7,0x0000], /* 0x60 quoteleft: middle dot */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1162,0x0000], /* 0x72 r: jungseong ae */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1165,0x0000], /* 0x74 t: jungseong eo */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x300E,0x0000], /* 0x7B braceleft */
[0x3001,0x0000], /* 0x7C bar */
[0x300F,0x0000], /* 0x7D braceright */
[0x203B,0x0000] /* 0x7E asciitilde */
];
// 3-D2 자판 - 기본 배열
K3_D2_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11BF,0x0000], /* 0x31 1: jongseong kieuk */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11C0,0x0000], /* 0x33 3: jongseong tieut */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1174,0x0000], /* 0x38 8: jungseong eui */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x0032,0x0000], /* 0x3C less: 2 */
[0x003D,0x0000], /* 0x3D equal */
[0x0033,0x0000], /* 0x3E greater: 3 */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11B9,0x0000], /* 0x41 A: jongseong bieub-sieuh */
[0x003E,0x0000], /* 0x42 B: greater */
[0x11AE,0x0000], /* 0x43 C: jongseong dieud */
[0x11C2,0x0000], /* 0x44 D: jongseong hieuh */
[0x11B8,0x0000], /* 0x45 E: jongseong bieub */
[0x11C1,0x0000], /* 0x46 F: jongseong pieup */
[0x11BD,0x0000], /* 0x47 G: jongseong jieuj */
[0x0022,0x0000], /* 0x48 H: quotatioin mark */
[0x0038,0x0000], /* 0x49 I: 8 */
[0x0034,0x0000], /* 0x4A J: 4 */
[0x0035,0x0000], /* 0x4B K: 5 */
[0x0036,0x0000], /* 0x4C L: 6 */
[0x0031,0x0000], /* 0x4D M: 1 */
[0x0030,0x0000], /* 0x4E N: 0 */
[0x0039,0x0000], /* 0x4F O: 9 */
[0x003B,0x0000], /* 0x50 P: semicolon */
[0x11B6,0x0000], /* 0x51 Q: jongseong lieul-hieuh */
[0x1164,0x0000], /* 0x52 R: jungseong yae */
[0x11AD,0x0000], /* 0x53 S: jongseong nieun-hieuh */
[0x11BE,0x0000], /* 0x54 T: jongseong chieuch */
[0x0037,0x0000], /* 0x55 U: 7 */
[0x003C,0x0000], /* 0x56 V: less */
[0x11B0,0x0000], /* 0x57 W: jongseong lieul-gieug */
[0x11A9,0x0000], /* 0x58 X: jongseong ssang_gieug */
[0x0027,0x0000], /* 0x59 Y: apostrophe */
[0x11B1,0x0000], /* 0x5A Z: jongseong lieul-mieum */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x11A2], /* 0x68 h: choseong nieun, jungseong ssang_alae_a */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1162,0x0000], /* 0x72 r: jungseong ae */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1165,0x0000], /* 0x74 t: jungseong eo */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x119E], /* 0x79 y: choseong lieul, jungseong alae_a */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
K3_D2_capslock_layout = [
0x25BC, /* 0x21 exclam: */
0x00AE, /* 0x22 quotedbl: registerd sign ® */
0x20AC, /* 0x23 numbersign: euro sign € */
0x00A4, /* 0x24 dollar: currency sign ¤ */
0x2031, /* 0x25 percent: per ten thousand sign ‱ */
0x2640, /* 0x26 ampersand: */
0x2026, /* 0x27 apostrophe: horizontal epllipsis */
0x300E, /* 0x28 parenleft: left white corner bracket 『 */
0x300F, /* 0x29 parenright: right white corner bracket 』 */
0x2116, /* 0x2A asterisk: numero sign № */
0x002B, /* 0x2B plus */
0x0032, /* 0x2C comma: 2 */
0x2015, /* 0x2D minus: horizontal bar ― */
0x0033, /* 0x2E period: 3 */
0x002C, /* 0x2F slash: comma */
0x300D, /* 0x30 0: right corner bracket 」 */
0x25BD, /* 0x31 1: down-pointing triangle ▽ */
0x25C7, /* 0x32 2: diamond ◇ */
0xFFE1, /* 0x33 3: fullwidth pound sign £ */
0xFFE0, /* 0x34 4: fullwidth cent sign ¢ */
0x2030, /* 0x35 5: per mille(per thousand) sign ‰ */
0x00B6, /* 0x36 6: pilcrow sign ¶ */
0x00A7, /* 0x37 7: section sign § */
0x203B, /* 0x38 8: reference mark */
0x300C, /* 0x39 9: left corner bracket 「 */
0x02D0, /* 0x3A colon: modifier letter triangular colon ː */
0x002E, /* 0x3B semicolon: period */
0x3001, /* 0x3C less: ideographic comma 、 */
0x003D, /* 0x3D equal */
0x3002, /* 0x3E greater: ideographic full stop 。 */
0x00A9, /* 0x3F question: copyright sign ⓒ */
0x25C6, /* 0x40 at: black diamond ◆ */
0x2196, /* 0x41 A: north-west arrow ↖ */
0x2265, /* 0x42 B: greater-than or equal to ≥ */
0x2195, /* 0x43 C: up-down arrow ↕ */
0x2605, /* 0x44 D: black star ★ */
0x2022, /* 0x45 E: bullet • */
0x300A, /* 0x46 F: left double angle bracket 《 */
0x300B, /* 0x47 G: right double angle bracket 》 */
0x2205, /* 0x48 H: empty set ∅ */
0x261E, /* 0x49 I: white right pointing index ☞ */
0x00B1, /* 0x4A J: plus minus sign ± */
0x2225, /* 0x4B K: parallel to ∥ */
0x221E, /* 0x4C L: infinity ∞ */
0xFFE2, /* 0x4D M: fullwidth not sign */
0x2122, /* 0x4E N: trademark ™ */
0x00B0, /* 0x4F O: degree sign ° */
0x2021, /* 0x50 P: double dagger ‡ */
0x25B2, /* 0x51 Q: black up-pointing triangle */
0x00F7, /* 0x52 R: division sign ÷ */
0x2197, /* 0x53 S: north-east arrow ↗ */
0x2252, /* 0x54 T: approximately equal to ≒ */
0x261C, /* 0x55 U: white left pointing index ☜ */
0x2264, /* 0x56 V: less-than or equal to ≤ */
0x25A0, /* 0x57 W: black square ■ */
0x2198, /* 0x58 X: south-east arrow ↘ */
0x2312, /* 0x59 Y: [A1D2] arc ⌒ */
0x2199, /* 0x5A Z: south-west arrow ↙ */
0x3010, /* 0x5B bracketleft: left black lenticular bracket 【 */
0xFFE6, /* 0x5C backslash: fullwidth won sign ₩ */
0x3011, /* 0x5D bracketright: right black lenticular bracket 】 */
0x2642, /* 0x5E asciicircum: male sign ♂ */
0x002D, /* 0x5F underscore: minus */
0x2713, /* 0x60 quoteleft: check mark ✓ */
0x2190, /* 0x61 a: leftwards arrow ← */
0x003E, /* 0x62 b: greater-than sign */
0x2194, /* 0x63 c: left right arrow ↔ */
0x2606, /* 0x64 d: white start */
0x25CB, /* 0x65 e: white circle ○ */
0x3008, /* 0x66 f: left angle bracket 〈 */
0x3009, /* 0x67 g: right angle bracket 〉 */
0x00B7, /* 0x68 h: middle dot */
0x0038, /* 0x69 i: 8 */
0x0034, /* 0x6A j: 4 */
0x0035, /* 0x6B k: 5 */
0x0036, /* 0x6C l: 6 */
0x0031, /* 0x6D m: 1 */
0x0030, /* 0x6E n: 0 */
0x0039, /* 0x6F o: 9 */
0x2020, /* 0x70 p: dagger † */
0x25B3, /* 0x71 q: up-pointing triangle △ */
0x00D7, /* 0x72 r: multiplication X × */
0x2192, /* 0x73 s: rightwards arrow → */
0x2260, /* 0x74 t: not equal to ≠ */
0x0037, /* 0x75 u: 7 */
0x003C, /* 0x76 v: less-than sign */
0x25A1, /* 0x77 w: square □ */
0x2191, /* 0x78 x: upwards arrow ↑ */
0x3003, /* 0x79 y: ditto mark 〃 */
0x2193, /* 0x7A z: downwards arrow ↓ */
0x3014, /* 0x7B braceleft: left tortoise shell bracket 〔 */
0xFFE5, /* 0x7C bar: fullwidth yen sign ¥ */
0x3015, /* 0x7D braceright: right tortoise shell bracket 〕 */
0x2611 /* 0x7E asciitilde: ballot with check ☑ */
];
K3_D2_y_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x1169,0x1169], /* 0x2F slash: jungseong o, jungseong o */
[0x110F,0x0000], /* 0x30 0: choseong kieuk */
[0x11BF,0x0000], /* 0x31 1: jongseong kieuk */
[0x11BB,0x0000], /* 0x32 2: jongseong ssang_sieus */
[0x11C0,0x0000], /* 0x33 3: jongseong tieut */
[0x116D,0x0000], /* 0x34 4: jungseong yo */
[0x1172,0x0000], /* 0x35 5: jungseong yu */
[0x1163,0x0000], /* 0x36 6: jungseong ya */
[0x1168,0x0000], /* 0x37 7: jungseong ye */
[0x1174,0x0000], /* 0x38 8: jungseong eui */
[0x116E,0x116E], /* 0x39 9: jungseong u, jungseong u */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x113C,0x0000], /* 0x3C less: choseong ap_sieus */
[0x003D,0x0000], /* 0x3D equal */
[0x113E,0x0000], /* 0x3E greater: choseong dwis_sieus */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11F0,0x0000], /* 0x41 A: jongseong yes_ieung */
[0x003E,0x0000], /* 0x42 B: greater-than sign */
[0x11AE,0x0000], /* 0x43 C: jongseong dieud */
[0x11C2,0x0000], /* 0x44 D: jongseong hieuh */
[0x11B8,0x0000], /* 0x45 E: jongseong bieub */
[0x11C1,0x0000], /* 0x46 F: jongseong pieup */
[0x11BD,0x0000], /* 0x47 G: jongseong jieuj */
[0x0022,0x0000], /* 0x48 H: quotation mark */
[0x1154,0x0000], /* 0x49 I: choseong ap_chieuch */
[0x114C,0x0000], /* 0x4A J: choseong yes_ieung */
[0x114E,0x0000], /* 0x4B K: choseong ap_jieuj */
[0x1150,0x0000], /* 0x4C L: choseong dwis_jieuj */
[0x1159,0x0000], /* 0x4D M: choseong yeolin_hieuh */
[0x1140,0x0000], /* 0x4E N: choseong yeolin_sieus */
[0x1155,0x0000], /* 0x4F O: choseong dwis_chieuch */
[0x003B,0x0000], /* 0x50 P: semicolon */
[0x11EB,0x0000], /* 0x51 Q: jongseong yeolin_sieus */
[0x1164,0x0000], /* 0x52 R: jungseong yae */
[0x11F9,0x0000], /* 0x53 S: jongseong yeolin_hieuh */
[0x11BE,0x0000], /* 0x54 T: jongseong chieuch */
[0x302E,0x0000], /* 0x55 U: hangeul single dot tone mark */
[0x003C,0x0000], /* 0x56 V: less-than sign */
[0x11B0,0x0000], /* 0x57 W: jongseong lieul-gieug */
[0x11A8,0x0000], /* 0x58 X: jongseong gieug */
[0x0027,0x0000], /* 0x59 Y: apostrophe */
[0x11B1,0x0000], /* 0x5A Z: jongseong lieul-mieum */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x0000], /* 0x6F o: choseong chieuch */
[0x1111,0x0000], /* 0x70 p: choseong pieup */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x1162,0x0000], /* 0x72 r: jungseong ae */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x1165,0x0000], /* 0x74 t: jungseong eo */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x119E,0x0000], /* 0x78 x: jungseong alae_a */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
K3_18Na_layout = [
[0x0021,0x0000], /* 0x21 exclam: exclamation mark */
[0x0022,0x0000], /* 0x22 quotedbl: quotation mark */
[0x0023,0x0000], /* 0x23 numbersign: number sign */
[0x0024,0x0000], /* 0x24 dollar: dollar sign */
[0x0025,0x0000], /* 0x25 percent: percent sign */
[0x0026,0x0000], /* 0x26 ampersand: ampersand */
[0x0027,0x0000], /* 0x27 apostrophe: apostrophe */
[0x0028,0x0000], /* 0x28 parenleft: left parenthesis */
[0x0029,0x0000], /* 0x29 parenright: right parenthesis */
[0x002A,0x0000], /* 0x2A asterisk: asterisk */
[0x002B,0x0000], /* 0x2B plus: plus sign */
[0x002C,0x0000], /* 0x2C comma: comma */
[0x002D,0x0000], /* 0x2D minus: minus sign */
[0x002E,0x0000], /* 0x2E period: period */
[0x002F,0x0000], /* 0x2F slash: slash */
[0x0030,0x0000], /* 0x30 0: 0 */
[0x0031,0x0000], /* 0x31 1: 1 */
[0x0032,0x0000], /* 0x32 2: 2 */
[0x0033,0x0000], /* 0x33 3: 3 */
[0x0034,0x0000], /* 0x34 4: 4 */
[0x0035,0x0000], /* 0x35 5: 5 */
[0x0036,0x0000], /* 0x36 6: 6 */
[0x0037,0x0000], /* 0x37 7: 7 */
[0x0038,0x0000], /* 0x38 8: 8 */
[0x0039,0x0000], /* 0x39 9: 9 */
[0x003A,0x0000], /* 0x3A colon: colon */
[0x11BD,0x11BE], /* 0x3B semicolon: jongseong jieuj, jongseong chieuch */
[0x003C,0x0000], /* 0x3C less: less-than sign */
[0x003D,0x0000], /* 0x3D equal: equals sign */
[0x003E,0x0000], /* 0x3E greater: greater-than sign */
[0x003F,0x0000], /* 0x3F question: question mark */
[0x0040,0x0000], /* 0x40 at: commercial at */
[0x1106,0x0000], /* 0x41 A: choseong mieum */
[0x11B8,0x11C1], /* 0x42 B: jongseong bieub, jongseong pieup */
[0x110E,0x0000], /* 0x43 C: choseong chieuch */
[0x110B,0x0000], /* 0x44 D: choseong ieung */
[0x1104,0x0000], /* 0x45 E: choseong ssang_dieud */
[0x1105,0x0000], /* 0x46 F: choseong lieul */
[0x1112,0x0000], /* 0x47 G: choseong hieuh */
[0x11AB,0x11C0], /* 0x48 H: jongseong nieun, jongseong tieut */
[0x11BC,0x11C2], /* 0x49 I: jongseong ieung, jongseong hieuh */
[0x0000,0x0000], /* 0x4A J: */
[0x0000,0x0000], /* 0x4B K: */
[0x003B,0x0000], /* 0x4C L: semicolon */
[0x11B7,0x11AE], /* 0x4D M: jongseong mieum, jongseong dieud */
[0x11AF,0x11BF], /* 0x4E N: jongseong rieul, jongseong kieuk */
[0x1164,0x0000], /* 0x4F O: jungseong yae */
[0x1168,0x0000], /* 0x50 P: jungseong ye */
[0x1108,0x0000], /* 0x51 Q: choseong ssang_pieup */
[0x1101,0x0000], /* 0x52 R: choseong ssang_gieug */
[0x1102,0x0000], /* 0x53 S: choseong nieun */
[0x110A,0x0000], /* 0x54 T: choseong ssang_sios */
[0x11BA,0x0000], /* 0x55 U: jongseong sieus */
[0x1111,0x0000], /* 0x56 V: choseong pieup */
[0x110D,0x0000], /* 0x57 W: choseong ssang_jieuj */
[0x1110,0x0000], /* 0x58 X: choseong tieut */
[0x11A8,0x0000], /* 0x59 Y: jongseong gieug */
[0x110F,0x0000], /* 0x5A Z: choseong kieuk */
[0x005B,0x0000], /* 0x5B bracketleft: left bracket */
[0x005C,0x0000], /* 0x5C backslash: backslash */
[0x005D,0x0000], /* 0x5D bracketright: right bracket */
[0x005E,0x0000], /* 0x5E asciicircum: circumflex accent */
[0x005F,0x0000], /* 0x5F underscore: underscore */
[0x0060,0x0000], /* 0x60 quoteleft: grave accent */
[0x1106,0x0000], /* 0x61 a: choseong mieum */
[0x1172,0x0000], /* 0x62 b: jungseong yu */
[0x110E,0x0000], /* 0x63 c: choseong chieuch */
[0x110B,0x0000], /* 0x64 d: choseong ieung */
[0x1103,0x0000], /* 0x65 e: choseong dieud */
[0x1105,0x0000], /* 0x66 f: choseong lieul */
[0x1112,0x0000], /* 0x67 g: choseong hieuh */
[0x1169,0x11AD], /* 0x68 h: jungseong o, jongseong nieun-hieuh */
[0x1163,0x0000], /* 0x69 i: jungseong ya */
[0x1165,0x11B9], /* 0x6A j: jungseong eo, jongseong bieup-sieus */
[0x1161,0x11B0], /* 0x6B k: jungseong a, jongseong lieul-gieug */
[0x1175,0x0000], /* 0x6C l: jungseong i */
[0x1173,0x0000], /* 0x6D m: jungseong eu */
[0x116E,0x0000], /* 0x6E n: jungseong u */
[0x1162,0x0000], /* 0x6F o: jungseong ae */
[0x1166,0x0000], /* 0x70 p: jungseong e */
[0x1107,0x0000], /* 0x71 q: choseong pieup */
[0x1100,0x0000], /* 0x72 r: choseong gieug */
[0x1102,0x0000], /* 0x73 s: choseong nieun */
[0x1109,0x0000], /* 0x74 t: choseong sieus */
[0x1167,0x0000], /* 0x75 u: jungseong yeo */
[0x1111,0x0000], /* 0x76 v: choseong pieup */
[0x110c,0x0000], /* 0x77 w: choseong cieuc */
[0x1110,0x0000], /* 0x78 x: choseong tieut */
[0x116D,0x0000], /* 0x79 y: jungseong yo */
[0x110F,0x0000], /* 0x7A z: choseong kieuk */
[0x007B,0x0000], /* 0x7B braceleft: left brace */
[0x007C,0x0000], /* 0x7C bar: vertical bar */
[0x007D,0x0000], /* 0x7D braceright: right brace */
[0x007E,0x0000] /* 0x7E asciitilde: tilde */
];
K3_Sin3_1995_layout = [
[0x0021,0x0000], /* 0x21 exclam: exclamation mark */
[0x0022,0x0000], /* 0x22 quotedbl: quotatioin mark */
[0x0023,0x0000], /* 0x23 numbersign: number sign */
[0x0024,0x0000], /* 0x24 dollar: dollar sign */
[0x0025,0x0000], /* 0x25 percent: percent sign */
[0x0026,0x0000], /* 0x26 ampersand: ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft: left parenthesis */
[0x0029,0x0000], /* 0x29 parenright: right parenthesis */
[0x002A,0x0000], /* 0x2A asterisk: asterisk */
[0x002B,0x0000], /* 0x2B plus: plus sign */
[0x002C,0x0000], /* 0x2C comma: comma */
[0x002D,0x0000], /* 0x2D minus: minus sign */
[0x002E,0x0000], /* 0x2E period: period */
[0x110F,0x1169], /* 0x2F slash: choseong kieuk, jungseong o */
[0x0030,0x0000], /* 0x30 0: 0 */
[0x0031,0x0000], /* 0x31 1: 1 */
[0x0032,0x0000], /* 0x32 2: 2 */
[0x0033,0x0000], /* 0x33 3: 3 */
[0x0034,0x0000], /* 0x34 4: 4 */
[0x0035,0x0000], /* 0x35 5: 5 */
[0x0036,0x0000], /* 0x36 6: 6 */
[0x0037,0x0000], /* 0x37 7: 7 */
[0x0038,0x0000], /* 0x38 8: 8 */
[0x0039,0x0000], /* 0x39 9: 9 */
[0x003A,0x0000], /* 0x3A colon: colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less: less-than sign */
[0x003D,0x0000], /* 0x3D equal: equals sign */
[0x003E,0x0000], /* 0x3E greater: greater-than sign */
[0x003F,0x0000], /* 0x3F question: question mark */
[0x0040,0x0000], /* 0x40 at: commertial at */
[0x1164,0x0000], /* 0x41 A: jungseong yae */
[0x116E,0x0000], /* 0x42 B: jungseong u */
[0x1166,0x0000], /* 0x43 C: jungseong e */
[0x1175,0x0000], /* 0x44 D: jungseong i */
[0x1167,0x0000], /* 0x45 E: jungseong yeo */
[0x1161,0x0000], /* 0x46 F: jungseong a */
[0x1173,0x0000], /* 0x47 G: jungseong eu */
[0x0000,0x0000], /* 0x48 H: */
[0x116E,0x0000], /* 0x49 I: jungseong o */
[0x003B,0x0000], /* 0x4A J: semicolon */
[0x0027,0x0000], /* 0x4B K: apostrophe */
[0x0000,0x0000], /* 0x4C L: */
[0x002F,0x0000], /* 0x4D M: slash */
[0x0000,0x0000], /* 0x4E N: */
[0x116E,0x0000], /* 0x4F O: jungseong u */
[0x1169,0x0000], /* 0x50 P: jungseong o */
[0x1174,0x0000], /* 0x51 Q: jungseong eui */
[0x1162,0x0000], /* 0x52 R: jungseong ae */
[0x1168,0x0000], /* 0x53 S: jungseong ye */
[0x1165,0x0000], /* 0x54 T: jungseong eo */
[0x0000,0x0000], /* 0x55 U: */
[0x1169,0x0000], /* 0x56 V: jungseong o */
[0x1163,0x0000], /* 0x57 W: jungseong ya */
[0x116D,0x0000], /* 0x58 X: jungseong yo */
[0x0000,0x0000], /* 0x59 Y: */
[0x1172,0x0000], /* 0x5A Z: jungseong yu */
[0x005B,0x0000], /* 0x5B bracketleft: left bracket */
[0x005C,0x0000], /* 0x5C backslash: backslash */
[0x005D,0x0000], /* 0x5D bracketright: right bracket */
[0x005E,0x0000], /* 0x5E asciicircum: circumflex accent */
[0x005F,0x0000], /* 0x5F underscore: underscore */
[0x0060,0x0000], /* 0x60 quoteleft: grave accent */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x11BB,0x0000], /* 0x62 b: jongseong ssang_sieus */
[0x11BE,0x0000], /* 0x63 c: jongseong chieuch */
[0x11C2,0x0000], /* 0x64 d: jongseong hieuh */
[0x11B8,0x0000], /* 0x65 e: jongseong bieub */
[0x11BD,0x0000], /* 0x66 f: jongseong jieuj */
[0x11C1,0x0000], /* 0x67 g: jongseong pieup */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x116E], /* 0x69 i: choseong mieum, jungseong u */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch, jungseong u */
[0x1111,0x1169], /* 0x70 p: choseong pieup, jungseong o */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x11AE,0x0000], /* 0x72 r: jongseong dieud */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x11C0,0x0000], /* 0x74 t: jongseong tieut */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x11BF,0x0000], /* 0x76 v: jongseong kieuk */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft: left brace */
[0x007C,0x0000], /* 0x7C bar: vertical line(bar) */
[0x007D,0x0000], /* 0x7D braceright: right brace */
[0x007E,0x0000] /* 0x7E asciitilde: tilde */
];
// 박경남 신세벌식 자판
K3_Sin3_BGN_layout = [
[0x203B,0x0000], /* 0x21 exclam: reference mark */
[0x00B7,0x0000], /* 0x22 quotedbl: middle dot */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x110F,0x1169], /* 0x2F slash: choseong kieuk, jungseong o */
[0x0030,0x0000], /* 0x30 0 */
[0x0031,0x0000], /* 0x31 1 */
[0x0032,0x0000], /* 0x32 2 */
[0x0033,0x0000], /* 0x33 3 */
[0x0034,0x0000], /* 0x34 4 */
[0x0035,0x0000], /* 0x35 5 */
[0x0036,0x0000], /* 0x36 6 */
[0x0037,0x0000], /* 0x37 7 */
[0x0038,0x0000], /* 0x38 8 */
[0x0039,0x0000], /* 0x39 9 */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x0021,0x0000], /* 0x3F question: exclamation mark */
[0x0040,0x0000], /* 0x40 at: commertial at */
[0x1172,0x0000], /* 0x41 A: jungseong yu */
[0x116E,0x0000], /* 0x42 B: jungseong u */
[0x1166,0x0000], /* 0x43 C: jungseong e */
[0x1175,0x0000], /* 0x44 D: jungseong i */
[0x1167,0x0000], /* 0x45 E: jungseong yeo */
[0x1161,0x0000], /* 0x46 F: jungseong a */
[0x1173,0x0000], /* 0x47 G: jungseong eu */
[0x0000,0x0000], /* 0x48 H: */
[0x1174,0x0000], /* 0x49 I: jungseong eui */
[0x0022,0x0000], /* 0x4A J: quotatioin mark */
[0x003B,0x0000], /* 0x4B K: semicolon */
[0x0027,0x0000], /* 0x4C L: apostrophe */
[0x002F,0x0000], /* 0x4D M: slash */
[0x0000,0x0000], /* 0x4E N: */
[0x116E,0x0000], /* 0x4F O: jungseong u */
[0x1169,0x0000], /* 0x50 P: jungseong o */
[0x1164,0x0000], /* 0x51 Q: jungseong yae */
[0x1162,0x0000], /* 0x52 R: jungseong ae */
[0x1168,0x0000], /* 0x53 S: jungseong ye */
[0x1165,0x0000], /* 0x54 T: jungseong eo */
[0x0000,0x0000], /* 0x55 U: */
[0x1169,0x0000], /* 0x56 V: jungseong o */
[0x1163,0x0000], /* 0x57 W: jungseong ya */
[0x116D,0x0000], /* 0x58 X: jungseong yo */
[0x0000,0x0000], /* 0x59 Y: */
[0x003F,0x0000], /* 0x5A Z: question mark */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x11BF,0x0000], /* 0x62 b: jongseong kieuk */
[0x11BE,0x0000], /* 0x63 c: jongseong chieuch */
[0x11AE,0x0000], /* 0x64 d: jongseong dieud */
[0x11B8,0x0000], /* 0x65 e: jongseong bieub */
[0x11BB,0x0000], /* 0x66 f: jongseong ssang_sieus */
[0x11BD,0x0000], /* 0x67 g: jongseong jieuj */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x1174], /* 0x69 i: choseong mieum, jungseong eui */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch, jungseong u */
[0x1111,0x1169], /* 0x70 p: choseong pieup, jungseong o */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x11C0,0x0000], /* 0x72 r: jongseong tieut */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x11C1,0x0000], /* 0x74 t: jongseong pieup */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x11C2,0x0000], /* 0x76 v: jongseong hieuh */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 박경남 수정 신세벌식 자판 (2003)
K3_Sin3_2003_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x0022,0x0000], /* 0x22 quotedbl */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x110F,0x1169], /* 0x2F slash: choseong kieuk, jungseong o */
[0x0030,0x0000], /* 0x30 0: 0 */
[0x0031,0x0000], /* 0x31 1: 1 */
[0x0032,0x0000], /* 0x32 2: 2 */
[0x0033,0x0000], /* 0x33 3: 3 */
[0x0034,0x0000], /* 0x34 4: 4 */
[0x0035,0x0000], /* 0x35 5: 5 */
[0x0036,0x0000], /* 0x36 6: 6 */
[0x0037,0x0000], /* 0x37 7: 7 */
[0x0038,0x0000], /* 0x38 8: 8 */
[0x0039,0x0000], /* 0x39 9: 9 */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less: */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at:commertial at */
[0x1172,0x11AC], /* 0x41 A: jungseong yu, jongseong nieun-jieuj */
[0x116E,0x0000], /* 0x42 B: jungseong u */
[0x1166,0x11AA], /* 0x43 C: jungseong e, jongseong gieug-sieus */
[0x1175,0x11B9], /* 0x44 D: jungseong i, jongseong bieub-sieus */
[0x1167,0x11B2], /* 0x45 E: jungseong yeo, jongseong lieul-bieub */
[0x1161,0x0000], /* 0x46 F: jungseong a */
[0x1173,0x0000], /* 0x47 G: jungseong eu */
[0x2018,0x0000], /* 0x48 H: left single quoatation mark */
[0x1174,0x0000], /* 0x49 I: jungseong eui */
[0x2019,0x0000], /* 0x4A J: right single quoatation mark */
[0x003B,0x0000], /* 0x4B K: semicolon */
[0x0027,0x0000], /* 0x4C L: apostrophe */
[0x002F,0x0000], /* 0x4D M: slash */
[0x00B7,0x0000], /* 0x4E N: middle dot */
[0x116E,0x0000], /* 0x4F O: jungseong u */
[0x1169,0x0000], /* 0x50 P: jungseong o */
[0x1164,0x11B3], /* 0x51 Q: jungseong yae, jongseong lieul-sieus */
[0x1162,0x11B4], /* 0x52 R: jungseong ae, jongseong lieul-tieut */
[0x1168,0x11AD], /* 0x53 S: jungseong ye, jongseong nieun-hieuh */
[0x1165,0x11B5], /* 0x54 T: jungseong eo, jongseong lieul-pieup */
[0x201D,0x0000], /* 0x55 U: right double quoatation mark */
[0x1169,0x11B6], /* 0x56 V: jungseong o, jongseong lieul-hieuh */
[0x1163,0x11B0], /* 0x57 W: jungseong ya, jongseong lieul-gieug */
[0x116D,0x11A9], /* 0x58 X: jungseong yo, jongseong ssang_gieug */
[0x201C,0x0000], /* 0x59 Y: left single quoatation mark */
[0x203B,0x11B1], /* 0x5A Z: reference mark, jongseong lieul-mieum */
[0x005B,0x119E], /* 0x5B bracketleft, jungseong alae_a */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x11AC], /* 0x61 a: jongseong ieung, jongseong nieun-jieuj */
[0x11BF,0x0000], /* 0x62 b: jongseong kieuk */
[0x11BE,0x11AA], /* 0x63 c: jongseong chieuch, jongseong gieug-sieus */
[0x11AE,0x11B9], /* 0x64 d: jongseong dieud, jongseong bieub-sieus */
[0x11B8,0x11B2], /* 0x65 e: jongseong bieub, jongseong lieul-bieub */
[0x11BB,0x0000], /* 0x66 f: jongseong ssang_sieus */
[0x11BD,0x0000], /* 0x67 g: jongseong jieuj */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x1174], /* 0x69 i: choseong mieum, jungseong eui */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch, jungseong u */
[0x1111,0x1169], /* 0x70 p: choseong pieup, jungseong o */
[0x11BA,0x11B3], /* 0x71 q: jongseong sieus, jongseong lieul-sieus */
[0x11C0,0x11B4], /* 0x72 r: jongseong tieut, jongseong lieul-tieut */
[0x11AB,0x11AD], /* 0x73 s: jongseong nieun, jongseong nieun-hieuh */
[0x11C1,0x11B5], /* 0x74 t: jongseong pieup, jongseong lieul-pieup */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x11C2,0x11B6], /* 0x76 v: jongseong hieuh, jongseong lieul-hieuh */
[0x11AF,0x11B0], /* 0x77 w: jongseong lieul, jongseong lieul-gieug */
[0x11A8,0x11A9], /* 0x78 x: jongseong gieug, jongseong ssang_gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x11B1], /* 0x7A z: jongseong mieum, jongseong lieul-mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
K3_Sin3_2012_layout = [
[0x0021,0x0000], /* 0x21 exclamation */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 number */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand: ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 left parenthesis */
[0x0029,0x0000], /* 0x29 right parenthesis */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x110F,0x1169], /* 0x2F slash: choseong kieuk, jungseong o */
[0x0030,0x0000], /* 0x30 0 */
[0x0031,0x0000], /* 0x31 1 */
[0x0032,0x0000], /* 0x32 2 */
[0x0033,0x0000], /* 0x33 3 */
[0x0034,0x0000], /* 0x34 4 */
[0x0035,0x0000], /* 0x35 5 */
[0x0036,0x0000], /* 0x36 6 */
[0x0037,0x0000], /* 0x37 7 */
[0x0038,0x0000], /* 0x38 8 */
[0x0039,0x0000], /* 0x39 9 */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less-than */
[0x003D,0x0000], /* 0x3D equals */
[0x003E,0x0000], /* 0x3E greater-than */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 commertial at */
[0x1172,0x11B9], /* 0x41 A: jungseong yu, jongseong bieub-sieus */
[0x116E,0x0000], /* 0x42 B: jungseong u */
[0x1166,0x11B6], /* 0x43 C: jungseong e,jongseong lieul-hieuh */
[0x1175,0x11AC], /* 0x44 D: jungseong i, jongseong nieun-jieuj */
[0x1167,0x11B2], /* 0x45 E: jungseong yeo, jongseong lieul-bieub */
[0x1161,0x11AA], /* 0x46 F: jungseong a, jongseong gieug-sieus */
[0x1173,0x0000], /* 0x47 G: jungseong eu */
[0x25A1,0x0000], /* 0x48 H: white squre */
[0x1174,0x0000], /* 0x49 I: jungseong eui */
[0x2015,0x0000], /* 0x4A J: horizontal bar */
[0x00B7,0x0000], /* 0x4B K: middle dot */
[0x003B,0x0000], /* 0x4C L: semicolon */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x0027,0x0000], /* 0x4E N: apostrophe */
[0x116E,0x0000], /* 0x4F O: jungseong u */
[0x1169,0x0000], /* 0x50 P: jungseong o */
[0x1164,0x11B3], /* 0x51 Q: jungseong yae, jongseong lieul-sieus */
[0x1165,0x11B4], /* 0x52 R: jungseong eo, jongseong lieul-tieut */
[0x1168,0x11AD], /* 0x53 S: jungseong ye, jongseong nieun-hieuh */
[0x1162,0x0000], /* 0x54 T: jungseong ae */
[0x25CB,0x0000], /* 0x55 U: white circle */
[0x1169,0x11B5], /* 0x56 V: jungseong o, jongseong lieul-pieup */
[0x1163,0x11B0], /* 0x57 W: jungseong ya, jongseong lieul-gieug */
[0x116D,0x11A9], /* 0x58 X: jungseong yo, jongseong ssang_gieug */
[0x00D7,0x0000], /* 0x59 Y: multiplication*/
[0x119E,0x11B1], /* 0x5A Z: jungseong alae_a, jongseong lieul-mieum*/
[0x005B,0x119E], /* 0x5B left bracket, jungseong alae_a */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D right bracket */
[0x005E,0x0000], /* 0x5E circumflex accent */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft: grave accent */
[0x11BC,0x11B9], /* 0x61 a: jongseong ieung, jongseong bieub-sieus */
[0x11BF,0x0000], /* 0x62 b: jongseong kieuk */
[0x11C2,0x11B6], /* 0x63 c: jongseong hieuh, jongseong lieul-hieuh */
[0x11BB,0x11AC], /* 0x64 d: jongseong ssang_sieus, jongseong nieun-jieuj */
[0x11B8,0x11B2], /* 0x65 e: jongseong bieub, jongseong lieul-bieub */
[0x11BE,0x11AA], /* 0x66 f: jongseong chieuch, jongseong gieug-sieus */
[0x11BD,0x0000], /* 0x67 g: jongseong jieuj */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x1174], /* 0x69 i: choseong mieum, jungseong eui */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch, jungseong u */
[0x1111,0x1169], /* 0x70 p: choseong pieup, jungseong o */
[0x11BA,0x11B3], /* 0x71 q: jongseong sios, jongseong lieul-sieus */
[0x11C0,0x11B4], /* 0x72 r: jongseong tieut, jongseong lieul-tieut */
[0x11AB,0x11AD], /* 0x73 s: jongseong nieun, jongseong nieun-hieuh */
[0x11AE,0x0000], /* 0x74 t: jongseong dieud */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x11C1,0x11B5], /* 0x76 v: jongseong pieup, jongseong lieul-pieup */
[0x11AF,0x11B0], /* 0x77 w: jongseong lieul, jongseong lieul-gieug */
[0x11A8,0x11A9], /* 0x78 x: jongseong gieug, jongseong ssang_gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x11B1], /* 0x7A z: jongseong mieum, jongseong lieul-mieum */
[0x007B,0x0000], /* 0x7B left brace */
[0x007C,0x0000], /* 0x7C vertical line(bar) */
[0x007D,0x0000], /* 0x7D right brace */
[0x007E,0x0000] /* 0x7E tilde */
];
// 신세벌식 2015 자판
K3_Sin3_2015_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x110F,0x1169], /* 0x2F slash: choseong kieuk, jungseong o */
[0x0030,0x0000], /* 0x30 0 */
[0x0031,0x0000], /* 0x31 1 */
[0x0032,0x0000], /* 0x32 2 */
[0x0033,0x0000], /* 0x33 3 */
[0x0034,0x0000], /* 0x34 4 */
[0x0035,0x0000], /* 0x35 5 */
[0x0036,0x0000], /* 0x36 6 */
[0x0037,0x0000], /* 0x37 7 */
[0x0038,0x0000], /* 0x38 8 */
[0x0039,0x0000], /* 0x39 9 */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11BB,0x0000], /* 0x41 A: jongseong ssang_sieus */
[0x11BF,0x0000], /* 0x42 B: jongseong kieuk */
[0x11BA,0x0000], /* 0x43 C: jongseong sieus */
[0x11AF,0x0000], /* 0x44 D: jongseong lieul */
[0x11BC,0x0000], /* 0x45 E: jongseong ieung */
[0x11C0,0x0000], /* 0x46 F: jongseong tieut */
[0x11AE,0x0000], /* 0x47 G: jongseong dieud */
[0x3008,0x0000], /* 0x48 H: left angle bracket 〈 */
[0x2026,0x0000], /* 0x49 I: horizontal ellipsis … */
[0x3009,0x0000], /* 0x4A J: right angle bracket 〉 */
[0x00B7,0x0000], /* 0x4B K: middle dot */
[0x003B,0x0000], /* 0x4C L: semicolon */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x0027,0x0000], /* 0x4E N: apostrophe */
[0x116E,0x0000], /* 0x4F O: jungseong u */
[0x1169,0x0000], /* 0x50 P: jungseong o */
[0x11C2,0x0000], /* 0x51 Q: jongseong hieuh */
[0x11BD,0x0000], /* 0x52 R: jongseong jieuj */
[0x11AB,0x0000], /* 0x53 S: jongseong nieun */
[0x11C1,0x0000], /* 0x54 T: jongseong pieup */
[0x300B,0x0000], /* 0x55 U: right double angle bracket 》 */
[0x11BE,0x0000], /* 0x56 V: jongseong chieuch */
[0x11B7,0x0000], /* 0x57 W: jongseong mieum */
[0x11A8,0x0000], /* 0x58 X: jongseong gieug */
[0x300A,0x0000], /* 0x59 Y: left double angle bracket 《 */
[0x11B8,0x0000], /* 0x5A Z: jongseong bieub */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x1163,0x0000], /* 0x61 a: jungseong ya */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch, jungseong u */
[0x1111,0x1169], /* 0x70 p: choseong pieup, jungseong o */
[0x1164,0x0000], /* 0x71 q: jungseong yae */
[0x1162,0x0000], /* 0x72 r: jungseong ae */
[0x1174,0x0000], /* 0x73 s: jungseong eui */
[0x1165,0x0000], /* 0x74 t: jungseong eo */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x1168,0x0000], /* 0x77 w: jungseong ye */
[0x116D,0x0000], /* 0x78 x: jungseong yo */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x1172,0x0000], /* 0x7A z: jungseong yu */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 신세벌식 M 자판 (up↔down)
K3_Sin3_M_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x110F,0x1169], /* 0x2F slash: choseong kieuk, jungseong o */
[0x0030,0x0000], /* 0x30 0 */
[0x0031,0x0000], /* 0x31 1 */
[0x0032,0x0000], /* 0x32 2 */
[0x0033,0x0000], /* 0x33 3 */
[0x0034,0x0000], /* 0x34 4 */
[0x0035,0x0000], /* 0x35 5 */
[0x0036,0x0000], /* 0x36 6 */
[0x0037,0x0000], /* 0x37 7 */
[0x0038,0x0000], /* 0x38 8 */
[0x0039,0x0000], /* 0x39 9 */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x11BC,0x0000], /* 0x41 A: jongseong ieung */
[0x11BE,0x0000], /* 0x42 B: jongseong chieuch */
[0x11C2,0x0000], /* 0x43 C: jongseong hieuh */
[0x11BB,0x0000], /* 0x44 D: jongseong ssang_sieus */
[0x11B8,0x0000], /* 0x45 E: jongseong bieub */
[0x11C0,0x0000], /* 0x46 F: jongseong tieut */
[0x11AE,0x0000], /* 0x47 G: jongseong dieud */
[0x300A,0x0000], /* 0x48 H: left double angle bracket 《 */
[0x203B,0x0000], /* 0x49 I: reference mark */
[0x300B,0x0000], /* 0x4A J: right double angle bracket 》 */
[0x00B7,0x0000], /* 0x4B K: middle dot */
[0x003B,0x0000], /* 0x4C L: semicolon */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x0027,0x0000], /* 0x4E N: apostrophe */
[0x116E,0x0000], /* 0x4F O: jungseong u */
[0x1169,0x0000], /* 0x50 P: jungseong o */
[0x11BA,0x0000], /* 0x51 Q: jongseong sieus */
[0x11BD,0x0000], /* 0x52 R: jongseong jieuj */
[0x11AB,0x0000], /* 0x53 S: jongseong nieun */
[0x11BF,0x0000], /* 0x54 T: jongseong kieuk */
[0x201D,0x0000], /* 0x55 U: right double quotation mark ” */
[0x11C1,0x0000], /* 0x56 V: jongseong pieup */
[0x11AF,0x0000], /* 0x57 W: jongseong lieul */
[0x11A8,0x0000], /* 0x58 X: jongseong gieug */
[0x201C,0x0000], /* 0x59 Y: left double quotation mark “ */
[0x11B7,0x0000], /* 0x5A Z: jongseong mieum */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x1172,0x0000], /* 0x61 a: jungseong yu */
[0x116E,0x0000], /* 0x62 b: jungseong u */
[0x1166,0x0000], /* 0x63 c: jungseong e */
[0x1175,0x0000], /* 0x64 d: jungseong i */
[0x1167,0x0000], /* 0x65 e: jungseong yeo */
[0x1161,0x0000], /* 0x66 f: jungseong a */
[0x1173,0x0000], /* 0x67 g: jungseong eu */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch, jungseong u */
[0x1111,0x1169], /* 0x70 p: choseong pieup, jungseong o */
[0x1164,0x0000], /* 0x71 q: jungseong yae */
[0x1162,0x0000], /* 0x72 r: jungseong ae */
[0x1174,0x0000], /* 0x73 s: jungseong eui */
[0x1165,0x0000], /* 0x74 t: jungseong eo */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x1169,0x0000], /* 0x76 v: jungseong o */
[0x1163,0x0000], /* 0x77 w: jungseong ya */
[0x116D,0x0000], /* 0x78 x: jungseong yo */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x1168,0x0000], /* 0x7A z: jungseong ye */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 신세벌식 P
K3_Sin3_P_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x002F,0x0000], /* 0x22 quotedbl: slash */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x0000], /* 0x2E period */
[0x110F,0x1169], /* 0x2F slash: choseong kieuk, jungseong o */
[0x0030,0x0000], /* 0x30 0 */
[0x0031,0x0000], /* 0x31 1 */
[0x0032,0x0000], /* 0x32 2 */
[0x0033,0x0000], /* 0x33 3 */
[0x0034,0x0000], /* 0x34 4 */
[0x0035,0x0000], /* 0x35 5 */
[0x0036,0x0000], /* 0x36 6 */
[0x0037,0x0000], /* 0x37 7 */
[0x0038,0x0000], /* 0x38 8 */
[0x0039,0x0000], /* 0x39 9 */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x1172,0x11AA], /* 0x41 A: jungseong yu, jongseong gieug-sieus */
[0x116E,0x0000], /* 0x42 B: jungseong u */
[0x1166,0x11A9], /* 0x43 C: jungseong e, jongseong ssang_gieug */
[0x1175,0x11B6], /* 0x44 D: jungseong i, jongseong lieul-hieuh */
[0x1167,0x11B2], /* 0x45 E: jungseong yeo, jongseong lieul-bieub */
[0x1161,0x11B5], /* 0x46 F: jungseong a, jongseong lieul-pieup */
[0x1173,0x0000], /* 0x47 G: jungseong eu */
[0x25A1,0x0000], /* 0x48 H: white squre */
[0x1173,0x0000], /* 0x49 I: jungseong eu */
[0x0027,0x0000], /* 0x4A J: apostrophe */
[0x00B7,0x0000], /* 0x4B K: middle dot */
[0x003B,0x0000], /* 0x4C L: semicolon */
[0x0022,0x0000], /* 0x4D M: quotatioin mark */
[0x2015,0x0000], /* 0x4E N: horizontal bar */
[0x116E,0x0000], /* 0x4F O: jungseong u */
[0x119E,0x0000], /* 0x50 P: jungseong alae_a */
[0x1164,0x11B3], /* 0x51 Q: jungseong yae, jongseong lieul-sieus */
[0x1165,0x11B4], /* 0x52 R: jungseong eo, jongseong lieul-tieut */
[0x1168,0x11AD], /* 0x53 S: jungseong ye, jongseong nieun-hieuh */
[0x1162,0x0000], /* 0x54 T: jungseong ae */
[0x25CB,0x0000], /* 0x55 U: white circle */
[0x1169,0x11AC], /* 0x56 V: jungseong o, jongseong nieun-jieuj */
[0x1163,0x11B0], /* 0x57 W: jungseong ya, jongseong lieul-gieug */
[0x116D,0x11B9], /* 0x58 X: jungseong yo, jongseong bieub-sieus */
[0x00D7,0x0000], /* 0x59 Y: multiplication */
[0x119E,0x11B1], /* 0x5A Z: jungseong alae_a, jongseong lieul-mieum */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x11BF,0x0000], /* 0x62 b: jongseong kieuk */
[0x11A8,0x0000], /* 0x63 c: jongseong gieug */
[0x11C2,0x0000], /* 0x64 d: jongseong hieuh */
[0x11B8,0x0000], /* 0x65 e: jongseong bieub */
[0x11C1,0x0000], /* 0x66 f: jongseong pieup */
[0x11AE,0x0000], /* 0x67 g: jongseong dieud */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x1173], /* 0x69 i: choseong mieum, jungseong eu */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch, jungseong u */
[0x1111,0x119E], /* 0x70 p: choseong pieup, jungseong alae_a */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x11C0,0x0000], /* 0x72 r: jongseong tieut */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x11BE,0x0000], /* 0x74 t: jongseong chieuch */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x11BD,0x0000], /* 0x76 v: jongseong jieuj */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11BB,0x0000], /* 0x78 x: jongseong ssang_sieus */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 신세벌식 P 옛한글
K3_Sin3_P_y_layout = [];
for(i=0;i<K3_Sin3_P_layout.length;++i) K3_Sin3_P_y_layout.push([K3_Sin3_P_layout[i][0],K3_Sin3_P_layout[i][1]]);
K3_Sin3_P_y_layout[52]=0x302E; /* 0x55 U: hangeul single dot tone mark */
K3_Sin3_P_y_layout[56]=0x302F; /* 0x59 Y: hangeul double dot tone mark */
K3_Sin3_P_y_capslock_layout = [];
for(i=0;i<K3_Sin3_P_y_layout.length;++i) K3_Sin3_P_y_capslock_layout.push([K3_Sin3_P_y_layout[i][0],0]);
K3_Sin3_P_y_capslock_layout[14]=[0x110F,0x0000]; /* 0x2F slash: choseong kieuk */
K3_Sin3_P_y_capslock_layout[40]=[0x0000,0x0000]; /* 0x49 I */
K3_Sin3_P_y_capslock_layout[46]=[0x0000,0x0000]; /* 0x4F O */
K3_Sin3_P_y_capslock_layout[47]=[0x0000,0x0000]; /* 0x50 P */
// 신세벌식 공동개발안
K3_Sin3_Gongdong_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x0022,0x0000], /* 0x22 quotedbl */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x1110,0x0000], /* 0x27 apostrophe: choseong tieuh */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x116F], /* 0x2C comma, jungseong weo */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x116A], /* 0x2E period, jungseong wa */
[0x110F,0x1169], /* 0x2F slash: choseong kieuk, jungseong o */
[0x0030,0x0000], /* 0x30 0 */
[0x0031,0x0000], /* 0x31 1 */
[0x0032,0x1171], /* 0x32 2, jungseong wi*/
[0x0033,0x116C], /* 0x33 3, jungseong oe*/
[0x0034,0x0000], /* 0x34 4 */
[0x0035,0x0000], /* 0x35 5 */
[0x0036,0x0000], /* 0x36 6 */
[0x0037,0x0000], /* 0x37 7 */
[0x0038,0x116B], /* 0x38 8, jungseong wae */
[0x0039,0x1170], /* 0x39 9, jungseong we */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x1172,0x0000], /* 0x41 A: jungseong yu */
[0x116E,0x0000], /* 0x42 B: jungseong u */
[0x1166,0x0000], /* 0x43 C: jungseong e */
[0x1175,0x0000], /* 0x44 D: jungseong i */
[0x1167,0x0000], /* 0x45 E: jungseong yeo */
[0x1161,0x0000], /* 0x46 F: jungseong a */
[0x1173,0x0000], /* 0x47 G: jungseong eu */
[0x300E,0x0000], /* 0x48 H: left white corner bracket */
[0x1174,0x0000], /* 0x49 I: jungseong eui */
[0x300F,0x0000], /* 0x4A J: right white corner bracket */
[0x003B,0x0000], /* 0x4B K: semicolon */
[0x0027,0x0000], /* 0x4C L: apostrophe */
[0x002F,0x0000], /* 0x4D M: slash */
[0x00B7,0x0000], /* 0x4E N: middle dot */
[0x116E,0x0000], /* 0x4F O: jungseong u */
[0x1169,0x0000], /* 0x50 P: jungseong o */
[0x203B,0x0000], /* 0x51 Q: reference mark */
[0x1162,0x0000], /* 0x52 R: jungseong ae */
[0x116D,0x0000], /* 0x53 S: jungseong yo */
[0x1165,0x0000], /* 0x54 T: jungseong eo */
[0x300B,0x0000], /* 0x55 U: right double angle bracket */
[0x1169,0x0000], /* 0x56 V: jungseong o */
[0x1168,0x0000], /* 0x57 W: jungseong ye */
[0x1163,0x0000], /* 0x58 X: jungseong ya */
[0x300A,0x0000], /* 0x59 Y: left single angle bracket */
[0x1164,0x0000], /* 0x5A Z: jungseong yae */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung */
[0x11BE,0x0000], /* 0x62 b: jongseong chieuch */
[0x11C2,0x0000], /* 0x63 c: jongseong hieuh */
[0x11BB,0x0000], /* 0x64 d: jongseong ssang_sieus */
[0x11B8,0x0000], /* 0x65 e: jongseong bieub */
[0x11C0,0x0000], /* 0x66 f: jongseong tieut */
[0x11AE,0x0000], /* 0x67 g: jongseong dieud */
[0x1102,0x0000], /* 0x68 h: choseong nieun */
[0x1106,0x1174], /* 0x69 i: choseong mieum, jungseong eui */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x110C,0x0000], /* 0x6C l: choseong jieuj */
[0x1112,0x0000], /* 0x6D m: choseong hieuh */
[0x1109,0x0000], /* 0x6E n: choseong sieus */
[0x110E,0x116E], /* 0x6F o: choseong chieuch, jungseong u */
[0x1111,0x1169], /* 0x70 p: choseong pieup, jungseong o */
[0x11BA,0x0000], /* 0x71 q: jongseong sieus */
[0x11BD,0x0000], /* 0x72 r: jongseong jieuj */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun */
[0x11BF,0x0000], /* 0x74 t: jongseong kieuk */
[0x1103,0x0000], /* 0x75 u: choseong dieud */
[0x11C1,0x0000], /* 0x76 v: jongseong pieup */
[0x11AF,0x0000], /* 0x77 w: jongseong lieul */
[0x11A8,0x0000], /* 0x78 x: jongseong gieug */
[0x1105,0x0000], /* 0x79 y: choseong lieul */
[0x11B7,0x0000], /* 0x7A z: jongseong mieum */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
K3_Sin3_Cham_layout = [
[0x0021,0x0000], /* 0x21 exclam */
[0x0022,0x0000], /* 0x22 quotedbl */
[0x0023,0x0000], /* 0x23 numbersign */
[0x0024,0x0000], /* 0x24 dollar */
[0x0025,0x0000], /* 0x25 percent */
[0x0026,0x0000], /* 0x26 ampersand */
[0x0027,0x0000], /* 0x27 apostrophe: apostrophe */
[0x0028,0x0000], /* 0x28 parenleft */
[0x0029,0x0000], /* 0x29 parenright */
[0x002A,0x0000], /* 0x2A asterisk */
[0x002B,0x0000], /* 0x2B plus */
[0x002C,0x0000], /* 0x2C comma */
[0x002D,0x0000], /* 0x2D minus */
[0x002E,0x116E], /* 0x2E period, jungseong u */
[0x1111,0x0000], /* 0x2F slash: choseong pieup */
[0x0030,0x0000], /* 0x30 0 */
[0x0031,0x0000], /* 0x31 1 */
[0x0032,0x0000], /* 0x32 2 */
[0x0033,0x0000], /* 0x33 3 */
[0x0034,0x0000], /* 0x34 4 */
[0x0035,0x0000], /* 0x35 5 */
[0x0036,0x0000], /* 0x36 6 */
[0x0037,0x0000], /* 0x37 7 */
[0x0038,0x0000], /* 0x38 8 */
[0x0039,0x0000], /* 0x39 9 */
[0x003A,0x0000], /* 0x3A colon */
[0x1107,0x0000], /* 0x3B semicolon: choseong bieub */
[0x003C,0x0000], /* 0x3C less */
[0x003D,0x0000], /* 0x3D equal */
[0x003E,0x0000], /* 0x3E greater */
[0x003F,0x0000], /* 0x3F question */
[0x0040,0x0000], /* 0x40 at */
[0x2018,0x0000], /* 0x41 A: left single quotation mark ‘ */
[0x25B3,0x11BF], /* 0x42 B: white up-pointing triangle △, jongseong kieuk */
[0x300A,0x0000], /* 0x43 C: left double angle bracket 《 */
[0x201C,0x0000], /* 0x44 D: left double quotation mark “ */
[0x300E,0x0000], /* 0x45 E: left white corner bracket 『 */
[0x201D,0x0000], /* 0x46 F: right double quotation mark ” */
[0x25CB,0x0000], /* 0x47 G: white circle */
[0x2A09,0x0000], /* 0x48 H: n-ary times operator */
[0x2192,0x0000], /* 0x49 I: rightwards arrow → */
[0x00B7,0x0000], /* 0x4A J: middle dot */
[0x2026,0x0000], /* 0x4B K: semicolon: horizontal epllipsis */
[0x002F,0x0000], /* 0x4C L: slash */
[0x119E,0x0000], /* 0x4D M: jungseong alae_a */
[0x001B,0x0000], /* 0x4E N: escape */
[0x2014,0x0000], /* 0x4F O: em dash — */
[0x003B,0x0000], /* 0x50 P: semicolon */
[0x300C,0x0000], /* 0x51 Q: left corner bracket 「 */
[0x300F,0x0000], /* 0x52 R: right white corner bracket 』 */
[0x2019,0x0000], /* 0x53 S: right single quotation mark ’ */
[0x25A1,0x0000], /* 0x54 T: square □ */
[0x2190,0x0000], /* 0x55 U: leftwards arrow ← */
[0x300B,0x0000], /* 0x56 V: right double angle bracket 》 */
[0x300D,0x0000], /* 0x57 W: reference mark */
[0x3009,0x0000], /* 0x58 X: right angle bracket 〉 */
[0x203B,0x0000], /* 0x59 Y: reference mark ※ */
[0x3008,0x0000], /* 0x5A Z: left angle bracket 〈 */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x005E,0x0000], /* 0x5E asciicircum */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x116D,0x11B7], /* 0x61 a: jungseong yo, jongseong mieum */
[0x1163,0x110F], /* 0x62 b: jungseong ya, choseong kieuk */
[0x1173,0x11C2], /* 0x63 c: jungseong eu, jongseong hieuh */
[0x1175,0x11BC], /* 0x64 d: jungseong i, jongseong ieung */
[0x1166,0x11A8], /* 0x65 e: jungseong e, jongseong gieug */
[0x1161,0x11AE], /* 0x66 f: jungseong a, jongseong dieud */
[0x1169,0x11C1], /* 0x67 g: jungseong o, jongseong pieup */
[0x1112,0x0000], /* 0x68 h: choseong dieud */
[0x1103,0x0000], /* 0x69 i: choseong mieum */
[0x110B,0x0000], /* 0x6A j: choseong ieung */
[0x1100,0x0000], /* 0x6B k: choseong gieug */
[0x1109,0x0000], /* 0x6C l: choseong sieus */
[0x1102,0x0000], /* 0x6D m: choseong nieun */
[0x110C,0x0000], /* 0x6E n: choseong jieuj */
[0x1105,0x1169], /* 0x6F o: choseong lieul, jungseong o */
[0x1110,0x0000], /* 0x70 p: choseong tieut */
[0x1172,0x11BB], /* 0x71 q: jungseong yu, jongseong ssang_sieus */
[0x116E,0x11C0], /* 0x72 r: jungseong u, jongseong tieut */
[0x1162,0x11AB], /* 0x73 s: jungseong ae, jongseong nieun */
[0x1167,0x11BE], /* 0x74 t: jungseong yeo, jongseong chieuch */
[0x1106,0x0000], /* 0x75 u: choseong hieuh */
[0x1165,0x11BD], /* 0x76 v: jungseong eo, jongseong jieuj */
[0x1174,0x11AF], /* 0x77 w: jungseong eui, jongseong lieul */
[0x1164,0x11BA], /* 0x78 x: jungseong yae, jongseong sieus */
[0x110E,0x0000], /* 0x79 y: choseong chieuch */
[0x1168,0x11B8], /* 0x7A z: jungseong ye, jongseong bieub */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 이건구 한 손 세벌식 오른손
K3_LGG_OH_r = [
[0x2026,0x0000], /* 0x21 exclam: horizontal ellipsis …*/
[0x0022,0x0000], /* 0x22 quotedbl: quotation mark " */
[0x25CB,0x0000], /* 0x23 numbersign: white circle ○ */
[0x25A1,0x0000], /* 0x24 dollar: square □ */
[0x1163,0x0000], /* 0x25 percent: jungseong ya ㅑ */
[0x1168,0x0000], /* 0x26 ampersand: jungseong ye ㅖ */
[0x0027,0x0000], /* 0x27 apostrophe */
[0x002A,0x0000], /* 0x28 parenleft: asterisk * */
[0xFFE6,0x0000], /* 0x29 parenright: full-width won sign ₩ */
[0x0026,0x0000], /* 0x2A asterisk: ampersand & */
[0x002B,0x0000], /* 0x2B plus */
[0x0030,0x0000], /* 0x2C comma: 0 */
[0x002D,0x0000], /* 0x2D minus */
[0x003B,0x0000], /* 0x2E period: semicolon ; */
[0x002F,0x0000], /* 0x2F slash */
[0x0039,0x0000], /* 0x30 0: 9 */
[0x2015,0x0000], /* 0x31 1: horizontal bar ― */
[0x203B,0x0000], /* 0x32 2: reference mark ※ */
[0x0028,0x0000], /* 0x33 3: left parenthesis ( */
[0x0029,0x0000], /* 0x34 4: right parenthesis ) */
[0x1163,0x0000], /* 0x35 5: jungseong ya ㅑ */
[0x11BA,0x0000], /* 0x36 6: jongseong sios _ㅅ */
[0x11B8,0x0000], /* 0x37 7: jongseong bieub _ㅂ */
[0x0037,0x0000], /* 0x38 8: 7 */
[0x0038,0x0000], /* 0x39 9: 8 */
[0x0023,0x0000], /* 0x3A colon: number sign # */
[0x0033,0x0000], /* 0x3B semicolon: 3 */
[0x00B7,0x0000], /* 0x3C less: middle dot · */
[0x003D,0x0000], /* 0x3D equal */
[0x003A,0x0000], /* 0x3E greater: colon : */
[0x003F,0x0000], /* 0x3F question */
[0x00D7,0x0000], /* 0x40 at: multiplication sign × */
[0x300E,0x0000], /* 0x41 A: left white corner bracket『 */
[0x1169,0x0000], /* 0x42 B: jungseong o ㅗ */
[0x1172,0x0000], /* 0x43 C: jungseong yu ㅠ */
[0x1100,0x0000], /* 0x44 D: choseong gieug ㄱ */
[0x116E,0x0000], /* 0x45 E: jungseong u (index finger) ㅜ */
[0x110B,0x0000], /* 0x46 F: choseong ieung ㅇ */
[0x1161,0x0000], /* 0x47 G: jungseong a ㅏ */
[0x1175,0x0000], /* 0x48 H: jungseong i ㅣ */
[0x0024,0x0000], /* 0x49 I: dollar sign $ */
[0x1173,0x0000], /* 0x4A J: jungseong eu ㅡ */
[0x0021,0x0000], /* 0x4B K: exclamation mark ! */
[0x0040,0x0000], /* 0x4C L: at sign @ */
[0x116E,0x0000], /* 0x4D M: jungseong u ㅜ */
[0x1162,0x0000], /* 0x4E N: jungseong ae ㅐ */
[0x0025,0x0000], /* 0x4F O: percent sign % */
[0x005E,0x0000], /* 0x50 P: circumflex accent mark ^ */
[0x300A,0x0000], /* 0x51 Q: left double angle bracket 《 */
[0x1169,0x0000], /* 0x52 R: jungseong o (index finger) ㅗ */
[0x003C,0x0000], /* 0x53 S: less-than sign < */
[0x1165,0x0000], /* 0x54 T: jungseong eo ㅓ */
[0x1166,0x0000], /* 0x55 U: jungseong e ㅔ */
[0x116D,0x0000], /* 0x56 V: jungseong yo ㅛ */
[0x300B,0x0000], /* 0x57 W: right double angle bracket 》 */
[0x003E,0x0000], /* 0x58 X: greater-than sign > */
[0x1167,0x0000], /* 0x59 Y: jungseong yeo ㅕ */
[0x300F,0x0000], /* 0x5A Z: right white corner bracket 』 */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x1174,0x0000], /* 0x5E asciicircum: jungseong eui ㅢ */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft: grave accent mark ` */
[0x300C,0x0000], /* 0x61 a: left corner bracket 「 */
[0x1106,0x0000], /* 0x62 b: choseong mieum ㅁ */
[0x1102,0x0000], /* 0x63 c: choseong nieun ㄴ */
[0x1100,0x0000], /* 0x64 d: choseong gieug ㄱ */
[0x110C,0x116E], /* 0x65 e: choseong jieuj ㅈ, jungseong u (index finger) ㅜ */
[0x110B,0x0000], /* 0x66 f: choseong ieung ㅇ */
[0x1107,0x0000], /* 0x67 g: choseong bieub ㅂ */
[0x11AB,0x0000], /* 0x68 h: jongseong nieun _ㄴ */
[0x0034,0x0000], /* 0x69 i: 4 */
[0x11BC,0x0000], /* 0x6A j: jongseong ieung _ㅇ */
[0x0031,0x0000], /* 0x6B k: 1 */
[0x0032,0x0000], /* 0x6C l: 2 */
[0x11BB,0x0000], /* 0x6D m: jongseong ssang-sios _ㅆ */
[0x11B7,0x0000], /* 0x6E n: jongseong mieum _ㅁ */
[0x0035,0x0000], /* 0x6F o: 5 */
[0x0036,0x0000], /* 0x70 p: 6 */
[0x3008,0x0000], /* 0x71 q: left angle bracket 〈 */
[0x1109,0x1169], /* 0x72 r: choseong sios ㅅ, jungseong o (index finger) ㅗ */
[0x002C,0x0000], /* 0x73 s: comma */
[0x1105,0x0000], /* 0x74 t: choseong rieul ㄹ */
[0x11A8,0x0000], /* 0x75 u: jongseong gieug _ㄱ */
[0x1103,0x0000], /* 0x76 v: choseong digeud ㄷ */
[0x3009,0x0000], /* 0x77 w: right angle bracket 〉 */
[0x002E,0x0000], /* 0x78 x: period . */
[0x11AF,0x0000], /* 0x79 y: jongseong rieul _ㄹ */
[0x300D,0x0000], /* 0x7A z: right corner bracket 」 */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 이건구 한 손 세벌식 오른손의 캡스락 배열
K3_LGG_OH_r_capslock_layout = [
[0x2015,0x0000], /* 0x21 exclam: horizontal bar ― */
[0x0027,0x0000], /* 0x22 quotedbl: apostrophe ' */
[0x0028,0x0000], /* 0x23 numbersign: left parenthesis ( */
[0x0029,0x0000], /* 0x24 dollar: right parenthesis ) */
[0x1163,0x0000], /* 0x25 percent: jungseong ya ㅑ */
[0x11B8,0x0000], /* 0x26 ampersand: jongseong bieub _ㅂ */
[0x0022,0x0000], /* 0x27 apostrophe: quotation mark " */
[0x0038,0x0000], /* 0x28 parenleft: 8 */
[0x0039,0x0000], /* 0x29 parenright: 9 */
[0x0037,0x0000], /* 0x2A asterisk: 7 */
[0x003D,0x0000], /* 0x2B plus: equal sign = */
[0x00B7,0x0000], /* 0x2C comma: middle dot · */
[0x005F,0x0000], /* 0x2D minus: underscore sign _ */
[0x003A,0x0000], /* 0x2E period: colon : */
[0x003F,0x0000], /* 0x2F slash: question mark ? */
[0xFFE6,0x0000], /* 0x30 0: full-width won sign */
[0x2026,0x0000], /* 0x31 1: horizontal ellipsis … */
[0x00D7,0x0000], /* 0x32 2: multiplication sign × */
[0x25CB,0x0000], /* 0x33 3: white circle ○ */
[0x25A1,0x0000], /* 0x34 4: square □ */
[0x1163,0x0000], /* 0x35 5: jungseong ya ㅑ */
[0x1174,0x0000], /* 0x36 6: jungseong eui ㅢ */
[0x1168,0x0000], /* 0x37 7: jungseong ye ㅖ */
[0x0026,0x0000], /* 0x38 8: ampersand & */
[0x002A,0x0000], /* 0x39 9: asterisk * */
[0x0033,0x0000], /* 0x3A colon: 3 */
[0x0023,0x0000], /* 0x3B semicolon: number sign # */
[0x0030,0x0000], /* 0x3C less: 0 */
[0x002B,0x0000], /* 0x3D equal: plus sign + */
[0x003B,0x0000], /* 0x3E greater: semicolon ; */
[0x002F,0x0000], /* 0x3F question: slash / */
[0x203B,0x0000], /* 0x40 at: reference mark ※ */
[0x300C,0x0000], /* 0x61 A: left corner bracket「 */
[0x1106,0x0000], /* 0x62 B: choseong mieum ㅁ */
[0x1102,0x0000], /* 0x63 C: choseong nieun ㄴ */
[0x1100,0x0000], /* 0x64 D: choseong gieug ㄱ */
[0x110C,0x0000], /* 0x65 E: choseong jieuj ㅈ */
[0x110B,0x0000], /* 0x66 F: choseong ieung ㅇ */
[0x1107,0x0000], /* 0x67 G: choseong bieub ㅂ */
[0x11AB,0x0000], /* 0x68 H: jongseong nieun _ㄴ */
[0x0034,0x0000], /* 0x69 I: 4 */
[0x11BC,0x0000], /* 0x6A J: jongseong ieung _ㅇ */
[0x0031,0x0000], /* 0x6B K: 1 */
[0x0032,0x0000], /* 0x6C L: 2 */
[0x11BB,0x0000], /* 0x6D M: jongseong ssang-sios _ㅆ */
[0x11B7,0x0000], /* 0x6E N: jongseong mieum _ㅁ */
[0x0035,0x0000], /* 0x6F O: 5 */
[0x0036,0x0000], /* 0x70 P: 6 */
[0x3008,0x0000], /* 0x71 Q: left angle bracket 〈 */
[0x1109,0x0000], /* 0x72 R: choseong sios ㅅ */
[0x002C,0x0000], /* 0x73 S: comma */
[0x1105,0x0000], /* 0x74 T: choseong rieul ㄹ */
[0x11A8,0x0000], /* 0x75 U: jongseong gieug _ㄱ */
[0x1103,0x0000], /* 0x76 V: choseong digeud ㄷ */
[0x3009,0x0000], /* 0x77 W: right angle bracket 〉 */
[0x002E,0x0000], /* 0x78 X: period . */
[0x11AF,0x0000], /* 0x79 Y: jongseong rieul _ㄹ */
[0x300D,0x0000], /* 0x7A Z: right corner bracket 」 */
[0x007B,0x0000], /* 0x5B bracketleft: left brace { */
[0x007C,0x0000], /* 0x5C backslash: pipe | */
[0x007D,0x0000], /* 0x5D bracketright: right brace } */
[0x11BA,0x0000], /* 0x5E asciicircum: jongseong sios _ㅅ */
[0x002D,0x0000], /* 0x5F underscore: minus sign - */
[0x007E,0x0000], /* 0x60 quoteleft: tilde ~ */
[0x300E,0x0000], /* 0x61 a: left white corner bracket 『 */
[0x1169,0x0000], /* 0x62 b: jungseong o ㅗ */
[0x1172,0x0000], /* 0x63 c: jungseong yu ㅠ */
[0x1100,0x0000], /* 0x64 d: choseong gieug ㄱ */
[0x116E,0x0000], /* 0x65 e: jungseong u ㅜ(index finger) */
[0x110B,0x0000], /* 0x66 f: choseong ieung ㅇ */
[0x1161,0x0000], /* 0x67 g: jungseong a ㅏ */
[0x1175,0x0000], /* 0x68 h: jungseong i ㅣ */
[0x0024,0x0000], /* 0x69 i: dollar sign $ */
[0x1173,0x0000], /* 0x6A j: jungseong eu ㅡ */
[0x0021,0x0000], /* 0x6B k: exclamation mark ! */
[0x0040,0x0000], /* 0x6C l: at sign @ */
[0x116E,0x0000], /* 0x6D m: jungseong u ㅜ */
[0x1162,0x0000], /* 0x6E n: jungseong ae ㅐ */
[0x0025,0x0000], /* 0x6F o: percent sign % */
[0x005E,0x0000], /* 0x70 p: circumflex accent mark ^ */
[0x300A,0x0000], /* 0x71 q: left double angle bracket 《 */
[0x1169,0x0000], /* 0x72 r: jungseong o ㅗ(index finger) */
[0x003C,0x0000], /* 0x73 s: less-than sign < */
[0x1165,0x0000], /* 0x74 t: jungseong eo ㅓ */
[0x1166,0x0000], /* 0x75 u: jungseong e ㅔ */
[0x116D,0x0000], /* 0x76 v: jungseong yo ㅛ */
[0x300B,0x0000], /* 0x77 w: right double angle bracket 》 */
[0x003E,0x0000], /* 0x78 x: greater-than sign > */
[0x1167,0x0000], /* 0x79 y: jungseong yeo ㅕ */
[0x300F,0x0000], /* 0x7A z: right white corner bracket 』 */
[0x005B,0x0000], /* 0x7B braceleft: left bracket [ */
[0x005C,0x0000], /* 0x7C bar: backslash \ */
[0x005D,0x0000], /* 0x7D braceright: right bracket ] */
[0x0060,0x0000] /* 0x7E asciitilde: grave accent mark ` */
]
// 이건구 한 손 세벌식 왼손
K3_LGG_OH_l = [
[0x0026,0x0000], /* 0x21 exclam: ampersand & */
[0x0022,0x0000], /* 0x22 quotedbl: quotation mark ? */
[0xFFE6,0x0000], /* 0x23 numbersign: full-width won sign ₩ */
[0x1168,0x0000], /* 0x24 dollar: jungseong ye ㅖ */
[0x1174,0x0000], /* 0x25 percent: jungseong eui ㅢ */
[0x25CB,0x0000], /* 0x26 ampersand: white circle ○ */
[0x0027,0x0000], /* 0x27 apostrophe */
[0x00D7,0x0000], /* 0x28 parenleft: multiplication sign × */
[0x2026,0x0000], /* 0x29 parenright: horizontal allipsis … */
[0x25A1,0x0000], /* 0x2A asterisk: square □ */
[0x002B,0x0000], /* 0x2B plus */
[0x1102,0x0000], /* 0x2C comma: choseong nieun ㄴ */
[0x002D,0x0000], /* 0x2D minus */
[0x300D,0x0000], /* 0x2E period: right corner bracket 」 */
[0x002F,0x0000], /* 0x2F slash */
[0x2015,0x0000], /* 0x30 0: horizontal bar ― */
[0x0037,0x0000], /* 0x31 1: 7 */
[0x0038,0x0000], /* 0x32 2: 8 */
[0x0039,0x0000], /* 0x33 3: 9 */
[0x11B8,0x0000], /* 0x34 4: jongseong bieub _ㅂ */
[0x11BA,0x0000], /* 0x35 5: jongseong sios _ㅅ */
[0x1163,0x0000], /* 0x36 6: jungseong ya ㅑ */
[0x0028,0x0000], /* 0x37 7: left parenthesis ( */
[0x0029,0x0000], /* 0x38 8: right parenthesis ) */
[0x203B,0x0000], /* 0x39 9: reference mark ※ */
[0x003A,0x0000], /* 0x3A colon */
[0x003B,0x0000], /* 0x3B semicolon */
[0x1172,0x0000], /* 0x3C less: jungseong yu ㅠ */
[0x003D,0x0000], /* 0x3D equal */
[0x300F,0x0000], /* 0x3E greater: right white corner bracket 』 */
[0x003F,0x0000], /* 0x3F question */
[0x002A,0x0000], /* 0x40 at: asterisk * */
[0x0021,0x0000], /* 0x41 A: exclamation mark ! */
[0x1162,0x0000], /* 0x42 B: jungseong ae ㅐ */
[0x003E,0x0000], /* 0x43 C: greater-than sign > */
[0x0023,0x0000], /* 0x44 D: number sign # */
[0x005E,0x0000], /* 0x45 E: circumflex accent mark ^ */
[0x1173,0x0000], /* 0x46 F: jungseong eu ㅡ */
[0x1175,0x0000], /* 0x47 G: jungseong i ㅣ */
[0x1161,0x0000], /* 0x48 H: jungseong a ㅏ */
[0x116E,0x0000], /* 0x49 I: jungseong u ㅜ(index finger) */
[0x110B,0x0000], /* 0x4A J: choseong ieung ㅇ */
[0x1100,0x0000], /* 0x4B K: choseong gieug ㄱ */
[0x300E,0x0000], /* 0x4C L: left white corner bracket 『 */
[0x116D,0x0000], /* 0x4D M: jungseong yo ㅛ */
[0x1169,0x0000], /* 0x4E N: jungseong o ㅗ */
[0x300A,0x0000], /* 0x4F O: left double angle bracket 《 */
[0x300B,0x0000], /* 0x50 P: right double angle bracket 》 */
[0x0024,0x0000], /* 0x51 Q: dollar sign $ */
[0x1166,0x0000], /* 0x52 R: jungseong e ㅔ */
[0x0040,0x0000], /* 0x53 S: at sign @ */
[0x1167,0x0000], /* 0x54 T: jungseong yeo ㅕ */
[0x1169,0x0000], /* 0x55 U: jungseong o ㅗ(index finger) */
[0x116E,0x0000], /* 0x56 V: jungseong u ㅜ */
[0x0025,0x0000], /* 0x57 W: percent sign % */
[0x003C,0x0000], /* 0x58 X: less-than sign < */
[0x1165,0x0000], /* 0x59 Y: jungseong eo ㅓ */
[0x00B7,0x0000], /* 0x5A Z: middle dot · */
[0x005B,0x0000], /* 0x5B bracketleft */
[0x005C,0x0000], /* 0x5C backslash */
[0x005D,0x0000], /* 0x5D bracketright */
[0x1163,0x0000], /* 0x5E asciicircum: jungseong ya ㅑ */
[0x005F,0x0000], /* 0x5F underscore */
[0x0060,0x0000], /* 0x60 quoteleft */
[0x0031,0x0000], /* 0x61 a: 1 */
[0x11B7,0x0000], /* 0x62 b: jongseong mieum _ㅁ */
[0x002E,0x0000], /* 0x63 c: period . */
[0x0033,0x0000], /* 0x64 d: 3 */
[0x0036,0x0000], /* 0x65 e: 6 */
[0x11BC,0x0000], /* 0x66 f: jongseong ieung _ㅇ */
[0x11AB,0x0000], /* 0x67 g: jongseong nieun _ㄴ */
[0x1107,0x0000], /* 0x68 h: choseong bieub ㅂ */
[0x110C,0x116E], /* 0x69 i: choseong jieuj ㅈ, jungseong u ㅜ(index finger) */
[0x110B,0x0000], /* 0x6A j: choseong ieung ㅇ */
[0x1100,0x0000], /* 0x6B k: choseong gieug ㄱ */
[0x300C,0x0000], /* 0x6C l: left corner bracket 「 */
[0x1103,0x0000], /* 0x6D m: choseong digeud ㄷ */
[0x1106,0x0000], /* 0x6E n: choseong mieum ㅁ */
[0x3008,0x0000], /* 0x6F o: left angle bracket 〈 */
[0x3009,0x0000], /* 0x70 p: right angle bracket 〉 */
[0x0034,0x0000], /* 0x71 q: 4 */
[0x11A8,0x0000], /* 0x72 r: jongseong gieug _ㄱ */
[0x0032,0x0000], /* 0x73 s: 2 */
[0x11AF,0x0000], /* 0x74 t: jongseong rieul _ㄹ */
[0x1109,0x1169], /* 0x75 u: choseong sios ㅅ, jungseong o ㅗ(index finger) */
[0x11BB,0x0000], /* 0x76 v: jongseong ssang-sios _ㅆ */
[0x0035,0x0000], /* 0x77 w: 5 */
[0x002C,0x0000], /* 0x78 x: comma */
[0x1105,0x0000], /* 0x79 y: choseong rieul ㄹ */
[0x0030,0x0000], /* 0x7A z: 0 */
[0x007B,0x0000], /* 0x7B braceleft */
[0x007C,0x0000], /* 0x7C bar */
[0x007D,0x0000], /* 0x7D braceright */
[0x007E,0x0000] /* 0x7E asciitilde */
];
// 이건구 한 손 세벌식 왼손의 캡스락 배열
K3_LGG_OH_l_capslock_layout = [
[0x0037,0x0000], /* 0x21 exclam: 7 */
[0x0027,0x0000], /* 0x22 quotedbl: apostrophe ' */
[0x0039,0x0000], /* 0x23 numbersign: 9 */
[0x11B8,0x0000], /* 0x24 dollar: jongseong bieub _ㅂ */
[0x11BA,0x0000], /* 0x25 percent: jongseong sios _ㅅ */
[0x0028,0x0000], /* 0x26 ampersand: left parenthesis ( */
[0x0022,0x0000], /* 0x27 apostrophe: quotation mark " */
[0x203B,0x0000], /* 0x28 parenleft: reference mark ※ */
[0x2015,0x0000], /* 0x29 parenright: horizontal bar ― */
[0x0029,0x0000], /* 0x2A asterisk: right parenthesis ) */
[0x003D,0x0000], /* 0x2B plus: equal sign = */
[0x1172,0x0000], /* 0x2C comma: jungseong yu ㅠ */
[0x005F,0x0000], /* 0x2D minus: underscore sign _ */
[0x300F,0x0000], /* 0x2E period: right white corner bracket 』 */
[0x003F,0x0000], /* 0x2F slash: question mark ? */
[0x2026,0x0000], /* 0x30 0: horizontal ellipsis … */
[0x0026,0x0000], /* 0x31 1: ampersand & */
[0x002A,0x0000], /* 0x32 2: asterisk * */
[0xFFE6,0x0000], /* 0x33 3: full-width won sign ₩ */
[0x1168,0x0000], /* 0x34 4: jungseong ye ㅖ */
[0x1174,0x0000], /* 0x35 5: jungseong eui ㅢ */
[0x1163,0x0000], /* 0x36 6: jungseong ya ㅑ */
[0x25CB,0x0000], /* 0x37 7: white circle ○ */
[0x25A1,0x0000], /* 0x38 8: square □ */
[0x00D7,0x0000], /* 0x39 9: multiplication sign × */
[0x003B,0x0000], /* 0x3A colon: semicolon ; */
[0x003A,0x0000], /* 0x3B semicolon: colon : */
[0x1172,0x0000], /* 0x3C less: jungseong yu ㅠ */
[0x002B,0x0000], /* 0x3D equal: plus sign + */
[0x300D,0x0000], /* 0x3E greater: right corner bracket 」 */
[0x002F,0x0000], /* 0x3F question: slash / */
[0x0038,0x0000], /* 0x40 at: 8 */
[0x0031,0x0000], /* 0x41 A: 1 */
[0x11B7,0x0000], /* 0x42 B: jongseong mieum _ㅁ */
[0x002E,0x0000], /* 0x43 C: period . */
[0x0033,0x0000], /* 0x44 D: 3 */
[0x0036,0x0000], /* 0x45 E: 6 */
[0x11BC,0x0000], /* 0x46 F: jongseong ieung _ㅇ */
[0x11AB,0x0000], /* 0x47 G: jongseong nieun _ㄴ */
[0x1107,0x0000], /* 0x48 H: choseong bieub ㅂ */
[0x110C,0x0000], /* 0x49 I: choseong jieuj ㅈ */
[0x110B,0x0000], /* 0x4A J: choseong ieung ㅇ */
[0x1100,0x0000], /* 0x4B K: choseong gieug ㄱ */
[0x300C,0x0000], /* 0x4C L: left corner bracket「 */
[0x1103,0x0000], /* 0x4D M: choseong digeud ㄷ */
[0x1106,0x0000], /* 0x4E N: choseong mieum ㅁ */
[0x3008,0x0000], /* 0x4F O: left angle bracket 〈 */
[0x3009,0x0000], /* 0x50 P: right angle bracket 〉 */
[0x0034,0x0000], /* 0x51 Q: 4 */
[0x11A8,0x0000], /* 0x52 R: jongseong gieug _ㄱ */
[0x0032,0x0000], /* 0x53 S: 2 */
[0x11AF,0x0000], /* 0x54 T: jongseong rieul _ㄹ */
[0x1109,0x0000], /* 0x55 U: choseong sios ㅅ */
[0x11BB,0x0000], /* 0x56 V: jongseong ssang-sios _ㅆ */
[0x0035,0x0000], /* 0x57 W: 5 */
[0x002C,0x0000], /* 0x58 X: comma */
[0x1105,0x0000], /* 0x59 Y: choseong rieul ㄹ */
[0x0030,0x0000], /* 0x5A Z: 0 */
[0x007B,0x0000], /* 0x5B bracketleft: left brace { */
[0x007C,0x0000], /* 0x5C backslash: pipe | */
[0x007D,0x0000], /* 0x5D bracketright: right brace } */
[0x1163,0x0000], /* 0x5E asciicircum: jungseong ya ㅑ */
[0x002D,0x0000], /* 0x5F underscore: minus sign - */
[0x007E,0x0000], /* 0x60 quoteleft: tilde ~ */
[0x0021,0x0000], /* 0x61 a: exclamation mark ! */
[0x1162,0x0000], /* 0x62 b: jungseong ae ㅐ */
[0x003E,0x0000], /* 0x63 c: greater-than sign > */
[0x0023,0x0000], /* 0x64 d: number sign # */
[0x005E,0x0000], /* 0x65 e: circumflex accent mark ^ */
[0x1173,0x0000], /* 0x66 f: jungseong eu ㅡ */
[0x1175,0x0000], /* 0x67 g: jungseong i ㅣ */
[0x1161,0x0000], /* 0x68 h: jungseong a ㅏ */
[0x116E,0x0000], /* 0x69 i: jungseong u ㅜ(index finger) */
[0x110B,0x0000], /* 0x6A j: choseong ieung ㅇ */
[0x1100,0x0000], /* 0x6B k: choseong gieug ㄱ */
[0x300E,0x0000], /* 0x6C l: left white corner bracket 『 */
[0x116D,0x0000], /* 0x6D m: jungseong yo ㅛ */
[0x1169,0x0000], /* 0x6E n: jungseong o ㅗ */
[0x300A,0x0000], /* 0x6F o: left double angle bracket 《 */
[0x300B,0x0000], /* 0x70 p: right double angle bracket 》 */
[0x0024,0x0000], /* 0x71 q: dollar sign $ */
[0x1166,0x0000], /* 0x72 r: jungseong e ㅔ */
[0x0040,0x0000], /* 0x73 s: at sign @ */
[0x1167,0x0000], /* 0x74 t: jungseong yeo ㅕ */
[0x1169,0x0000], /* 0x75 u: jungseong o ㅗ(index finger) */
[0x116E,0x0000], /* 0x76 v: jungseong u ㅜ */
[0x0025,0x0000], /* 0x77 w: percent sign % */
[0x003C,0x0000], /* 0x78 x: less-than sign < */
[0x1165,0x0000], /* 0x79 y: jungseong eo ㅓ */
[0x00B7,0x0000], /* 0x7A z: middle dot · */
[0x005B,0x0000], /* 0x7B braceleft: left bracket [ */
[0x005C,0x0000], /* 0x7C bar: backslash \ */
[0x005D,0x0000], /* 0x7D braceright: right bracket ] */
[0x0060,0x0000] /* 0x7E asciitilde: grave accent mark ` */
];
K3_LGG_OH_ohk = [
[0x1168,0x0000], /* 0x21 exclam: jungseong ye ㅖ */
[0x0000,0x0000], /* 0x22 quotedbl: null */
[0x000D,0x0000], /* 0x23 numbersign: ENTER */
[0x003F,0x0000], /* 0x24 dollar: question mark ? */
[0x0021,0x0000], /* 0x25 percent: exclamation mark ! */
[0x0000,0x0000], /* 0x26 ampersand: null */
[0x0000,0x0000], /* 0x27 apostrophe: null */
[0x0000,0x0000], /* 0x28 parenleft: null */
[0x0000,0x0000], /* 0x29 parenright: null */
[0x0000,0x0000], /* 0x2A asterisk: null */
[0x0000,0x0000], /* 0x2B plus: null */
[0x0000,0x0000], /* 0x2C comma: null */
[0x0000,0x0000], /* 0x2D minus: null */
[0x0000,0x0000], /* 0x2E period: null */
[0x0000,0x0000], /* 0x2F slash: null */
[0x0000,0x0000], /* 0x30 0: null */
[0x11B8,0x0000], /* 0x31 1: jongseong bieub _ㅂ */
[0x11BA,0x0000], /* 0x32 2: jongseong sios _ㅅ */
[0x1163,0x0000], /* 0x33 3: jungseong ya ㅑ */
[0x002E,0x0000], /* 0x34 4: period . */
[0x002C,0x0000], /* 0x35 5: comma */
[0x000D,0x0000], /* 0x36 6: ENTER */
[0x0000,0x0000], /* 0x37 7: null */
[0x0000,0x0000], /* 0x38 8: null */
[0x0000,0x0000], /* 0x39 9: null */
[0x0000,0x0000], /* 0x3A colon: null */
[0x0000,0x0000], /* 0x3B semicolon: null */
[0x0000,0x0000], /* 0x3C less: null */
[0x0000,0x0000], /* 0x3D equal: null */
[0x0000,0x0000], /* 0x3E greater: null */
[0x0000,0x0000], /* 0x3F question: null */
[0x1174,0x0000], /* 0x40 at: jungseong eui ㅢ */
[0x1173,0x0000], /* 0x41 A: jungseong eu ㅡ */
[0x1172,0x0000], /* 0x42 B: jungseong yu ㅠ */
[0x1169,0x0000], /* 0x43 C: jungseong o ㅗ */
[0x1161,0x0000], /* 0x44 D: jungseong ㅏ a */
[0x1165,0x0000], /* 0x45 E: jungseong eo ㅓ */
[0x110B,0x0000], /* 0x46 F: choseong ieung ㅇ */
[0x1100,0x0000], /* 0x47 G: choseong giyeok ㄱ */
[0x0000,0x0000], /* 0x48 H: null */
[0x0000,0x0000], /* 0x49 I: null */
[0x0000,0x0000], /* 0x4A J: null */
[0x0000,0x0000], /* 0x4B K: null */
[0x0000,0x0000], /* 0x4C L: null */
[0x0000,0x0000], /* 0x4D M: null */
[0x0000,0x0000], /* 0x4E N: null */
[0x0000,0x0000], /* 0x4F O: null */
[0x0000,0x0000], /* 0x50 P: null */
[0x1166,0x0000], /* 0x51 Q: jungseong e ㅔ */
[0x1169,0x0000], /* 0x52 R: jungseong o ㅗ(index finger) */
[0x1175,0x0000], /* 0x53 S: jungseong i ㅣ */
[0x116E,0x0000], /* 0x54 T: jungseong u ㅜ(index finger) */
[0x0000,0x0000], /* 0x55 U: null */
[0x116D,0x0000], /* 0x56 V: jungseong yo ㅛ */
[0x1167,0x0000], /* 0x57 W: jungseong yeo ㅕ */
[0x1162,0x0000], /* 0x58 X: jungseong ae ㅐ */
[0x0000,0x0000], /* 0x59 Y: null */
[0x116E,0x0000], /* 0x5A Z: jungseong u ㅜ */
[0x0000,0x0000], /* 0x5B bracketleft: null */
[0x0000,0x0000], /* 0x5C backslash: null */
[0x0000,0x0000], /* 0x5D bracketright: null */
[0x000D,0x0000], /* 0x5E asciicircum: ENTER */
[0x0000,0x0000], /* 0x5F underscore: null */
[0x0008,0x0000], /* 0x60 quoteleft: BACKSPACE */
[0x11BC,0x0000], /* 0x61 a: jongseong ieung _ㅇ */
[0x1102,0x0000], /* 0x62 b: choseong nieun ㄴ */
[0x1106,0x0000], /* 0x63 c: choseong mieum ㅁ */
[0x1107,0x0000], /* 0x64 d: choseong bieub ㅂ */
[0x1105,0x0000], /* 0x65 e: choseong rieul ㄹ */
[0x110B,0x0000], /* 0x66 f: choseong ieung ㅇ */
[0x1100,0x0000], /* 0x67 g: choseong giyeog ㄱ */
[0x0000,0x0000], /* 0x68 h: null */
[0x0000,0x0000], /* 0x69 i: null */
[0x0000,0x0000], /* 0x6A j: null */
[0x0000,0x0000], /* 0x6B k: null */
[0x0000,0x0000], /* 0x6C l: null */
[0x0000,0x0000], /* 0x6D m: null */
[0x0000,0x0000], /* 0x6E n: null */
[0x0000,0x0000], /* 0x6F o: null */
[0x0000,0x0000], /* 0x70 p: null */
[0x11A8,0x0000], /* 0x71 q: jongseong giyeog _ㄱ */
[0x1109,0x1169], /* 0x72 r: choseong sios ㅅ, jungseong o ㅗ(index finger) */
[0x11AB,0x0000], /* 0x73 s: jongseong nieun _ㄴ */
[0x110C,0x116E], /* 0x74 t: choseong jieuj ㅈ, jungseong u ㅜ(index finger) */
[0x0000,0x0000], /* 0x75 u: null */
[0x1103,0x0000], /* 0x76 v: choseong digeud ㄷ */
[0x11AF,0x0000], /* 0x77 w: jongseong rieul _ㄹ */
[0x11B7,0x0000], /* 0x78 x: jongseong mieum _ㅁ */
[0x0000,0x0000], /* 0x79 y: null */
[0x11BB,0x0000], /* 0x7A z: jongseong ssang-sios _ㅆ */
[0x0000,0x0000], /* 0x7B braceleft: null */
[0x0000,0x0000], /* 0x7C bar: null */
[0x0000,0x0000], /* 0x7D braceright: null */
[0x0008,0x0000] /* 0x7E asciitilde: BACKSPACE */
];
K3_LGG_OH_ohk_capslock_layout = [
[0x007B,0x0000], /* 0x21 exclam: left brace { */
[0x0000,0x0000], /* 0x22 quotedbl: null */
[0x007E,0x0000], /* 0x23 numbersign: tilde ~ */
[0x003C,0x0000], /* 0x24 dollar: less-than sign < */
[0x003E,0x0000], /* 0x25 percent: greater-than sign > */
[0x0000,0x0000], /* 0x26 ampersand: null */
[0x0000,0x0000], /* 0x27 apostrophe: null */
[0x0000,0x0000], /* 0x28 parenleft: null */
[0x0000,0x0000], /* 0x29 parenright: null */
[0x0000,0x0000], /* 0x2A asterisk: null */
[0x0000,0x0000], /* 0x2B plus: null */
[0x0000,0x0000], /* 0x2C comma: null */
[0x0000,0x0000], /* 0x2D minus: null */
[0x0000,0x0000], /* 0x2E period: null */
[0x0000,0x0000], /* 0x2F slash: null */
[0x0000,0x0000], /* 0x30 0: null */
[0x005B,0x0000], /* 0x31 1: left bracket [ */
[0x005D,0x0000], /* 0x32 2: right bracket ] */
[0x0060,0x0000], /* 0x33 3: grave accent mark ` */
[0x002E,0x0000], /* 0x34 4: period . */
[0x002C,0x0000], /* 0x35 5: comma */
[0x000D,0x0000], /* 0x36 6: ENTER */
[0x0000,0x0000], /* 0x37 7: null */
[0x0000,0x0000], /* 0x38 8: null */
[0x0000,0x0000], /* 0x39 9: null */
[0x0000,0x0000], /* 0x3A colon: null */
[0x0000,0x0000], /* 0x3B semicolon: null */
[0x0000,0x0000], /* 0x3C less: null */
[0x0000,0x0000], /* 0x3D equal: null */
[0x0000,0x0000], /* 0x3E greater: null */
[0x0000,0x0000], /* 0x3F question: null */
[0x007D,0x0000], /* 0x40 at: right brace } */
[0x005F,0x0000], /* 0x41 A: underscore sign _ */
[0x003A,0x0000], /* 0x42 B: colon : */
[0x0040,0x0000], /* 0x43 C: at sign @ */
[0x0025,0x0000], /* 0x44 D: percent sign % */
[0x002A,0x0000], /* 0x45 E: asterisk * */
[0x005E,0x0000], /* 0x46 F: circumflex accent mark ^ */
[0x0022,0x0000], /* 0x47 G: quotation mark */
[0x0000,0x0000], /* 0x48 H: null */
[0x0000,0x0000], /* 0x49 I: null */
[0x0000,0x0000], /* 0x4A J: null */
[0x0000,0x0000], /* 0x4B K: null */
[0x0000,0x0000], /* 0x4C L: null */
[0x0000,0x0000], /* 0x4D M: null */
[0x0000,0x0000], /* 0x4E N: null */
[0x0000,0x0000], /* 0x4F O: null */
[0x0000,0x0000], /* 0x50 P: null */
[0x005C,0x0000], /* 0x51 Q: backslash \ */
[0x0028,0x0000], /* 0x52 R: left parenthesis ( */
[0x0024,0x0000], /* 0x53 S: dollar sign $ */
[0x0029,0x0000], /* 0x54 T: right parenthesis ) */
[0x0000,0x0000], /* 0x55 U: null */
[0x0023,0x0000], /* 0x56 V: number sign # */
[0x0026,0x0000], /* 0x57 W: ampersand & */
[0x007C,0x0000], /* 0x58 X: pipe | */
[0x0000,0x0000], /* 0x59 Y: null */
[0x002B,0x0000], /* 0x5A Z: plus sign + */
[0x0000,0x0000], /* 0x5B bracketleft: null */
[0x0000,0x0000], /* 0x5C backslash: null */
[0x0000,0x0000], /* 0x5D bracketright: null */
[0x000D,0x0000], /* 0x5E asciicircum: ENTER */
[0x0000,0x0000], /* 0x5F underscore: null */
[0x0008,0x0000], /* 0x60 quoteleft: BACKSPACE */
[0x002D,0x0000], /* 0x61 a: minus sign - */
[0x003B,0x0000], /* 0x62 b: semicolon ; */
[0x0032,0x0000], /* 0x63 c: 2 */
[0x0035,0x0000], /* 0x64 d: 5 */
[0x0038,0x0000], /* 0x65 e: 8 */
[0x0036,0x0000], /* 0x66 f: 6 */
[0x0027,0x0000], /* 0x67 g: apostrophe ' */
[0x0000,0x0000], /* 0x68 h: null */
[0x0000,0x0000], /* 0x69 i: null */
[0x0000,0x0000], /* 0x6A j: null */
[0x0000,0x0000], /* 0x6B k: null */
[0x0000,0x0000], /* 0x6C l: null */
[0x0000,0x0000], /* 0x6D m: null */
[0x0000,0x0000], /* 0x6E n: null */
[0x0000,0x0000], /* 0x6F o: null */
[0x0000,0x0000], /* 0x70 p: null */
[0x002F,0x0000], /* 0x71 q: slash / */
[0x0039,0x0000], /* 0x72 r: 9 */
[0x0034,0x0000], /* 0x73 s: 4 */
[0x0030,0x0000], /* 0x74 t: 0 */
[0x0000,0x0000], /* 0x75 u: null */
[0x0033,0x0000], /* 0x76 v: 3 */
[0x0037,0x0000], /* 0x77 w: 7 */
[0x0031,0x0000], /* 0x78 x: 1 */
[0x0000,0x0000], /* 0x79 y: null */
[0x003D,0x0000], /* 0x7A z: equal sign = */
[0x0000,0x0000], /* 0x7B braceleft: null */
[0x0000,0x0000], /* 0x7C bar: null */
[0x0000,0x0000], /* 0x7D braceright: null */
[0x0008,0x0000] /* 0x7E asciitilde: BACKSPACE */
];
// 3-2011 옛한글, 3-2012 옛한글, 3-2014, 3-2015P 자판의 한글 확장 배열
K3_2012y_extended_hangeul_layout = [
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x21 exclam: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x22 quotedbl: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x23 numbersign: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x24 dollar: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x25 percent: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x26 ampersand: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x27 apostrophe: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x28 parenleft: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x29 parenright: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x2A asterisk: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x2B plus: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x2C comma: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x2D minus: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x2E period: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x2F slash: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x30 0: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x31 1: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x32 2: jongseong ap_sieus(non-standard), jongseong ssang_ap_sieus(non-standard), jongseong dwis_sieus(non-standard), jongseong ssang_dwis_sieus(non-standard) */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x33 3: */
[[0xD7C2,0x0000], [0x0000,0x0000]], /* 0x34 4: i+yo */
[[0xD7C3,0x0000], [0x0000,0x0000]], /* 0x35 5: i+yu */
[[0x1199,0x0000], [0x0000,0x0000]], /* 0x36 6: i+ya */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x37 7: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x38 8: */
[[0x113C,0x113D], [0x113E,0x113F]], /* 0x39 9: choseong ap_sieus, choseong ssang_ap_sieus, choseong dwis_sieus, choseong ssang_dwis_sieus */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x3A colon: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x3B semicolon: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x3C less: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x3D equal: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x3E greater: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x3F question: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x40 at: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x41 A: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x42 B: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x43 C: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x44 D: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x45 E: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x46 F: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x47 G: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x48 H: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x49 I: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x4A J: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x4B K: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x4C L: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x4D M: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x4E N: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x4F O: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x50 P: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x51 Q: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x52 R: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x53 S: */
[[0xD7BE,0x0000], [0x0000,0x0000]], /* 0x54 T: i+yae */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x55 U: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x56 V: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x57 W: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x58 X: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x59 Y: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x5A Z: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x5B */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x5C */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x5D */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x5E */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x5F */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x60 */
[[0x0000,0x0000], [0x11F0,0x11EE]], /* 0x61 a: yes_ieung, ssang_yesieung */
[[0x119B,0x0000], [0x1195,0x0000]], /* 0x62 b: i+u, eu+u */
[[0x1168,0x0000], [0xD7BB,0x0000]], /* 0x63 c: ye, eu+e */
[[0xD7C4,0x0000], [0x1174,0x0000]], /* 0x64 d: i+i, eu+i */
[[0xD7BF,0x0000], [0x119E,0x11A2]], /* 0x65 e: i+yeo, alae_a, ssang_alae_a */
[[0x1198,0x0000], [0xD7B9,0x0000]], /* 0x66 f: i+a, eu+a */
[[0x119C,0x0000], [0x1196,0x0000]], /* 0x67 g: i+eu, eu+eu */
[[0x0000,0x0000], [0x1159,0xA97C]], /* 0x68 h: choseong yeolin_hieuh, choseong ssang_yeolin_hieuh */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x69 i: */
[[0x0000,0x0000], [0x114C,0x0000]], /* 0x6A j: choseong yes_ieung, choseong ssang_yes_ieung(non-standard) */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x6B k: */
[[0x114E,0x114F], [0x1150,0x1151]], /* 0x6C l: choseong ap_jieuj, choseong ssang_ap_jieuj, choseong dwis_jieuj, choseong ssang_dwis_jieuj */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x6D m: */
[[0x0000,0x0000], [0x1140,0x0000]], /* 0x6E n: choseong yeolin_sieus*/
[[0x1154,0x0000], [0x1155,0x0000]], /* 0x6F o: choseong ap_chieuch, choseong dwis_chieuch */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x70 p: */
[[0x0000,0x0000], [0x11EB,0x0000]], /* 0x71 q: jongseong yeolin_sieus */
[[0x0000,0x0000], [0xD7BA,0x0000]], /* 0x72 r: eu+eo */
[[0x0000,0x0000], [0x11F9,0x0000]], /* 0x73 s: jongseong yeolin_hieuh, jongseong yeolin_ssang_hieuh(non-standard) */
[[0x1164,0xD7BE], [0x0000,0x0000]], /* 0x74 t: yae, i+yae */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x75 u: */
[[0x119A,0x0000], [0xD7BC,0x0000]], /* 0x76 v: i+o, eu+o */
[[0x0000,0x0000], [0xD7DD,0x0000]], /* 0x77 w: jongseong lieul-ieung */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x78 x: jongseong ap_jieuj(non-standard), jongseong ssang_ap_jieuj(non-standard), jongseong dwis_jieuj(non-standard), jongseong ssang_dwis_jieuj(non-standard) */
[[0x0000,0x0000], [0x111B,0x0000]], /* 0x79 y: choseong lieul-ieung */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x7A z: jongseong ap_chieuch(non-standard), jongseong dwis_chieuch(non-standard) */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x7B braceleft: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x7C bar: */
[[0x0000,0x0000], [0x0000,0x0000]], /* 0x7D braceright: */
[[0x0000,0x0000], [0x0000,0x0000]] /* 0x7E asciitilde: */
];
// 3-2011 자판 기호 확장 배열
K3_2011_extended_sign_layout = [/*!*/[0,0,0], /*"*/[0x266A,0x266C,0], /*#*/[0,0,0], /*$*/[0xFFE0,0,0], /*%*/[0,0,0],
/*&*/[0,0,0], /*'*/[0x326B,0x2030,0x2031], /*(*/[0,0,0], /*)*/[0,0,0], /***/[0,0,0], /*+*/[0x2640,0,0],
/*,*/[0x3001,0x3008,0x2266], /*-*/[0x3010,0x3014,0x2601], /*.*/[0x3002,0x3009,0x2267], /*/*/[0x2026,0x203B,0x2504],
/*0*/[0x326A,0x300D,0], /*1*/[0x3BC,0x2122,0], /*2*/[0xB2,0x2109,0], /*3*/[0xB3,0x2103,0], /*4*/[0xFFE6,0xFFE5,0],
/*5*/[0x20AC,0xFFE1,0], /*6*/[0x327E,0x2702,0], /*7*/[0xA7,0x300E,0], /*8*/[0,0x300F,0], /*9*/[0,0x300C,0],
/*:*/[0x2463,0x246D,0x3254], /*;*/[0x3265,0x2463,0x246D],
/*<*/[0,0,0], /*=*/[0x3011,0x3015,0x2603], /*>*/[0,0,0], /*?*/[0,0,0], /*@*/[0,0,0],
/*A*/[0,0,0], /*B*/[0,0,0], /*C*/[0,0,0], /*D*/[0,0,0], /*E*/[0x2715,0,0], /*F*/[0,0,0], /*G*/[0xA6,0x2506,0],
/*H*/[0x2469,0x2473,0x325A], /*I*/[0x2466,0x2470,0x3257], /*J*/[0x2460,0x246A,0x3251], /*K*/[0x2461,0x246B,0x3252],
/*L*/[0x2462,0x246C,0x3253], /*M*/[0x201D,0x2019,0], /*N*/[0x201C,0x2018,0], /*O*/[0x2467,0x2471,0x3258],
/*P*/[0x2468,0x2472,0x3259], /*Q*/[0x2199,0x2196,0x261F], /*R*/[0xB4,0,0], /*S*/[0,0,0], /*T*/[0,0,0x2610],
/*U*/[0x2465,0x246F,0x3256], /*V*/[0,0,0], /*W*/[0x2198,0x2197,0x261D], /*X*/[0,0,0],
/*Y*/[0x2464,0x246E,0x3255], /*Z*/[0,0,0], /*[*/[0x7B,0xB1,0x2600], /*\*/[0x2260,0x2252,0xB6],
/*]*/[0x7D,0xF7,0x2602], /*^*/[0x321C,0,0], /*_*/[0x2642,0,0], /*`*/[0xA9,0xAE,0x2117],
/*a*/[0x25C7,0x25C8,0x25C6], /*b*/[0x2D0,0x25C1,0x25C0], /*c*/[0xB0,0x260E,0x2668], /*d*/[0x25CB,0x25C9,0x25CF],
/*e*/[0xD7,0x2194,0x2195], /*f*/[0x2015,0x25B3,0x25B2], /*g*/[0x7C,0x25BD,0x25BC], /*h*/[0x3261,0x2469,0x2473],
/*i*/[0x3264,0x2466,0x2470], /*j*/[0x3267,0x2460,0x246A], /*k*/[0x3260,0x2461,0x246B], /*l*/[0x3268,0x2462,0x246C],
/*m*/[0x326D,0x300B,0], /*n*/[0x3266,0x300A,0], /*o*/[0x3269,0x2467,0x2471], /*p*/[0x326C,0x2468,0x2472],
/*q*/[0x2190,0x2193,0x261C], /*r*/[0x60,0x2022,0x25E6], /*s*/[0x25A1,0x25A3,0x25A0], /*t*/[0x3003,0x2713,0x2611],
/*u*/[0x3262,0x2465,0x246F], /*v*/[0,0x25B7,0x25B6], /*w*/[0x2192,0x2191,0x261E], /*x*/[0x2032,0x2606,0x2605],
/*y*/[0x3263,0x2464,0x246E], /*z*/[0x2033,0x2661,0x2665], /*{*/[0,0,0], /*|*/[0,0,0], /*}*/[0xF7,0,0], /*~*/[0x327F,0,0]];
// 3-2012 자판 기호 확장 배열
K3_2012_extended_sign_layout = [/*!*/[0,0,0], /*"*/[0x266A,0x266C,0], /*#*/[0,0,0], /*$*/[0xFFE0,0,0],
/*%*/[8240,8241,0], /*&*/[0,0,0], /*'*/[0x326B,0xF7,0],
/*(*/[0,0,0], /*)*/[0,0,0], /***/[0,0,0], /*+*/[0,0,0],
/*,*/[0x3001,0x3008,0x2266], /*-*/[0xB1,0x2642,0x2601], /*.*/[0x3002,0x3009,0x2267], /*/*/[0x2026,0x203B,0x2504],
/*0*/[0x326A,0x300D,0], /*1*/[0x3BC,0x2122,0], /*2*/[0xB2,0x2109,0], /*3*/[0xB3,0x2103,0], /*4*/[0xFFE6,0xFFE5,0x4B0],
/*5*/[0x20AC,0xFFE1,0], /*6*/[0x327E,0x2702,0], /*7*/[0xA7,0x300E,0], /*8*/[0,0x300F,0], /*9*/[0,0x300C,0],
/*:*/[0x2463,0x246D,0x3254], /*;*/[0x3265,0x2463,0x246D], /*<*/[0,0x2640,0], /*=*/[0x2260,0,0x2603],
/*>*/[0,0,0], /*?*/[0,0,0], /*@*/[0,0,0],
/*A*/[0,0,0], /*B*/[0,0,0], /*C*/[0,0,0], /*D*/[0,0,0],
/*E*/[0x2715,0,0], /*F*/[0,0,0], /*G*/[0xA6,0x2506,0], /*H*/[0x2469,0x2473,0x325A],
/*I*/[0x2466,0x2470,0x3257], /*J*/[0x2460,0x246A,0x3251], /*K*/[0x2461,0x246B,0x3252], /*L*/[0x2462,0x246C,0x3253],
/*M*/[0x201D,0x2019,0], /*N*/[0x201C,0x2018,0], /*O*/[0x2467,0x2471,0x3258], /*P*/[0x2468,0x2472,0x3259],
/*Q*/[0x2199,0x2196,0x261F], /*R*/[0,0,0], /*S*/[0,0,0], /*T*/[0,0,0x2610],
/*U*/[0x2465,0x246F,0x3256], /*V*/[0,0,0], /*W*/[0x2198,0x2197,0x261D], /*X*/[0,0,0],
/*Y*/[0x2464,0x246E,0x3255], /*Z*/[0,0,0], /*[*/[0x3010,0x3014,0x2600], /*\*/[0x2252,0xB6,0],
/*]*/[0x3011,0x3015,0x2602], /*^*/[0x321C,0,0], /*_*/[0,0,0], /*`*/[0xA9,0xAE,0x2117],
/*a*/[0x25C7,0x25C8,0x25C6], /*b*/[0x2D0,0x25C1,0x25C0], /*c*/[0xB0,0x260E,0x2668], /*d*/[0x25CB,0x25C9,0x25CF],
/*e*/[0xD7,0x2194,0x2195], /*f*/[0xB7,0x25B3,0x25B2], /*g*/[0x2015,0x25BD,0x25BC], /*h*/[0x3261,0x2469,0x2473],
/*i*/[0x3264,0x2466,0x2470], /*j*/[0x3267,0x2460,0x246A], /*k*/[0x3260,0x2461,0x246B], /*l*/[0x3268,0x2462,0x246C],
/*m*/[0x326D,0x300B,0], /*n*/[0x3266,0x300A,0], /*o*/[0x3269,0x2467,0x2471], /*p*/[0x326C,0x2468,0x2472],
/*q*/[0x2190,0x2193,0x261C], /*r*/[0xB4,0x2022,0x25E6], /*s*/[0x25A1,0x25A3,0x25A0], /*t*/[0x3003,0x2713,0x2611],
/*u*/[0x3262,0x2465,0x246F], /*v*/[0,0x25B7,0x25B6], /*w*/[0x2192,0x2191,0x261E], /*x*/[0x2032,0x2606,0x2605],
/*y*/[0x3263,0x2464,0x246E], /*z*/[0x2033,0x2661,0x2665], /*{*/[0,0,0], /*|*/[0,0,0], /*}*/[0,0,0], /*~*/[0x327F,0,0]];
K3_2011y_extended_sign_layout = [
[[0x2921,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x21 exclam */
[[0x266A,0x266C,0x0000], [0x0000,0x0000,0x0000]], /* 0x22 quotedbl */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x23 numbersign */
[[0x0000,0x0000,0x0000], [0xFE35,0x0000,0x0000]], /* 0x24 dollar */
[[0x0000,0x0000,0x0000], [0xFE36,0x0000,0x0000]], /* 0x25 percent */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x26 ampersand */
[[0x326B,0x3279,0x0000], [0x2030,0x2031,0x0000]], /* 0x27 apostrophe */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x28 parenleft */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x29 parenright */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2A asterisk */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2B plus */
[[0x3001,0x0000,0x0000], [0x2266,0x226A,0x0000]], /* 0x2C comma */
[[0x2642,0x2600,0x2601], [0x0000,0x0000,0x0000]], /* 0x2D minus */
[[0x3002,0x0000,0x0000], [0x2267,0x226B,0x0000]], /* 0x2E period */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2F slash */
[[0x326A,0x3278,0x0000], [0x2713,0x2611,0x2610]], /* 0x30 0 */
[[0x2195,0x21C5,0x21F3], [0x03BC,0x0000,0x0000]], /* 0x31 1 */
[[0x2194,0x21C4,0x2B04], [0x2109,0x0000,0x0000]], /* 0x32 2 */
[[0xFFE5,0x04B0,0x0000], [0x2103,0x0000,0x0000]], /* 0x33 3 */
[[0xFFE6,0x0000,0x0000], [0xFFE0,0x0000,0x0000]], /* 0x34 4 */
[[0x20AC,0xFFE1,0x0000], [0x0000,0x0000,0x0000]], /* 0x35 5 */
[[0x327E,0x321C,0x327F], [0x2702,0x0000,0x0000]], /* 0x36 6 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x37 7 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x38 8 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x39 9 */
[[0x2084,0x0000,0x0000], [0x2074,0x0000,0x0000]], /* 0x3A colon */
[[0x3265,0x3273,0x0000], [0x2463,0x246D,0x3254,0x325E,0x32B9]], /* 0x3B semicolon */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x3C less */
[[0x2640,0x2602,0x2603], [0x2260,0x2245,0x0000]], /* 0x3D equal */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x3E greater */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x3F question */
[[0x2922,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x40 at */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x41 A */
[[0x0000,0x0000,0x0000], [0xFE40,0xFE3E,0x0000]], /* 0x42 B */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x43 C */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x44 D */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x45 E */
[[0x0000,0x0000,0x0000], [0xFE41,0xFE43,0x0000]], /* 0x46 F */
[[0x0000,0x0000,0x0000], [0xFE42,0xFE44,0x0000]], /* 0x47 G */
[[0x2080,0x0000,0x0000], [0x2070,0x0000,0x0000]], /* 0x48 H */
[[0x2087,0x0000,0x0000], [0x2077,0x0000,0x0000]], /* 0x49 I */
[[0x2081,0x0000,0x0000], [0x00B9,0x0000,0x0000]], /* 0x4A J */
[[0x2082,0x0000,0x0000], [0x00B2,0x33A1,0x0000]], /* 0x4B K */
[[0x2083,0x0000,0x0000], [0x00B3,0x33A5,0x0000]], /* 0x4C L */
[[0x2019,0x201D,0x0000], [0x0000,0x0000,0x0000]], /* 0x4D M */
[[0x2018,0x201C,0x0000], [0x0000,0x0000,0x0000]], /* 0x4E N */
[[0x2088,0x0000,0x0000], [0x2078,0x0000,0x0000]], /* 0x4F O */
[[0x2089,0x0000,0x0000], [0x2079,0x0000,0x0000]], /* 0x50 P */
[[0x2199,0x21B2,0x2B10], [0x2196,0x21B0,0x2B11]], /* 0x51 Q */
[[0x0000,0x0000,0x0000], [0xFE3B,0xFE39,0x0000]], /* 0x52 R */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x53 S */
[[0x0000,0x0000,0x0000], [0xFE38,0xFE3C,0xFE3A]], /* 0x54 T */
[[0x2086,0x0000,0x0000], [0x2076,0x0000,0x0000]], /* 0x55 U */
[[0x0000,0x0000,0x0000], [0xFE3F,0xFE3D,0x0000]], /* 0x56 V */
[[0x2198,0x21B3,0x2B0E], [0x2197,0x21B1,0x2B0F]], /* 0x57 W */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x58 X */
[[0x2085,0x0000,0x0000], [0x2075,0x0000,0x0000]], /* 0x59 Y */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5A Z */
[[0x2022,0x25B3,0x25B2], [0x00B1,0x25B7,0x25B6]], /* 0x5B bracketleft */
[[0x00B6,0x23CE,0x0000], [0x2252,0x2248,0x0000]], /* 0x5C backslash */
[[0x2025,0x25BD,0x25BC], [0x00F7,0x25C1,0x25C0]], /* 0x5D bracketright */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5E asciicircum */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5F underscore */
[[0x00A9,0x00AE,0x2117], [0x2122,0x0000,0x0000]], /* 0x60 quoteleft */
[[0x2026,0x2504,0x0000], [0x25C7,0x25C8,0x25C6]], /* 0x61 a */
[[0x00A7,0x0000,0x0000], [0x3009,0x300B,0x0000]], /* 0x62 b */
[[0x00B0,0x0000,0x0000], [0x260E,0x2668,0x0000]], /* 0x63 c */
[[0x302E,0x0000,0x0000], [0x25CB,0x25C9,0x25CF]], /* 0x64 d */
[[0x00D7,0x2715,0x0000], [0x203B,0x327C,0x327D]], /* 0x65 e */
[[0x2015,0x00AF,0xFFE3], [0x300C,0x300E,0x0000]], /* 0x66 f */
[[0x007C,0x00A6,0x2506], [0x300D,0x300F,0x0000]], /* 0x67 g */
[[0x3261,0x326F,0x0000], [0x2469,0x2473,0x325A,0x32B5,0x32BF]], /* 0x68 h */
[[0x3264,0x3272,0x0000], [0x2466,0x2470,0x3257,0x32B2,0x32BC]], /* 0x69 i */
[[0x3267,0x3275,0x0000], [0x2460,0x246A,0x3251,0x325B,0x32B6]], /* 0x6A j */
[[0x3260,0x326E,0x0000], [0x2461,0x246B,0x3252,0x325C,0x32B7]], /* 0x6B k */
[[0x3268,0x3276,0x0000], [0x2462,0x246C,0x3253,0x325D,0x32B8]], /* 0x6C l */
[[0x326D,0x327B,0x0000], [0x2234,0x2235,0x2261]], /* 0x6D m */
[[0x3266,0x3274,0x0000], [0x221E,0x221D,0x0000]], /* 0x6E n */
[[0x3269,0x3277,0x0000], [0x2467,0x2471,0x3258,0x32B3,0x32BD]], /* 0x6F o */
[[0x326C,0x327A,0x0000], [0x2468,0x2472,0x3259,0x32B4,0x32BE]], /* 0x70 p */
[[0x2190,0x261C,0x21E6], [0x2193,0x261F,0x21E9]], /* 0x71 q */
[[0x0060,0x00B4,0x0000], [0x007B,0x3010,0x3014]], /* 0x72 r */
[[0x302F,0x0000,0x0000], [0x25A1,0x25A3,0x25A0]], /* 0x73 s */
[[0x3003,0x0000,0x0000], [0x3011,0x3015,0x0000]], /* 0x74 t */
[[0x3262,0x3270,0x0000], [0x2465,0x246F,0x3256,0x32B1,0x32BB]], /* 0x75 u */
[[0x02D0,0x0000,0x0000], [0x3008,0x300A,0x0000]], /* 0x76 v */
[[0x2192,0x261E,0x21E8], [0x2191,0x261D,0x21E7]], /* 0x77 w */
[[0x2032,0x0000,0x0000], [0x2606,0x2605,0x0000]], /* 0x78 x */
[[0x3263,0x3271,0x0000], [0x2464,0x246E,0x3255,0x325F,0x32BA]], /* 0x79 y */
[[0x2033,0x0000,0x0000], [0x2661,0x2665,0x0000]], /* 0x7A z */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x7B braceleft */
[[0x00A6,0x2506,0x0000], [0x0000,0x0000,0x0000]], /* 0x7C bar */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x7D braceright */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]] /* 0x7E asciitilde */
];
K3_2012y_extended_sign_layout = [ // 3-2012 옛한글, 3-2014, 3-2015P, 3-P2 자판의 기호 확장 배열
[[0x2921,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x21 exclam */
[[0x266A,0x266C,0x0000], [0x0000,0x0000,0x0000]], /* 0x22 quotedbl */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x23 numbersign */
[[0x0000,0x0000,0x0000], [0xFE35,0x0000,0x0000]], /* 0x24 dollar */
[[0x0000,0x0000,0x0000], [0xFE36,0x0000,0x0000]], /* 0x25 percent */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x26 ampersand */
[[0x326B,0x3279,0x0000], [0x00F7,0x0000,0x0000]], /* 0x27 apostrophe */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x28 parenleft */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x29 parenright */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2A asterisk */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2B plus */
[[0x3001,0x0000,0x0000], [0x2266,0x226A,0x0000]], /* 0x2C comma */
[[0x2642,0x2600,0x2601], [0x00B1,0x002D,0x0000]], /* 0x2D minus */
[[0x3002,0x0000,0x0000], [0x2267,0x226B,0x0000]], /* 0x2E period */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2F slash */
[[0x326A,0x3278,0x0000], [0x2713,0x2611,0x2610]], /* 0x30 0 */
[[0x2195,0x21C5,0x21F3], [0x03BC,0x0000,0x0000]], /* 0x31 1 */
[[0x2194,0x21C4,0x2B04], [0x2109,0x0000,0x0000]], /* 0x32 2 */
[[0xFFE5,0x04B0,0x0000], [0x2103,0x0000,0x0000]], /* 0x33 3 */
[[0xFFE6,0x0000,0x0000], [0xFFE0,0x0000,0x0000]], /* 0x34 4 */
[[0x20AC,0xFFE1,0x0000], [0x2030,0x2031,0x0000]], /* 0x35 5 */
[[0x327E,0x321C,0x327F], [0x2702,0x0000,0x0000]], /* 0x36 6 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x37 7 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x38 8 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x39 9 */
[[0x2084,0x0000,0x0000], [0x2074,0x0000,0x0000]], /* 0x3A colon */
[[0x3265,0x3273,0x0000], [0x2463,0x246D,0x3254,0x325E,0x32B9]], /* 0x3B semicolon */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x3C less */
[[0x2640,0x2602,0x2603], [0x2260,0x2245,0x0000]], /* 0x3D equal */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x3E greater */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x3F question */
[[0x2922,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x40 at */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x41 A */
[[0x0000,0x0000,0x0000], [0xFE40,0xFE3E,0x0000]], /* 0x42 B */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x43 C */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x44 D */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x45 E */
[[0x0000,0x0000,0x0000], [0xFE41,0xFE43,0x0000]], /* 0x46 F */
[[0x0000,0x0000,0x0000], [0xFE42,0xFE44,0x0000]], /* 0x47 G */
[[0x2080,0x0000,0x0000], [0x2070,0x0000,0x0000]], /* 0x48 H */
[[0x2087,0x0000,0x0000], [0x2077,0x0000,0x0000]], /* 0x49 I */
[[0x2081,0x0000,0x0000], [0x00B9,0x0000,0x0000]], /* 0x4A J */
[[0x2082,0x0000,0x0000], [0x00B2,0x33A1,0x0000]], /* 0x4B K */
[[0x2083,0x0000,0x0000], [0x00B3,0x33A5,0x0000]], /* 0x4C L */
[[0x2019,0x201D,0x0000], [0x0000,0x0000,0x0000]], /* 0x4D M */
[[0x2018,0x201C,0x0000], [0x0000,0x0000,0x0000]], /* 0x4E N */
[[0x2088,0x0000,0x0000], [0x2078,0x0000,0x0000]], /* 0x4F O */
[[0x2089,0x0000,0x0000], [0x2079,0x0000,0x0000]], /* 0x50 P */
[[0x2199,0x21B2,0x2B10], [0x2196,0x21B0,0x2B11]], /* 0x51 Q */
[[0x0000,0x0000,0x0000], [0xFE3B,0xFE39,0x0000]], /* 0x52 R */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x53 S */
[[0x0000,0x0000,0x0000], [0xFE3C,0xFE3A,0x0000]], /* 0x54 T */
[[0x2086,0x0000,0x0000], [0x2076,0x0000,0x0000]], /* 0x55 U */
[[0x0000,0x0000,0x0000], [0xFE3F,0xFE3D,0x0000]], /* 0x56 V */
[[0x2198,0x21B3,0x2B0E], [0x2197,0x21B1,0x2B0F]], /* 0x57 W */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x58 X */
[[0x2085,0x0000,0x0000], [0x2075,0x0000,0x0000]], /* 0x59 Y */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5A Z */
[[0x25B3,0x25B2,0x0000], [0x25B7,0x25B6,0x0000]], /* 0x5B bracketleft */
[[0x00B6,0x23CE,0x0000], [0x2252,0x2248,0x0000]], /* 0x5C backslash:*/
[[0x25BD,0x25BC,0x0000], [0x25C1,0x25C0,0x0000]], /* 0x5D bracketright:*/
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5E asciicircum:*/
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5F */
[[0x00A9,0x00AE,0x2117], [0x2122,0x0000,0x0000]], /* 0x60 */
[[0x2026,0x2504,0x0000], [0x25C7,0x25C8,0x25C6]], /* 0x61 a */
[[0x00A7,0x0000,0x0000], [0x3009,0x300B,0x0000]], /* 0x62 b */
[[0x00B0,0x0000,0x0000], [0x260E,0x2668,0x0000]], /* 0x63 c */
[[0x302E,0x0000,0x0000], [0x25CB,0x25C9,0x25CF]], /* 0x64 d */
[[0x00D7,0x2715,0x0000], [0x203B,0x327C,0x327D]], /* 0x65 e */
[[0x00B7,0x2022,0x25E6], [0x300C,0x300E,0x0000]], /* 0x66 f */
[[0x2015,0x00AF,0xFFE3], [0x300D,0x300F,0x0000]], /* 0x67 g */
[[0x3261,0x326F,0x0000], [0x2469,0x2473,0x325A,0x32B5,0x32BF]], /* 0x68 h */
[[0x3264,0x3272,0x0000], [0x2466,0x2470,0x3257,0x32B2,0x32BC]], /* 0x69 i */
[[0x3267,0x3275,0x0000], [0x2460,0x246A,0x3251,0x325B,0x32B6]], /* 0x6A j */
[[0x3260,0x326E,0x0000], [0x2461,0x246B,0x3252,0x325C,0x32B7]], /* 0x6B k */
[[0x3268,0x3276,0x0000], [0x2462,0x246C,0x3253,0x325D,0x32B8]], /* 0x6C l */
[[0x326D,0x327B,0x0000], [0x2234,0x2235,0x2261]], /* 0x6D m */
[[0x3266,0x3274,0x0000], [0x221E,0x221D,0x0000]], /* 0x6E n */
[[0x3269,0x3277,0x0000], [0x2467,0x2471,0x3258,0x32B3,0x32BD]], /* 0x6F o */
[[0x326C,0x327A,0x0000], [0x2468,0x2472,0x3259,0x32B4,0x32BE]], /* 0x70 p */
[[0x2190,0x261C,0x21E6], [0x2193,0x261F,0x21E9]], /* 0x71 q */
[[0x00B4,0x0000,0x0000], [0x3010,0x3014,0x0000]], /* 0x72 r */
[[0x302F,0x0000,0x0000], [0x25A1,0x25A3,0x25A0]], /* 0x73 s */
[[0x3003,0x0000,0x0000], [0x3011,0x3015,0x0000]], /* 0x74 t */
[[0x3262,0x3270,0x0000], [0x2465,0x246F,0x3256,0x32B1,0x32BB]], /* 0x75 u */
[[0x02D0,0x0000,0x0000], [0x3008,0x300A,0x0000]], /* 0x76 v */
[[0x2192,0x261E,0x21E8], [0x2191,0x261D,0x21E7]], /* 0x77 w */
[[0x2032,0x0000,0x0000], [0x2606,0x2605,0x0000]], /* 0x78 x */
[[0x3263,0x3271,0x0000], [0x2464,0x246E,0x3255,0x325F,0x32BA]], /* 0x79 y */
[[0x2033,0x0000,0x0000], [0x2661,0x2665,0x0000]], /* 0x7A z */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x7B braceleft */
[[0x00A6,0x2506,0x0000], [0x0000,0x0000,0x0000]], /* 0x7C bar */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x7D braceright */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]] /* 0x7E asciitilde */
];
// 3-P3 자판의 기호 확장 배열
K3_P3_extended_sign_layout = [
[[0x2921,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x21 exclam */
[[0x2019,0x201D,0x0000], [0x0000,0x0000,0x0000]], /* 0x22 quotedbl */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x23 numbersign */
[[0x0000,0x0000,0x0000], [0xFE35,0x0000,0x0000]], /* 0x24 dollar */
[[0x0000,0x0000,0x0000], [0xFE36,0x0000,0x0000]], /* 0x25 percent */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x26 ampersand */
[[0x326B,0x3279,0x0000], [0x221E,0x221D,0x0000]], /* 0x27 apostrophe */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x28 parenleft */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x29 parenright */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2A asterisk */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2B plus */
[[0x3001,0x0000,0x0000], [0x2461,0x246B,0x3252,0x325C,0x32B7]], /* 0x2C comma */
[[0x2642,0x2600,0x2601], [0x00B1,0x002D,0x0000]], /* 0x2D minus */
[[0x3002,0x0000,0x0000], [0x2462,0x246C,0x3253,0x325D,0x32B8]], /* 0x2E period */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x2F slash */
[[0x326A,0x3278,0x0000], [0x2713,0x2611,0x2610]], /* 0x30 0 */
[[0x2195,0x21C5,0x21F3], [0x03BC,0x0000,0x0000]], /* 0x31 1 */
[[0x2194,0x21C4,0x2B04], [0x2109,0x0000,0x0000]], /* 0x32 2 */
[[0xFFE5,0x04B0,0x0000], [0x2103,0x0000,0x0000]], /* 0x33 3 */
[[0xFFE6,0x0000,0x0000], [0xFFE0,0x0000,0x0000]], /* 0x34 4 */
[[0x20AC,0xFFE1,0x0000], [0x2030,0x2031,0x0000]], /* 0x35 5 */
[[0x327E,0x321C,0x327F], [0x2702,0x0000,0x0000]], /* 0x36 6 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x37 7 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x38 8 */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x39 9 */
[[0x266A,0x266C,0x0000], [0x0000,0x0000,0x0000]], /* 0x3A colon */
[[0x3265,0x3273,0x0000], [0x2234,0x2235,0x2261]], /* 0x3B semicolon */
[[0x2082,0x0000,0x0000], [0x00B2,0x33A1,0x0000]], /* 0x3C less */
[[0x2640,0x2602,0x2603], [0x2260,0x2245,0x0000]], /* 0x3D equal */
[[0x2083,0x0000,0x0000], [0x00B3,0x33A5,0x0000]], /* 0x3E greater */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x3F question */
[[0x2922,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x40 at */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x41 A */
[[0x2267,0x226B,0x0000], [0xFE40,0xFE3E,0x0000]], /* 0x42 B */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x43 C */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x44 D */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x45 E */
[[0x0000,0x0000,0x0000], [0xFE41,0xFE43,0x0000]], /* 0x46 F */
[[0x2266,0x226A,0x0000], [0xFE42,0xFE44,0x0000]], /* 0x47 G */
[[0x2018,0x201C,0x0000], [0x0000,0x0000,0x0000]], /* 0x48 H */
[[0x2088,0x0000,0x0000], [0x2078,0x0000,0x0000]], /* 0x49 I */
[[0x2084,0x0000,0x0000], [0x2074,0x0000,0x0000]], /* 0x4A J */
[[0x2085,0x0000,0x0000], [0x2075,0x0000,0x0000]], /* 0x4B K */
[[0x2086,0x0000,0x0000], [0x2076,0x0000,0x0000]], /* 0x4C L */
[[0x2081,0x0000,0x0000], [0x00B9,0x0000,0x0000]], /* 0x4D M */
[[0x2080,0x0000,0x0000], [0x2070,0x0000,0x0000]], /* 0x4E N */
[[0x2089,0x0000,0x0000], [0x2079,0x0000,0x0000]], /* 0x4F O */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x50 P */
[[0x2199,0x21B2,0x2B10], [0x2196,0x21B0,0x2B11]], /* 0x51 Q */
[[0x0000,0x0000,0x0000], [0xFE3B,0xFE39,0x0000]], /* 0x52 R */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x53 S */
[[0x0000,0x0000,0x0000], [0xFE3C,0xFE3A,0x0000]], /* 0x54 T */
[[0x2087,0x0000,0x0000], [0x2077,0x0000,0x0000]], /* 0x55 U */
[[0x0000,0x0000,0x0000], [0xFE3F,0xFE3D,0x0000]], /* 0x56 V */
[[0x2198,0x21B3,0x2B0E], [0x2197,0x21B1,0x2B0F]], /* 0x57 W */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x58 X */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x59 Y */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5A Z */
[[0x25B3,0x25B2,0x0000], [0x25B7,0x25B6,0x0000]], /* 0x5B bracketleft */
[[0x00B6,0x23CE,0x0000], [0x2252,0x2248,0x0000]], /* 0x5C backslash:*/
[[0x25BD,0x25BC,0x0000], [0x25C1,0x25C0,0x0000]], /* 0x5D bracketright:*/
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5E asciicircum:*/
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x5F */
[[0x00A9,0x00AE,0x2117], [0x2122,0x0000,0x0000]], /* 0x60 */
[[0x2026,0x2504,0x0000], [0x25C7,0x25C8,0x25C6]], /* 0x61 a */
[[0x00A7,0x0000,0x0000], [0x3009,0x300B,0x0000]], /* 0x62 b */
[[0x00B0,0x0000,0x0000], [0x260E,0x2668,0x0000]], /* 0x63 c */
[[0x302E,0x0000,0x0000], [0x25CB,0x25C9,0x25CF]], /* 0x64 d */
[[0x00D7,0x2715,0x0000], [0x203B,0x327C,0x327D]], /* 0x65 e */
[[0x00B7,0x2022,0x25E6], [0x300C,0x300E,0x0000]], /* 0x66 f */
[[0x2015,0x00AF,0xFFE3], [0x300D,0x300F,0x0000]], /* 0x67 g */
[[0x3261,0x326F,0x0000], [0x0000,0x0000,0x0000]], /* 0x68 h */
[[0x3264,0x3272,0x0000], [0x2467,0x2471,0x3258,0x32B3,0x32BD]], /* 0x69 i */
[[0x3267,0x3275,0x0000], [0x2463,0x246D,0x3254,0x325E,0x32B9]], /* 0x6A j */
[[0x3260,0x326E,0x0000], [0x2464,0x246E,0x3255,0x325F,0x32BA]], /* 0x6B k */
[[0x3268,0x3276,0x0000], [0x2465,0x246F,0x3256,0x32B1,0x32BB]], /* 0x6C l */
[[0x326D,0x327B,0x0000], [0x2460,0x246A,0x3251,0x325B,0x32B6]], /* 0x6D m */
[[0x3266,0x3274,0x0000], [0x2469,0x2473,0x325A,0x32B5,0x32BF]], /* 0x6E n */
[[0x3269,0x3277,0x0000], [0x2468,0x2472,0x3259,0x32B4,0x32BE]], /* 0x6F o */
[[0x326C,0x327A,0x0000], [0x0000,0x0000,0x0000]], /* 0x70 p */
[[0x2190,0x261C,0x21E6], [0x2193,0x261F,0x21E9]], /* 0x71 q */
[[0x00B4,0x0000,0x0000], [0x3010,0x3014,0x0000]], /* 0x72 r */
[[0x302F,0x0000,0x0000], [0x25A1,0x25A3,0x25A0]], /* 0x73 s */
[[0x3003,0x0000,0x0000], [0x3011,0x3015,0x0000]], /* 0x74 t */
[[0x3262,0x3270,0x0000], [0x2466,0x2470,0x3257,0x32B2,0x32BC]], /* 0x75 u */
[[0x02D0,0x0000,0x0000], [0x3008,0x300A,0x0000]], /* 0x76 v */
[[0x2192,0x261E,0x21E8], [0x2191,0x261D,0x21E7]], /* 0x77 w */
[[0x2032,0x0000,0x0000], [0x2606,0x2605,0x0000]], /* 0x78 x */
[[0x3263,0x3271,0x0000], [0x00F7,0x0000,0x0000]], /* 0x79 y */
[[0x2033,0x0000,0x0000], [0x2661,0x2665,0x0000]], /* 0x7A z */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x7B braceleft */
[[0x00A6,0x2506,0x0000], [0x0000,0x0000,0x0000]], /* 0x7C bar */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]], /* 0x7D braceright */
[[0x0000,0x0000,0x0000], [0x0000,0x0000,0x0000]] /* 0x7E asciitilde */
];
K3_Anmatae_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotation mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x002E, /* 0x27 apostrophe: period */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x11B7, /* 0x2C comma: jongseong mieum */
0x002D, /* 0x2D minus: minus sign */
0x11AF, /* 0x2E period: jongseong lieul */
0x11C2, /* 0x2F slash: jongseong hieuh */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x003A, /* 0x3A colon: colon */
0x116E, /* 0x3B semicolon: jungseong u */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: equals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x11F9, /* 0x3F question: jongseong yeolin_hieuh */
0x0040, /* 0x40 at: commercial at */
0x1107, /* 0x41 A: choseong bieub */
0x11f0, /* 0x42 B: jongseong yesieung */
0x11B8, /* 0x43 C: jongseong bieub */
0x1103, /* 0x44 D: choseong dieud */
0x1102, /* 0x45 E: choseong nieun */
0x1100, /* 0x46 F: choseong gieug */
0x114C, /* 0x47 G: choseong yesieung */
0x1165, /* 0x48 H: jungseong eo */
0x002F, /* 0x49 I: slash */
0x119E, /* 0x4A J: jungseong alae_a */
0x1175, /* 0x4B K: jungseong i */
0x1169, /* 0x4C L: jungseong o */
0x11AB, /* 0x4D M: jongseong nienu */
0x11EB, /* 0x4E N: jongseung pansieus */
0x005B, /* 0x4F O: left bracket */
0x005D, /* 0x50 P: right bracket */
0x1106, /* 0x51 Q: choseong mieum */
0x1105, /* 0x52 R: choseong lieul */
0x110C, /* 0x53 S: choseong jieuj */
0x1159, /* 0x54 T: choseong yeolin_hieuh */
0x0027, /* 0x55 U: apostrophe */
0x11A8, /* 0x56 V: jongseong gieug */
0x1140, /* 0x57 W: choseong pansieus */
0x11AE, /* 0x58 X: jongseong dieud */
0x003B, /* 0x59 Y: semicolon */
0x11BD, /* 0x5A Z: jongseong jieuj */
0x002C, /* 0x5B bracketleft: comma */
0x005C, /* 0x5C backslash: backslash */
0x003F, /* 0x5D bracketright: qustion mark */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x1107, /* 0x61 a: choseong bieub */
0x11BC, /* 0x62 b: jongseong ieung */
0x11B8, /* 0x63 c: jongseong bieub */
0x1103, /* 0x64 d: choseong dieud */
0x1102, /* 0x65 e: choseong nieun */
0x1100, /* 0x66 f: choseong gieug */
0x110B, /* 0x67 g: choseong ieung */
0x1165, /* 0x68 h: jungseong eo */
0x1173, /* 0x69 i: jungseong eu */
0x1161, /* 0x6A j: jungseong a */
0x1175, /* 0x6B k: jungseong i */
0x1169, /* 0x6C l: jungseong o */
0x11AB, /* 0x6D m: jongseong nieun */
0x11BA, /* 0x6E n: jongseong sieus */
0x116D, /* 0x6F o: jungseong yo */
0x1172, /* 0x70 p: jungseong yu */
0x1106, /* 0x71 q: choseong mieum */
0x1105, /* 0x72 r: choseong lieul */
0x110C, /* 0x73 s: choseong jieuj */
0x1112, /* 0x74 t: choseong hieuh */
0x1163, /* 0x75 u: jungseong ya */
0x11A8, /* 0x76 v: jongseong kiyok */
0x1109, /* 0x77 w: choseong sieus */
0x11AE, /* 0x78 x: jongseong dieud */
0x1167, /* 0x79 y: jungseong yeo */
0x11BD, /* 0x7A z: jongseong jieuj */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical bar */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
K3_Semoe_2014_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x1169, /* 0x27 apostrophe: jungseong o */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x002F, /* 0x2F slash: slash */
0x0030, /* 0x30 0: 0 */
0x11B9, /* 0x31 1: jongseong bieub-sieus */
0x11AE, /* 0x32 2: jongseong dieud */
0x11B8, /* 0x33 3: jongseong bieub */
0x116D, /* 0x34 4: jungseong yo */
0x1163, /* 0x35 5: jungseong ya */
0x119E, /* 0x36 6: jungseong alae_a */
0x11A2, /* 0x37 7: jungseong ssang_alae_a */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x0034, /* 0x3A colon: 4 */
0x11BB, /* 0x3B semicolon: jongseong ssang_sieus */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: euals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x2606, /* 0x41 A: ☆ white star */
0x003B, /* 0x42 B: semicolon */
0x300C, /* 0x43 C: 「 left corner bracket */
0x25B2, /* 0x44 D: ▲ black up-pointing triangle */
0x2192, /* 0x45 E: → rightwards arrow */
0x00B7, /* 0x46 F: middle dot */
0x003A, /* 0x47 G: colon */
0x0030, /* 0x48 H: 0 */
0x0037, /* 0x49 I: 7 */
0x0031, /* 0x4A J: 1 */
0x0032, /* 0x4B K: 2 */
0x0033, /* 0x4C L: 3 */
0x0022, /* 0x4D M: quotatioin mark */
0x0027, /* 0x4E N: apostrophe */
0x0038, /* 0x4F O: 8 */
0x0039, /* 0x50 P: 9 */
0x2661, /* 0x51 Q: ♡ white heart suit */
0x2194, /* 0x52 R: ↔ left right arrow */
0x25BD, /* 0x53 S: ▽ white down-pointing triangle */
0x203B, /* 0x54 T: ※ reference mark */
0x0036, /* 0x55 U: 6 */
0x300D, /* 0x56 V: 」 right corner bracket */
0x2190, /* 0x57 W: ← leftwards arrow */
0x25CE, /* 0x58 X: ◎ bullseye */
0x0035, /* 0x59 Y: 5 */
0x25A1, /* 0x5A Z: □ white square */
0x005B, /* 0x5B bracketleft: left bracket */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x11BC, /* 0x61 a: jongseong ieung */
0x1166, /* 0x62 b: jungseong e */
0x116E, /* 0x63 c: jungseong u */
0x1175, /* 0x64 d: jungseong i */
0x11AF, /* 0x65 e: jongseong lieul */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1102, /* 0x68 h: choseong nieun */
0x1106, /* 0x69 i: choseong mieum */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1112, /* 0x6D m: choseong hieuh */
0x1109, /* 0x6E n: choseong sieus */
0x1107, /* 0x6F o: choseong bieub */
0x116E, /* 0x70 p: jungseong u */
0x11C2, /* 0x71 q: jongseong hieuh */
0x1165, /* 0x72 r: jungseong eo */
0x11AB, /* 0x73 s: jongseong nieun */
0x1167, /* 0x74 t: jungseong yeo */
0x1103, /* 0x75 u: choseong dieud */
0x1169, /* 0x76 v: jungseong o */
0x11BA, /* 0x77 w: jongseong sieus */
0x11A8, /* 0x78 x: jongseong gieug */
0x1105, /* 0x79 y: choseong lieul */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
K3_Semoe_2014_sublayout = [
0x0000, /* 0x21 exclam: */
0x0000, /* 0x22 quotedbl */
0x0000, /* 0x23 numbersign */
0x0000, /* 0x24 dollar */
0x0000, /* 0x25 percent */
0x0000, /* 0x26 ampersand */
0x0000, /* 0x27 apostrophe */
0x0000, /* 0x28 parenleft */
0x0000, /* 0x29 parenright */
0x0000, /* 0x2A asterisk */
0x0000, /* 0x2B plus */
0x0000, /* 0x2C comma */
0x0000, /* 0x2D minus */
0x0000, /* 0x2E period */
0x0000, /* 0x2F slash */
0x0000, /* 0x30 0 */
0x11B3, /* 0x31 1: jongseong lieul-sieus */
0x11C0, /* 0x32 2: jongseong tieut */
0x11C1, /* 0x33 3: jongseong pieup */
0x0000, /* 0x34 4 */
0x0000, /* 0x35 5 */
0x0000, /* 0x36 6 */
0x0000, /* 0x37 7 */
0x0000, /* 0x38 8 */
0x0000, /* 0x39 9 */
0x0000, /* 0x3A colon */
0x0000, /* 0x3B semicolon */
0x0000, /* 0x3C less */
0x0000, /* 0x3D equal */
0x0000, /* 0x3E greater */
0x0000, /* 0x3F question */
0x0000, /* 0x40 at */
0x0000, /* 0x41 A */
0x0000, /* 0x42 B */
0x0000, /* 0x43 C */
0x0000, /* 0x44 D */
0x0000, /* 0x45 E */
0x0000, /* 0x46 F */
0x0000, /* 0x47 G */
0x0000, /* 0x48 H */
0x0000, /* 0x49 I */
0x0000, /* 0x4A J */
0x0000, /* 0x4B K */
0x0000, /* 0x4C L */
0x0000, /* 0x4D M */
0x0000, /* 0x4E N */
0x0000, /* 0x4F O */
0x0000, /* 0x50 P */
0x0000, /* 0x51 Q */
0x0000, /* 0x52 R */
0x0000, /* 0x53 S */
0x0000, /* 0x54 T */
0x0000, /* 0x55 U */
0x0000, /* 0x56 V */
0x0000, /* 0x57 W */
0x0000, /* 0x58 X */
0x0000, /* 0x59 Y */
0x0000, /* 0x5A Z */
0x0000, /* 0x3A colon */
0x0000, /* 0x3B semicolon */
0x0000, /* 0x3C less */
0x0000, /* 0x3D equal */
0x0000, /* 0x3E greater */
0x0000, /* 0x3F question */
0x11B8, /* 0x61 a: jongseong bieub */
0x0000, /* 0x62 b */
0x0000, /* 0x63 c */
0x0000, /* 0x64 d */
0x11BD, /* 0x65 e: jongseong jieuj */
0x0000, /* 0x66 f */
0x0000, /* 0x67 g */
0x0000, /* 0x68 h */
0x0000, /* 0x69 i */
0x0000, /* 0x6A j */
0x0000, /* 0x6B k */
0x0000, /* 0x6C l */
0x0000, /* 0x6D m */
0x0000, /* 0x6E n */
0x0000, /* 0x6F o */
0x0000, /* 0x70 p */
0x11B6, /* 0x71 q: jongseong lieul-hieuh */
0x0000, /* 0x72 r */
0x11AD, /* 0x73 s: jongseong nieun-hieuh */
0x0000, /* 0x74 t */
0x0000, /* 0x75 u */
0x0000, /* 0x76 v */
0x11BE, /* 0x77 w: jongseong chieuch */
0x11BF, /* 0x78 x: jongseong kieuk */
0x0000, /* 0x79 y */
0x11AE, /* 0x7A z: jongseong dieud */
0x0000, /* 0x7B braceleft */
0x0000, /* 0x7C bar */
0x0000, /* 0x7D braceright */
0x0000 /* 0x7E asciitilde */
];
K3_Semoe_2015_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x116E, /* 0x27 apostrophe: jungseong u */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x002F, /* 0x2F slash: slash */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x003A, /* 0x3A colon: colon */
0x11BB, /* 0x3B semicolon: jongseong ssang_sieus */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: euals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x2190, /* 0x41 A: ← leftwards arrow */
0x0027, /* 0x42 B: apostrophe */
0x300C, /* 0x43 C: 「 left corner bracket */
0x2192, /* 0x44 D: → rightwards arrow */
0x2661, /* 0x45 E: ♡ white heart suit */
0x25CB, /* 0x46 F: ○ */
0x00D7, /* 0x47 G: × */
0x3008, /* 0x48 H: 〈 */
0x2026, /* 0x49 I: … */
0x3009, /* 0x4A J: 〉 */
0x00B7, /* 0x4B K: · */
0x003B, /* 0x4C L: colon */
0x300F, /* 0x4D M: 』 */
0x300E, /* 0x4E N: 『 */
0x25B3, /* 0x4F O: △ white up-pointing triangle */
0x25BD, /* 0x50 P: ▽ white down-pointing triangle */
0x2194, /* 0x51 Q: ↔ left right arrow */
0x2606, /* 0x52 R: ☆ white star */
0x2193, /* 0x53 S: ↓ */
0x203B, /* 0x54 T: ※ reference mark */
0x300B, /* 0x55 U: 》 */
0x300D, /* 0x56 V: 」 right corner bracket */
0x2191, /* 0x57 W: ↑ */
0x25CE, /* 0x58 X: ◎ bullseye */
0x300A, /* 0x59 Y: 《 */
0x25A1, /* 0x5A Z: □ white square */
0x005B, /* 0x5B bracketleft: left bracket */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x11AF, /* 0x65 e: jongseong lieul */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1109, /* 0x68 h: choseong sieus */
0x1103, /* 0x69 i: choseong dieud */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1105, /* 0x6D m: choseong lieul */
0x1112, /* 0x6E n: choseong hieuh */
0x1107, /* 0x6F o: choseong pieup */
0x1169, /* 0x70 p: jungseong o */
0x11BA, /* 0x71 q: jongseong sieus */
0x1165, /* 0x72 r: jungseong eo */
0x11AB, /* 0x73 s: jongseong nieun */
0x1167, /* 0x74 t: jungseong yeo */
0x1102, /* 0x75 u: choseong nieun */
0x1169, /* 0x76 v: jungseong o */
0x11B8, /* 0x77 w: jongseong pieup */
0x11A8, /* 0x78 x: jongseong gieug */
0x1106, /* 0x79 y: choseong mieum */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
K3_Semoe_2015_sublayout = [
0x0000, /* 0x21 exclam */
0x0000, /* 0x22 quotedbl */
0x0000, /* 0x23 numbersign */
0x0000, /* 0x24 dollar */
0x0000, /* 0x25 percent */
0x0000, /* 0x26 ampersand */
0x0000, /* 0x27 apostrophe */
0x0000, /* 0x28 parenleft */
0x0000, /* 0x29 parenright */
0x0000, /* 0x2A asterisk */
0x0000, /* 0x2B plus */
0x0000, /* 0x2C comma */
0x0000, /* 0x2D minus */
0x0000, /* 0x2E period */
0x0000, /* 0x2F slash */
0x0000, /* 0x30 0 */
0x0000, /* 0x31 1 */
0x0000, /* 0x32 2 */
0x0000, /* 0x33 3 */
0x0000, /* 0x34 4 */
0x0000, /* 0x35 5 */
0x0000, /* 0x36 6 */
0x0000, /* 0x37 7 */
0x0000, /* 0x38 8 */
0x0000, /* 0x39 9 */
0x0000, /* 0x3A colon */
0x0000, /* 0x3B semicolon */
0x0000, /* 0x3C less */
0x0000, /* 0x3D equal */
0x0000, /* 0x3E greater */
0x0000, /* 0x3F question */
0x0000, /* 0x40 at */
0x0000, /* 0x41 A */
0x0000, /* 0x42 B */
0x0000, /* 0x43 C */
0x0000, /* 0x44 D */
0x0000, /* 0x45 E */
0x0000, /* 0x46 F */
0x0000, /* 0x47 G */
0x0000, /* 0x48 H */
0x0000, /* 0x49 I */
0x0000, /* 0x4A J */
0x0000, /* 0x4B K */
0x0000, /* 0x4C L */
0x0000, /* 0x4D M */
0x0000, /* 0x4E N */
0x0000, /* 0x4F O */
0x0000, /* 0x50 P */
0x0000, /* 0x51 Q */
0x0000, /* 0x52 R */
0x0000, /* 0x53 S */
0x0000, /* 0x54 T */
0x0000, /* 0x55 U */
0x0000, /* 0x56 V */
0x0000, /* 0x57 W */
0x0000, /* 0x58 X */
0x0000, /* 0x59 Y */
0x0000, /* 0x5A Z */
0x0000, /* 0x3A colon */
0x0000, /* 0x3B semicolon */
0x0000, /* 0x3C less */
0x0000, /* 0x3D equal */
0x0000, /* 0x3E greater */
0x0000, /* 0x3F question */
0x11B6, /* 0x61 a: jongseong lieul-hieuh */
0x1172, /* 0x62 b: jungseong yu */
0x1168, /* 0x63 c: jungseong ye */
0x0000, /* 0x64 d */
0x11BD, /* 0x65 e: jongseong jieuj */
0x0000, /* 0x66 f */
0x0000, /* 0x67 g */
0x0000, /* 0x68 h */
0x0000, /* 0x69 i */
0x0000, /* 0x6A j */
0x0000, /* 0x6B k */
0x0000, /* 0x6C l */
0x0000, /* 0x6D m */
0x0000, /* 0x6E n */
0x0000, /* 0x6F o */
0x0000, /* 0x70 p */
0x11BE, /* 0x71 q: jongseong chieuch */
0x1163, /* 0x72 r: jongseong ya */
0x11C2, /* 0x73 s: jongseong hieuh */
0x1164, /* 0x74 t: jungseong yae */
0x0000, /* 0x75 u */
0x116D, /* 0x76 v: jungseong yo */
0x11C1, /* 0x77 w: jongseong pieup */
0x11BF, /* 0x78 x: jongseong kieuk */
0x0000, /* 0x79 y */
0x11AE, /* 0x7A z: jongseong dieud */
0x0000, /* 0x7B braceleft */
0x0000, /* 0x7C bar */
0x0000, /* 0x7D braceright */
0x0000 /* 0x7E asciitilde */
];
K3_Semoe_2016_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x005B, /* 0x27 apostrophe: left bracket */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002C, /* 0x2C comma: comma */
0x002D, /* 0x2D minus: minus sign */
0x002E, /* 0x2E period: period */
0x002F, /* 0x2F slash: slash */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x003A, /* 0x3A colon: colon */
0x11BB, /* 0x3B semicolon: jongseong ssang_sieus */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: euals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x2190, /* 0x41 A: ← leftwards arrow */
0x00B0, /* 0x42 B: ° */
0x300C, /* 0x43 C: 「 left corner bracket */
0x2192, /* 0x44 D: → rightwards arrow */
0x2661, /* 0x45 E: ♡ white heart suit */
0x25CB, /* 0x46 F: ○ */
0x00D7, /* 0x47 G: × */
0x00B7, /* 0x48 H: · */
0x2015, /* 0x49 I: ― */
-100, /* 0x4A J: */
0x0027, /* 0x4B K: apostrophe */
0x003B, /* 0x4C L: colon */
0x300F, /* 0x4D M: 』 */
0x300E, /* 0x4E N: 『 */
0x25B3, /* 0x4F O: △ white up-pointing triangle */
0x25BD, /* 0x50 P: ▽ white down-pointing triangle */
0x2194, /* 0x51 Q: ↔ left right arrow */
0x2606, /* 0x52 R: ☆ white star */
0x2193, /* 0x53 S: ↓ */
0x203B, /* 0x54 T: ※ reference mark */
0x3009, /* 0x55 U: 〉 */
0x300D, /* 0x56 V: 」 right corner bracket */
0x2191, /* 0x57 W: ↑ */
0x25CE, /* 0x58 X: ◎ bullseye */
0x3008, /* 0x59 Y: 〈 */
0x25A1, /* 0x5A Z: □ white square */
0x1169, /* 0x5B bracketleft: jungseong o */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x11AF, /* 0x65 e: jongseong lieul */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1112, /* 0x68 h: choseong hieuh */
0x1103, /* 0x69 i: choseong dieud */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1105, /* 0x6D m: choseong lieul */
0x1109, /* 0x6E n: choseong sieus */
0x1107, /* 0x6F o: choseong pieup */
0x116E, /* 0x70 p: jungseong u */
0x11BA, /* 0x71 q: jongseong sieus */
0x1165, /* 0x72 r: jungseong eo */
0x11AB, /* 0x73 s: jongseong nieun */
0x1167, /* 0x74 t: jungseong yeo */
0x1102, /* 0x75 u: choseong nieun */
0x1169, /* 0x76 v: jungseong o */
0x11B8, /* 0x77 w: jongseong pieup */
0x11A8, /* 0x78 x: jongseong gieug */
0x1106, /* 0x79 y: choseong mieum */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
K3_Semoe_2016_sublayout = [
0x0000, /* 0x21 exclam */
0x0000, /* 0x22 quotedbl */
0x0000, /* 0x23 numbersign */
0x0000, /* 0x24 dollar */
0x0000, /* 0x25 percent */
0x0000, /* 0x26 ampersand */
0x0000, /* 0x27 apostrophe */
0x0000, /* 0x28 parenleft */
0x0000, /* 0x29 parenright */
0x0000, /* 0x2A asterisk */
0x0000, /* 0x2B plus */
0x0000, /* 0x2C comma */
0x0000, /* 0x2D minus */
0x0000, /* 0x2E period */
0x0000, /* 0x2F slash */
0x0000, /* 0x30 0 */
0x0000, /* 0x31 1 */
0x0000, /* 0x32 2 */
0x0000, /* 0x33 3 */
0x0000, /* 0x34 4 */
0x0000, /* 0x35 5 */
0x0000, /* 0x36 6 */
0x0000, /* 0x37 7 */
0x0000, /* 0x38 8 */
0x0000, /* 0x39 9 */
0x0000, /* 0x3A colon */
0x0000, /* 0x3B semicolon */
0x0000, /* 0x3C less */
0x0000, /* 0x3D equal */
0x0000, /* 0x3E greater */
0x0000, /* 0x3F question */
0x0000, /* 0x40 at */
0x0000, /* 0x41 A */
0x0000, /* 0x42 B */
0x0000, /* 0x43 C */
0x0000, /* 0x44 D */
0x0000, /* 0x45 E */
0x0000, /* 0x46 F */
0x0000, /* 0x47 G */
0x0000, /* 0x48 H */
0x0000, /* 0x49 I */
0x0000, /* 0x4A J */
0x0000, /* 0x4B K */
0x0000, /* 0x4C L */
0x0000, /* 0x4D M */
0x0000, /* 0x4E N */
0x0000, /* 0x4F O */
0x0000, /* 0x50 P */
0x0000, /* 0x51 Q */
0x0000, /* 0x52 R */
0x0000, /* 0x53 S */
0x0000, /* 0x54 T */
0x0000, /* 0x55 U */
0x0000, /* 0x56 V */
0x0000, /* 0x57 W */
0x0000, /* 0x58 X */
0x0000, /* 0x59 Y */
0x0000, /* 0x5A Z */
0x0000, /* 0x3A colon */
0x0000, /* 0x3B semicolon */
0x0000, /* 0x3C less */
0x0000, /* 0x3D equal */
0x0000, /* 0x3E greater */
0x0000, /* 0x3F question */
0x11B6, /* 0x61 a: jongseong lieul-hieuh */
0x1172, /* 0x62 b: jungseong yu */
0x1168, /* 0x63 c: jungseong ye */
0x0000, /* 0x64 d */
0x11BD, /* 0x65 e: jongseong jieuj */
0x0000, /* 0x66 f */
0x0000, /* 0x67 g */
0x0000, /* 0x68 h */
0x0000, /* 0x69 i */
0x0000, /* 0x6A j */
0x0000, /* 0x6B k */
0x0000, /* 0x6C l */
0x0000, /* 0x6D m */
0x0000, /* 0x6E n */
0x0000, /* 0x6F o */
0x0000, /* 0x70 p */
0x11BE, /* 0x71 q: jongseong chieuch */
0x1163, /* 0x72 r: jongseong ya */
0x11C2, /* 0x73 s: jongseong hieuh */
0x1164, /* 0x74 t: jungseong yae */
0x0000, /* 0x75 u */
0x116D, /* 0x76 v: jungseong yo */
0x11C1, /* 0x77 w: jongseong pieup */
0x11BF, /* 0x78 x: jongseong kieuk */
0x0000, /* 0x79 y */
0x11AE, /* 0x7A z: jongseong dieud */
0x0000, /* 0x7B braceleft */
0x0000, /* 0x7C bar */
0x0000, /* 0x7D braceright */
0x0000 /* 0x7E asciitilde */
];
K3_Semoe_2017_layout = [
0x0021, /* 0x21 exclam: exclamation mark */
0x0022, /* 0x22 quotedbl: quotatioin mark */
0x0023, /* 0x23 numbersign: number sign */
0x0024, /* 0x24 dollar: dollar sign */
0x0025, /* 0x25 percent: percent sign */
0x0026, /* 0x26 ampersand: ampersand */
0x002C, /* 0x27 apostrophe: comma */
0x0028, /* 0x28 parenleft: left parenthesis */
0x0029, /* 0x29 parenright: right parenthesis */
0x002A, /* 0x2A asterisk: asterisk */
0x002B, /* 0x2B plus: plus sign */
0x002E, /* 0x2C comma: period */
0x002D, /* 0x2D minus: minus sign */
0x1169, /* 0x2E period: jungseong o */
0x002F, /* 0x2F slash: slash */
0x0030, /* 0x30 0: 0 */
0x0031, /* 0x31 1: 1 */
0x0032, /* 0x32 2: 2 */
0x0033, /* 0x33 3: 3 */
0x0034, /* 0x34 4: 4 */
0x0035, /* 0x35 5: 5 */
0x0036, /* 0x36 6: 6 */
0x0037, /* 0x37 7: 7 */
0x0038, /* 0x38 8: 8 */
0x0039, /* 0x39 9: 9 */
0x003A, /* 0x3A colon: colon */
0x11BB, /* 0x3B semicolon: jongseong ssang_sieus */
0x003C, /* 0x3C less: less-than sign */
0x003D, /* 0x3D equal: euals sign */
0x003E, /* 0x3E greater: greater-than sign */
0x003F, /* 0x3F question: question mark */
0x0040, /* 0x40 at: commertial at */
0x2190, /* 0x41 A: ← leftwards arrow */
0x00B0, /* 0x42 B: ° */
0x300C, /* 0x43 C: 「 left corner bracket */
0x2192, /* 0x44 D: → rightwards arrow */
0x2661, /* 0x45 E: ♡ white heart suit */
0x25CB, /* 0x46 F: ○ */
0x00D7, /* 0x47 G: × */
0x00B7, /* 0x48 H: · */
0x2015, /* 0x49 I: ― */
0x0000, /* 0x4A J: */
0x0027, /* 0x4B K: apostrophe */
0x003B, /* 0x4C L: colon */
0x300F, /* 0x4D M: 』 */
0x300E, /* 0x4E N: 『 */
0x25B3, /* 0x4F O: △ white up-pointing triangle */
0x25BD, /* 0x50 P: ▽ white down-pointing triangle */
0x2194, /* 0x51 Q: ↔ left right arrow */
0x2606, /* 0x52 R: ☆ white star */
0x2193, /* 0x53 S: ↓ */
0x203B, /* 0x54 T: ※ reference mark */
0x3009, /* 0x55 U: 〉 */
0x300D, /* 0x56 V: 」 right corner bracket */
0x2191, /* 0x57 W: ↑ */
0x25CE, /* 0x58 X: ◎ bullseye */
0x3008, /* 0x59 Y: 〈 */
0x25A1, /* 0x5A Z: □ white square */
0x005B, /* 0x5B bracketleft: left bracket */
0x005C, /* 0x5C backslash: backslash */
0x005D, /* 0x5D bracketright: right bracket */
0x005E, /* 0x5E asciicircum: circumflex accent */
0x005F, /* 0x5F underscore: underscore */
0x0060, /* 0x60 quoteleft: grave accent */
0x11BC, /* 0x61 a: jongseong ieung */
0x116E, /* 0x62 b: jungseong u */
0x1166, /* 0x63 c: jungseong e */
0x1175, /* 0x64 d: jungseong i */
0x11AF, /* 0x65 e: jongseong lieul */
0x1161, /* 0x66 f: jungseong a */
0x1173, /* 0x67 g: jungseong eu */
0x1112, /* 0x68 h: choseong hieuh */
0x1103, /* 0x69 i: choseong dieud */
0x110B, /* 0x6A j: choseong ieung */
0x1100, /* 0x6B k: choseong gieug */
0x110C, /* 0x6C l: choseong jieuj */
0x1105, /* 0x6D m: choseong lieul */
0x1109, /* 0x6E n: choseong sieus */
0x1107, /* 0x6F o: choseong pieup */
0x116E, /* 0x70 p: jungseong u */
0x11BA, /* 0x71 q: jongseong sieus */
0x1165, /* 0x72 r: jungseong eo */
0x11AB, /* 0x73 s: jongseong nieun */
0x1167, /* 0x74 t: jungseong yeo */
0x1102, /* 0x75 u: choseong nieun */
0x1169, /* 0x76 v: jungseong o */
0x11B8, /* 0x77 w: jongseong pieup */
0x11A8, /* 0x78 x: jongseong gieug */
0x1106, /* 0x79 y: choseong mieum */
0x11B7, /* 0x7A z: jongseong mieum */
0x007B, /* 0x7B braceleft: left brace */
0x007C, /* 0x7C bar: vertical line(bar) */
0x007D, /* 0x7D braceright: right brace */
0x007E /* 0x7E asciitilde: tilde */
];
K3_Semoe_2017_sublayout = [
0x0000, /* 0x21 exclam */
0x0000, /* 0x22 quotedbl */
0x0000, /* 0x23 numbersign */
0x0000, /* 0x24 dollar */
0x0000, /* 0x25 percent */
0x0000, /* 0x26 ampersand */
0x0000, /* 0x27 apostrophe */
0x0000, /* 0x28 parenleft */
0x0000, /* 0x29 parenright */
0x0000, /* 0x2A asterisk */
0x0000, /* 0x2B plus */
0x0000, /* 0x2C comma */
0x0000, /* 0x2D minus */
0x0000, /* 0x2E period */
0x0000, /* 0x2F slash */
0x0000, /* 0x30 0 */
0x0000, /* 0x31 1 */
0x0000, /* 0x32 2 */
0x0000, /* 0x33 3 */
0x0000, /* 0x34 4 */
0x0000, /* 0x35 5 */
0x0000, /* 0x36 6 */
0x0000, /* 0x37 7 */
0x0000, /* 0x38 8 */
0x0000, /* 0x39 9 */
0x0000, /* 0x3A colon */
0x0000, /* 0x3B semicolon */
0x0000, /* 0x3C less */
0x0000, /* 0x3D equal */
0x0000, /* 0x3E greater */
0x0000, /* 0x3F question */
0x0000, /* 0x40 at */
0x0000, /* 0x41 A */
0x0000, /* 0x42 B */
0x0000, /* 0x43 C */
0x0000, /* 0x44 D */
0x0000, /* 0x45 E */
0x0000, /* 0x46 F */
0x0000, /* 0x47 G */
0x0000, /* 0x48 H */
0x0000, /* 0x49 I */
0x0000, /* 0x4A J */
0x0000, /* 0x4B K */
0x0000, /* 0x4C L */
0x0000, /* 0x4D M */
0x0000, /* 0x4E N */
0x0000, /* 0x4F O */
0x0000, /* 0x50 P */
0x0000, /* 0x51 Q */
0x0000, /* 0x52 R */
0x0000, /* 0x53 S */
0x0000, /* 0x54 T */
0x0000, /* 0x55 U */
0x0000, /* 0x56 V */
0x0000, /* 0x57 W */
0x0000, /* 0x58 X */
0x0000, /* 0x59 Y */
0x0000, /* 0x5A Z */
0x0000, /* 0x3A colon */
0x0000, /* 0x3B semicolon */
0x0000, /* 0x3C less */
0x0000, /* 0x3D equal */
0x0000, /* 0x3E greater */
0x0000, /* 0x3F question */
0x11B6, /* 0x61 a: jongseong lieul-hieuh */
0x1172, /* 0x62 b: jungseong yu */
0x1168, /* 0x63 c: jungseong ye */
0x0000, /* 0x64 d */
0x11BD, /* 0x65 e: jongseong jieuj */
0x0000, /* 0x66 f */
0x0000, /* 0x67 g */
0x0000, /* 0x68 h */
0x0000, /* 0x69 i */
0x0000, /* 0x6A j */
0x0000, /* 0x6B k */
0x0000, /* 0x6C l */
0x0000, /* 0x6D m */
0x0000, /* 0x6E n */
0x0000, /* 0x6F o */
0x0000, /* 0x70 p */
0x11BE, /* 0x71 q: jongseong chieuch */
0x1163, /* 0x72 r: jongseong ya */
0x11C2, /* 0x73 s: jongseong hieuh */
0x1164, /* 0x74 t: jungseong yae */
0x0000, /* 0x75 u */
0x116D, /* 0x76 v: jungseong yo */
0x11C1, /* 0x77 w: jongseong pieup */
0x11BF, /* 0x78 x: jongseong kieuk */
0x0000, /* 0x79 y */
0x11AE, /* 0x7A z: jongseong dieud */
0x0000, /* 0x7B braceleft */
0x0000, /* 0x7C bar */
0x0000, /* 0x7D braceright */
0x0000 /* 0x7E asciitilde */
];
} // input_additional_keyboard_layout_info()
function input_additional_combination_table_info() {
//K4_1969_Typewriter_combination_table = hangeul_combination_table_default.concat([
K4_1969_Typewriter_combination_table = hangeul_combination_table_default.concat([
[0x11631175,0x1164], /* jungseong ya + jungseong i = jungseong yae */
[0x11633163,0x3152], /* jungseong ya + hangeul letter i = hangeul letter yae */
//[0x31513163,0x3152], /* hangeul letter ya + hangeul letter i = hangeul letter yae */
[0x11671175,0x1168], /* jungseong yeo + jungseong i = jungseong ye */
[0x11673163,0x3156], /* jungseong yeo + hangeul letter i = hangeul letter ye */
//[0x31553163,0x3156], /* hangeul letter yeo + hangeul letter i = hangeul letter ye */
[0x1169314F,0x3158], /* jungseong o + hangeul letter a = hangeul letter wa */
[0x11693150,0x3159], /* jungseong o + hangeul letter ae = hangeul letter wae */
[0x11693163,0x315A], /* jungseong o + hangeul letter i = hangeul letter oe */
[0x116E3153,0x315D], /* jungseong u + hangeul letter eo = hangeul letter weo */
[0x116E3154,0x315E], /* jungseong u + hangeul letter e = hangeul letter we */
[0x116E3163,0x315F], /* jungseong u + hangeul letter i = hangeul letter wi */
//0x1171], /* 0x32 2, jungseong wi*/
//0x116C], /* 0x33 3, jungseong oe*/
//116A jungseong wa
//116F jungseong weo
[0x31613163,0x3156], /* hangeul letter eu + hangeul letter i = hangeul letter eui */
]);
K3_Oesol_Typewriter_combination_table = [
[0x11001100,0x1101], /* choseong gieug + gieug = ssang_gieug */
[0x11031103,0x1104], /* choseong dieud + dieud = ssang_dieud */
[0x11071107,0x1108], /* choseong bieup + bieup = ssang_bieup */
[0x11091109,0x110A], /* choseong sieus + sieus = ssang_sieus */
[0x110C110C,0x110D], /* choseong jieuj + jieuj = ssang_jieuj */
[0x11691161,0x116A], /* jungseong o + a = wa */
[0x11691162,0x116B], /* jungseong o + ae = wae */
[0x11691175,0x116C], /* jungseong o + i = oe */
[0x116E1165,0x116F], /* jungseong u + eo = weo */
[0x116E1166,0x1170], /* jungseong u + e = we */
[0x116E1175,0x1171], /* jungseong u + i = wi */
[0x11731175,0x1174] /* jungseong eu + i = eui */
];
Gaon38A_combination_table = hangeul_combination_table_default.concat([
[0x11611175,0x1162], /* jungseong a + i = ae */
[0x11631175,0x1164], /* jungseong ya + i = yae */
[0x11651175,0x1166], /* jungseong eo + i = e */
[0x11671175,0x1168], /* jungseong yeo + i = ye */
[0x116A1175,0x116B], /* jungseong wa + i = wae */
[0x116F1175,0x1170], /* jungseong weo + i = we */
]);
Gaon38Ay_combination_table = hangeul_combination_table_full.concat([
[0x110C113C,0x114E], /* jieuj + ap_sieus = ap_jieuj */
[0x110C113E,0x1150], /* jieuj + dwit-sieus = dwit-jieuj */
[0x110D113C,0x114F], /* ssang_jieuj + ap_sieus = ssang_ap_jieuj */
[0x110D113E,0x1151], /* ssang_jieuj + dwit-sieus = ssang_dwit-jieuj */
[0x110E113C,0x1154], /* chieuch + ap_sieus = ap_chieuch */
[0x110E113E,0x1155], /* chieuch + dwit-sieus = dwit-chieuch */
[0x11991175,0xD7BE], /* i-ya + i = i-yae */
[0xD7BF1175,0xD7C0], /* i-yeo + i = i-ye */
[0xD7BA1175,0xD7BB], /* eu-eo + i = eu-e */
[0x117F1175,0x1180], /* o-eo + i = o-e */
[0x11A61175,0x11A7], /* o-ya + i = o-yae */
[0xD7B21175,0xD7B3], /* yo-a + i = yo-ae */
[0x11841175,0x1185], /* yo-ya + i = yo-yae */
[0x11891175,0x118A], /* u-a + i = u-ae */
[0xD7B51175,0x118C], /* u-yeo + i = u-ye */
[0x118F1175,0x1190], /* yu-eo + i = yu-e */
[0x118E1175,0xD7B7], /* yu-a + i = yu-ae */
[0x119F1175,0xD7C6], /* alae_a-eo + i = alae_a-e */
]);
K3_Galmadeuli_Gong3_combination_table = hangeul_combination_table_default.concat([
[0x11751162,0x1164], /* jungseong i + ae = yae */
[0x11621162,0x1164], /* jungseong ae + ae = yae */
]);
K3_2015_additional_fortis_combination_table = [ // 3-2015 자판의 첫소리 된소리 추가 조합
[0x1100110B,0x1101], /* choseong gieug + ieung = ssang_gieug */
[0x110B1100,0x1101], /* choseong ieung + gieug = ssang_gieug */
[0x11031106,0x1104], /* choseong dieud + mieum = ssang_dieud */
[0x11061103,0x1104], /* choseong mieum + dieud = ssang_dieud */
[0x1107110C,0x1108], /* choseong bieub + jieuj = ssang_bieub */
[0x110C1107,0x1108], /* choseong jieuj + bieub = ssang_bieub */
[0x11091112,0x110A], /* choseong sieus + hieuh = ssang_sieus */
[0x11121109,0x110A], /* choseong hieuh + sieus = ssang_sieus */
[0x110C1100,0x110D], /* choseong jieuj + gieug = ssang_jieuj */
[0x1100110C,0x110D] /* choseong gieug + jieuj = ssang_jieuj */
];
K3_reverse_compound_sound_combination_table = [ // 겹받침 거꿀차례 조합
[0x11A811B7,0x11A9], /* jongseong gieug + mieum = ssang_gieug */
[0x11B711A8,0x11A9], /* jongseong mieum + gieug = ssang_gieug */
[0x11BA11A8,0x11AA], /* jongseong sieus + gieug = gieug-sieus */
[0x11BD11AB,0x11AC], /* jongseong jieuj + nieun = jieun-cieuj */
[0x11C211AB,0x11AD], /* jongseong hieuh + nieun = nieun-hieuh */
[0x11A811AF,0x11B0], /* jongseong gieug + lieul = lieul-gieug */
[0x11B711AF,0x11B1], /* jongseong mieum + lieul = lieul-mieum */
[0x11B811AF,0x11B2], /* jongseong bieub + lieul = lieul-bieub */
[0x11BA11AF,0x11B3], /* jongseong sieus + lieul = lieul-sieus */
[0x11C011AF,0x11B4], /* jongseong tieut + lieul = lieul-tieut */
[0x11C111AF,0x11B5], /* jongseong pieup + lieul = lieul-pieup */
[0x11C211AF,0x11B6], /* jongseong hieuh + lieul = lieul-hieuh */
[0x11BA11B8,0x11B9] /* jongseong sieus + bieub = bieub-sieus */
];
K3_2015_combination_table = K3_Galmadeuli_Gong3_combination_table.slice(0);
K3_2015_additional_combination_table = K3_2015_additional_fortis_combination_table.concat(K3_reverse_compound_sound_combination_table);
K3_2015y_combination_table = hangeul_combination_table_full.slice(0);
K3_2015y_combination_table.unshift(
[0x11BC11A8,0x11EC], /* jongseong ieung + gieug = yesieung-gieug */
[0x11BC11A9,0x11ED], /* jongseong ieung + ssang_gieug = yesieung-ssang_gieug */
[0x11BC11B7,0xD7F5], /* jongseong ieung + mieum = yesieung-mieum */
[0x11BC11BA,0x11F1], /* jongseong ieung + sieus = yesieung-sieus */
[0x11BC11BC,0x11EE], /* jongseong ieung + ieung = ssang_yesieung */
[0x11BC11BF,0x11EF], /* jongseong ieung + kieuk = yesieung-kieuk */
[0x11BC11C2,0xD7F6], /* jongseong ieung + hieuh = yesieung-hieuh */
[0x11BC11EB,0x11F2], /* jongseong ieung + yeolin_sieus = yesieung-yeolin_sieus */
[0x11BC11F0,0x11EE], /* jongseong ieung + yesieung = ssang_yesieung */
[0x11F011BC,0x11EE] /* jongseong yesieung + ieung = ssang_yesieung */
);
K3_D1_combination_table = K3_Galmadeuli_Gong3_combination_table.slice(0);
K3_D1_additional_combination_table = [
[0x1100110B,0x1101], /* choseong gieug + ieung = ssang_gieug */
[0x110B1100,0x1101], /* choseong ieung + gieug = ssang_gieug */
[0x11001103,0x1104], /* choseong gieug + dieud = ssang_dieud */
[0x11031100,0x1104], /* choseong dieud + gieug = ssang_dieud */
[0x1107110B,0x1108], /* choseong bieub + ieung = ssang_bieub */
[0x110B1107,0x1108], /* choseong ieung + bieub = ssang_bieub */
[0x11001109,0x110A], /* choseong gieug + sieus = ssang_sieus */
[0x11091100,0x110A], /* choseong sieus + gieug = ssang_sieus */
[0x110C110B,0x110D], /* choseong jieuj + ieung = ssang_jieuj */
[0x110B110C,0x110D], /* choseong ieung + jieuj = ssang_jieuj */
];
K3_D1_y_combination_table = hangeul_combination_table_full.slice(0);
K3_D1_y_combination_table.unshift(
[0x11651165,0x119E], /* jungseong eo + eo = alae_a */
[0x11741165,0x11A2] /* jungseong eui + eo = ssang_alae_a */
);
K3_Sin3_2015_combination_table = hangeul_combination_table_default.concat(K3_2015_additional_combination_table);
K3_2015M_combination_table = K3_Galmadeuli_Gong3_combination_table.slice(0);
// K3_2015M_combination_table.unshift(K3_2015_additional_compound_sound_combination_table);
K3_2015M_combination_table.unshift(
[0x11621165,0x1164] /* jungseong ae + eo = yae */
);
K3_Sin3_Gongdong_additional_combination_table = [
[0x1100110B,0x1101], /* choseong gieug + ieung = ssang_gieug */
[0x11061103,0x1104], /* choseong mieum + dieud = ssang_dieud */
[0x1107110C,0x1108], /* choseong bieub + jieuj = ssang_bieub */
[0x1107110B,0x1108], /* choseong bieub + ieung = ssang_bieub */
[0x11121109,0x110A], /* choseong hieuh + sieus = ssang_sieus */
[0x1109110C,0x110A], /* choseong sieus + jieuj = ssang_sieus */
[0x110C1100,0x110D] /* choseong jieuj + gieug = ssang_jieuj */
];
K3_Sin3_Gongdong_abbreviation_table = [
{phonemes: [0x1100,0x11BA], chars: [0xAC12]}, /* ㄱ *ㅅ : 값 */
{phonemes: [0x1102,0x11BA], chars: [0xB299]}, /* ㄴ *ㅅ : 늙 */
{phonemes: [0x1103,0x11BA], chars: [0xB2ED]}, /* ㄷ *ㅅ : 닭 */
{phonemes: [0x1105,0x11BA], chars: [0xB97C]}, /* ㄹ *ㅅ : 를 */
{phonemes: [0x1106,0x11BA], chars: [0xB9CE]}, /* ㅁ *ㅅ : 많 */
{phonemes: [0x1107,0x11BA], chars: [0xBC23]}, /* ㅂ *ㅅ : 밝 */
{phonemes: [0x1109,0x11BA], chars: [0xC0B6]}, /* ㅅ *ㅅ : 삶 */
{phonemes: [0x110B,0x11BA], chars: [0xC54A]}, /* ㅇ *ㅅ : 않 */
{phonemes: [0x110C,0x11BA], chars: [0xC796]}, /* ㅈ *ㅅ : 잖 */
{phonemes: [0x110E,0x11BA], chars: [0xCC2E]}, /* ㅊ *ㅅ : 찮 */
{phonemes: [0x1112,0x11BA], chars: [0xD759]}, /* ㅎ *ㅅ : 흙 */
{phonemes: [0x1100,0x1109], chars: [0xAC19]}, /* ㄱ ㅅ : 같 */
{phonemes: [0x1100,0x110C], chars: [0xACA0]}, /* ㄱ ㅈ : 겠 */
{phonemes: [0x1106,0x1109], chars: [0xB9D1]}, /* ㅁ ㅅ : 맑 */
{phonemes: [0x110B,0x1100], chars: [0xC788]}, /* ㅇ ㄱ : 있 */
{phonemes: [0x110B,0x1107], chars: [0xC785]}, /* ㅇ ㅂ : 입 */
{phonemes: [0x110B,0x110B], chars: [0xC600]}, /* ㅇ ㅇ : 였 */
{phonemes: [0x110B,0x1109], chars: [0xC77D]}, /* ㅇ ㅅ : 읽 */
{phonemes: [0x110B,0x110C], chars: [0xC5C6]}, /* ㅇ ㅈ : 없 */
{phonemes: [0x110B,0x1112], chars: [0xC783]}, /* ㅇ ㅎ : 잃 */
{phonemes: [0x110C,0x110B], chars: [0xC815]}, /* ㅈ ㅇ : 정 */
{phonemes: [0x1109,0x1107], chars: [0xC2B5]}, /* ㅅ ㅂ : 습 */
{phonemes: [0x1112,0x1107], chars: [0xD569]} /* ㅎ ㅂ : 합 */
];
K3_Sin3_Cham_combination_table = hangeul_combination_table_default.concat([
[0x110B1100,0x1101], /* choseong ieung + gieug = ssang_gieug */
[0x110B1103,0x1104], /* choseong ieung + dieud = ssang_dieud */
[0x110B1107,0x1108], /* choseong ieung + bieub = ssang_bieub */
[0x110B1109,0x110A], /* choseong ieung + sieus = ssang_sieus */
[0x11001109,0x110D], /* choseong gieun + sieus = ssang_jieuj */
]);
K3_Sin3_Cham_additional_combination_table = [
[0x11751161,0x1163], /* jungseong i + a = ya */
[0x11A811B7,0x11A9], /* jongseong gieug + mieum = ssang_gieug */
[0x11A811BB,0x11AA], /* jongseong gieug + ssang_sieus = gieug-sieus */
[0x11BA11A8,0x11AA], /* jongseong sieus + gieug = gieug-sieus */
[0x11A811AB,0x11AC], /* jongseong gieug + nieun = lieul-jieuj */
[0x11BD11AB,0x11AC], /* jongseong jieuj + nieun = lieul-jieuj */
[0x11AB11BC,0x11AD], /* jongseong nieun + ieung = nieun-hieuh */
[0x11BC11AB,0x11AD], /* jongseong ieung + nieun = nieun-hieuh */
[0x11C211AB,0x11AD], /* jongseong hieuh + nieun = nieun-hieuh */
[0x11A811AF,0x11B0], /* jongseong gieug + lieul = lieul-gieug */
[0x11B711AF,0x11B1], /* jongseong mieum + lieul = lieul-mieum */
[0x11AB11B7,0x11B2], /* jongseong nieun + mieum = lieul-bieub */
[0x11B711AB,0x11B2], /* jongseong mieum + nieun = lieul-bieub */
[0x11B811AF,0x11B2], /* jongseong bieub + lieul = lieul-bieub */
[0x11AF11BB,0x11B3], /* jongseong lieul + ssang_sieus = lieul-sieus */
[0x11BB11AF,0x11B3], /* jongseong ssang_sieus + lieul = lieul-sieus */
[0x11C011AF,0x11B4], /* jongseong tieut + lieul = lieul-tieut */
[0x11C111AF,0x11B5], /* jongseong pieup + lieul = lieul-pieup */
[0x11BC11AF,0x11B6], /* jongseong ieung + lieul = lieul-hieuh */
[0x11AF11BC,0x11B6], /* jongseong lieul + ieung = lieul-hieuh */
[0x11C211AF,0x11B6], /* jongseong hieuh + lieul = lieul-hieuh */
[0x11BC11B7,0x11B6], /* jongseong ieung + mieum = lieul-hieuh */
[0x11BA11B8,0x11B9], /* jongseong sieus + bieub = bieub-sieus */
[0x11A811B8,0x11BF], /* jongseong gieug + bieub = kieuk */
//[0x11091100,0x11AA], /* choseong sieus + gieug = jongseong gieug-sieus */
//[0x110C1102,0x11AC], /* choseong jieuj + nieun = jongseong nieun-jieuj */
//[0x11021112,0x11AD], /* choseong nieun + hieuh = jongseong nieun-hieuh */
//[0x11051100,0x11AF], /* choseong lieul + gieug = jongseong lieul-gieug */
//[0x11051106,0x11B1], /* choseong lieul + mieum = jongseong lieul-mieum */
//[0x11051109,0x11B4], /* choseong lieul + sieus = jongseong lieul-sieus */
//[0x11051110,0x11B4], /* choseong lieul + tieut = jongseong lieul-tieut */
//[0x11051111,0x11C1], /* choseong lieul + pieup = jongseong lieul-pieup */
//[0x11051112,0x11B6], /* choseong lieul + hieuh = jongseong lieul-hieuh */
//[0x11071109,0x11B9], /* choseong bieub + sieus = jongseong bieub-sieus */
];
K3_18Na_combination_table = hangeul_combination_table_default.concat([
[0x11AB11BC,0x11AD], /* jongseong nieun + ieung = jongseong nieun-hieuh */
[0x11AF11AB,0x11B4], /* jongseong lieul + nieun = jongseong lieul-tieut */
[0x11AF11BC,0x11B6], /* jongseong lieul + ieung = jongseong lieul-hieuh */
[0x11B211B8,0x11B5], /* jongseong lieul-bieub + bieub = jongseong lieul-pieup */
[0x11BD11BD,0x11BE] /* jongseong jieuj + jieuj = jongseong chieuch */
]);
K3_LGG_OH_combination_table = [
//첫 된소리 조합 규칙
[0x11001100, 0x1101], /* choseong kiyeok ㄱ + kiyeok ㄱ = ssangkiyeok ㄲ */
[0x11031100, 0x1104], /* choseong tikeut ㄷ + kiyeok ㄱ = ssangtikeut ㄸ */
[0x11071100, 0x1108], /* choseong pieup ㅂ + kiyeok ㄱ = ssangpieup ㅃ */
[0x11091100, 0x110A], /* choseong sios ㅅ + kiyeok ㄱ = ssangsios ㅆ */
[0x110C1100, 0x110D], /* choseong cieuc ㅈ + kiyeok ㄱ = ssangcieuc ㅉ */
//첫 거센소리 조합 규칙
[0x1100110B, 0x110F], /* choseong kiyeok ㄱ + ieung ㅇ = khiyeok ㅋ */
[0x1103110B, 0x1110], /* choseong tikeut ㄷ + ieung ㅇ = thieuth ㅌ */
[0x1107110B, 0x1111], /* choseong pieup ㅂ + ieung ㅇ = phieup ㅍ */
[0x110C110B, 0x110E], /* choseong cieuc ㅈ + ieung ㅇ = chieuc ㅊ */
[0x110B110B, 0x1112], /* choseong ieung ㅇ + ieung ㅇ = hieuh ㅎ */
//가 조합 규칙
[0x11691161, 0x116A], /* jungseong o ㅗ + a ㅏ = wa ㅘ */
[0x11691162, 0x116B], /* jungseong o ㅗ + ae ㅐ = wae ㅙ */
[0x11691175, 0x116C], /* jungseong o ㅗ + i ㅣ = oe ㅚ */
[0x116E1165, 0x116F], /* jungseong u ㅜ + eo ㅓ = weo ㅝ */
[0x116E1166, 0x1170], /* jungseong u ㅜ + e ㅔ = we ㅞ */
[0x116E1175, 0x1171], /* jungseong u ㅜ + i ㅣ = wi ㅟ */
[0x11631163, 0x1164], /* jungseong ya ㅑ + ya ㅑ = yae ㅒ */
//끝 치환 규칙
[0x11AB11AB, 0x11C0], /* jongseong nieun ㄴ + nieun ㄴ = thieuth ㅌ */
[0x11AF11AF, 0x11BF], /* jongseong rieul ㄹ + rieul ㄹ = khieukh ㅋ */
[0x11B711B7, 0x11AE], /* jongseong mieum ㅁ + mieum ㅁ = tigeut ㄷ */
[0x11B811B8, 0x11C1], /* jongseong pieup ㅂ + pieup ㅂ = phieuph ㅍ */
[0x11BA11BA, 0x11BD], /* jongseong sios ㅅ + sios ㅅ = cieuc ㅈ */
[0x11BB11BB, 0x11BE], /* jongseong ssangsios ㅆ + ssangsios ㅆ = chieuch ㅊ */
[0x11BC11BC, 0x11C2], /* jongseong ieung ㅇ + ieung ㅇ = hieuh ㅎ */
//끝 겹받침 조합 규칙
[0x11A811A8, 0x11A9], /* jongseong kiyeok ㄱ + kiyeok ㄱ = ssangkiyeok ㄲ */
[0x11A811BA, 0x11AA], /* jongseong kiyeok ㄱ + sios ㅅ = kiyeok-sois ㄳ */
[0x11AB11BA, 0x11AC], /* jongseong nieun ㄴ + sios ㅅ = nieun-cieuc ㄵ */
[0x11AB11BC, 0x11AD], /* jongseong nieun ㄴ + ieung ㅇ = nieun-hieuh ㄶ */
[0x11AF11A8, 0x11B0], /* jongseong rieul ㄹ + kiyeok ㄱ = rieul-kiyeok ㄺ */
[0x11AF11B7, 0x11B1], /* jongseong rieul ㄹ + mieum ㅁ = rieul-mieum ㄻ */
[0x11AF11B8, 0x11B2], /* jongseong rieul ㄹ + pieup ㅂ = rieul-pieup ㄼ */
[0x11AF11BA, 0x11B3], /* jongseong rieul ㄹ + sios ㅅ = rieul-sios ㄽ */
[0x11AF11AB, 0x11B4], /* jongseong rieul ㄹ + nieun ㄴ = rieul-thieuth ㄾ */
[0x11B211B8, 0x11B5], /* jongseong rieul-pieup ㄼ + pieup ㅂ = rieul-phieuph ㄿ */
[0x11AF11BC, 0x11B6], /* jongseong rieul ㄹ + ieung ㅇ = rieul-hieuh ㅀ */
[0x11B811BA, 0x11B9], /* jongseong pieup ㅂ + sios ㅅ = pieup-sios ㅄ */
];
K3_Semoe_2014_combination_table = [
{phonemes: [0x1169,0x1161,0x1175], char: 0x116B}, /* jungseong o + a + i = wae */
{phonemes: [0x110B,0x1100], char: 0x1101}, /* choseong ieung + gieug = ssang_gieug */
{phonemes: [0x110B,0x1103], char: 0x1104}, /* choseong ieung + dieud = ssang_dieud */
{phonemes: [0x110B,0x1106], char: 0x1104}, /* choseong mieum + ieung = ssang_dieud */
{phonemes: [0x110B,0x1107], char: 0x1108}, /* choseong ieung + bieub = ssang_bieub */
{phonemes: [0x110B,0x1109], char: 0x110A}, /* choseong ieung + sieus = ssang_sieus */
{phonemes: [0x110B,0x110C], char: 0x110D}, /* choseong ieung + jieuj = ssang_jieuj */
{phonemes: [0x1112,0x1100], char: 0x110F}, /* choseong hieuh + gieug = kieuk */
{phonemes: [0x1112,0x1103], char: 0x1110}, /* choseong hieuh + dieud = tieut */
{phonemes: [0x1112,0x1107], char: 0x1111}, /* choseong hieuh + bieub = pieup */
{phonemes: [0x1112,0x1109], char: 0x110E}, /* choseong hieuh + sieus = chieuch */
{phonemes: [0x1112,0x110C], char: 0x110E}, /* choseong hieuh + jieuj = chieuch */
{phonemes: [0x1161,0x1175], char: 0x1162}, /* jungseong a + i = ae */
{phonemes: [0x1161,0x1165], char: 0x116D}, /* jungseong a + eo = yo */
{phonemes: [0x1161,0x1166], char: 0x1163}, /* jungseong a + e = ya */
{phonemes: [0x1161,0x116E], char: 0x1172}, /* jungseong a + u = yu */
{phonemes: [0x1165,0x1161], char: 0x116D}, /* jungseong eo + a = yo */
{phonemes: [0x1165,0x1167], char: 0x1164}, /* jungseong eo + yeo = yae */
{phonemes: [0x1166,0x1175], char: 0x1168}, /* jungseong e + i = ye */
{phonemes: [0x1169,0x1175], char: 0x116C}, /* jungseong o + i = oe */
{phonemes: [0x1169,0x1161], char: 0x116A}, /* jungseong o + a = wa */
{phonemes: [0x1169,0x1166], char: 0x1168}, /* jungseong o + e = ye */
{phonemes: [0x1169,0x1169], char: 0x116D}, /* jungseong o + o = yo */
{phonemes: [0x1169,0x116E], char: 0x116D}, /* jungseong o + u = yo */
{phonemes: [0x116A,0x1175], char: 0x116B}, /* jungseong wa + i = wae */
{phonemes: [0x1169,0x1167], char: 0x1164}, /* jungseong o + yeo = yae */
{phonemes: [0x116F,0x1175], char: 0x1170}, /* jungseong weo + i = we */
{phonemes: [0x116E,0x1165], char: 0x116F}, /* jungseong u + eo = weo */
{phonemes: [0x116E,0x1166], char: 0x1170}, /* jungseong u + e = we */
{phonemes: [0x116E,0x1175], char: 0x1171}, /* jungseong u + i = wi */
{phonemes: [0x1173,0x1175], char: 0x1174}, /* jungseong eu + i = eui */
{phonemes: [0x1175,0x119E], char: 0x11A1}, /* jungseong i + alae_a = alae_ae */
{phonemes: [0x11A8,0x11BA], char: 0x11AA}, /* jongseong gieug + sieus = gieug-sieus */
{phonemes: [0x11AB,0x11AB], char: 0x11AD}, /* jongseong nieun + nieun = nieun-hieuh */
{phonemes: [0x11AB,0x11AF], char: 0x11AC}, /* jongseong nieun + lieul = nieun-jieuj */
{phonemes: [0x11AB,0x11BB], char: 0x11AD}, /* jongseong nieun + ssang_sieus = nieun-hieuh */
{phonemes: [0x11AB,0x11B7], char: 0x11C0}, /* jongseong nieun + mieum = tieut */
{phonemes: [0x11AB,0x11C2], char: 0x11AD}, /* jongseong nieun + hieuh = nieun-hieuh */
{phonemes: [0x11AE,0x11AF], char: 0x11B4}, /* jongseong dieud + lieul = lieul-tieut */
{phonemes: [0x11AE,0x11B8], char: 0x11B5}, /* jongseong dieud + bieub = lieul-pieup */
{phonemes: [0x11AE,0x11BB], char: 0x11C0}, /* jongseong dieud + ssang_sieus = tikeut */
{phonemes: [0x11AE,0x11C2], char: 0x11C0}, /* jongseong dieud + hieuh = tieut */
{phonemes: [0x11AF,0x11A8], char: 0x11B0}, /* jongseong lieul + gieug = lieul-gieug */
{phonemes: [0x11AF,0x11B7], char: 0x11B1}, /* jongseong lieul + mieum = lieul-mieum */
{phonemes: [0x11AF,0x11B8], char: 0x11B5}, /* jongseong lieul + bieub = lieul-pieup */
{phonemes: [0x11AF,0x11BA], char: 0x11B9}, /* jongseong lieul + sieus = bieub-sieus */
{phonemes: [0x11AF,0x11BC], char: 0x11B2}, /* jongseong lieul + ieung = lieul-bieub */
{phonemes: [0x11B7,0x11A8], char: 0x11B0}, /* jongseong mieum + gieug = lieul-gieug */
{phonemes: [0x11B7,0x11BB], char: 0x11AE}, /* jongseong mieum + ssang_sieus = dieud */
{phonemes: [0x11B7,0x11C2], char: 0x11B4}, /* jongseong mieum + hieuh = lieul-tieut */
{phonemes: [0x11B8,0x11BA], char: 0x11B9}, /* jongseong bieub + sieus = bieub-sieus */
{phonemes: [0x11B8,0x11C2], char: 0x11C1}, /* jongseong bieub + hieuh = pieup */
{phonemes: [0x11B9,0x11BB], char: 0x11B3}, /* jongseong bieub-sieus + ssang_sieus = lieul-sieus */
{phonemes: [0x11BA,0x11BB], char: 0x11BE}, /* jongseong sieus + ssang_sieus = chieuch */
{phonemes: [0x11BA,0x11BC], char: 0x11BB}, /* jongseong sieus + ieung = ssang_sieus */
{phonemes: [0x11BA,0x11C2], char: 0x11B3}, /* jongseong sieus + hieuh = lieul-sieus */
{phonemes: [0x11BB,0x11A8], char: 0x11BF}, /* jongseong ssang_sieus + gieug = kieuk */
{phonemes: [0x11BB,0x11AB], char: 0x11C2}, /* jongseong ssang_sieus + nieun = hieuh */
{phonemes: [0x11BB,0x11AF], char: 0x11BD}, /* jongseong ssang_sieus + lieul = jieuj */
{phonemes: [0x11BB,0x11B7], char: 0x11AE}, /* jongseong ssang_sieus + mieum = dieud */
{phonemes: [0x11BB,0x11B8], char: 0x11C1}, /* jongseong ssang_sieus + bieub = pieup */
{phonemes: [0x11BB,0x11BA], char: 0x11BE}, /* jongseong ssang_sieus + sieus = chieuch */
{phonemes: [0x11BB,0x11BC], char: 0x11B8}, /* jongseong ssang_sieus + ieung = bieub */
{phonemes: [0x11BB,0x11C2], char: 0x11B6}, /* jongseong ssang_sieus + hieuh = lieul-hieuh */
{phonemes: [0x11BC,0x11A8], char: 0x11A9}, /* jongseong ieung + gieug = ssang_egieug */
{phonemes: [0x11BC,0x11AB], char: 0x11C1}, /* jongseong ieung + nieun = pieup */
{phonemes: [0x11BC,0x11B7], char: 0x11B4}, /* jongseong ieung + mieum = lieul-tikeut */
{phonemes: [0x11BC,0x11B8], char: 0x11B5}, /* jongseong ieung + bieub = lieul-pieup */
{phonemes: [0x11BC,0x11BA], char: 0x11BB}, /* jongseong ieung + sieus = ssang_sieus */
{phonemes: [0x11BC,0x11BB], char: 0x11B6}, /* jongseong ieung + ssang_sieus = lieul-hieuh */
{phonemes: [0x11BC,0x11C2], char: 0x11B5}, /* jongseong ieung + hieuh = lieul-pieup */
{phonemes: [0x11C2,0x11A8], char: 0x11BF}, /* jongseong hieuh + gieug = kieuk */
{phonemes: [0x11C2,0x11AE], char: 0x11C0}, /* jongseong hieuh + dieud = tieut */
{phonemes: [0x11C2,0x11B8], char: 0x11C1} /* jongseong hieuh + bieub = pieup */
];
K3_Semoe_2015_combination_table = [
{phonemes: [0x1169,0x1161,0x1175], char: 0x116A}, /* jungseong o + a + i = wae */
{phonemes: [0x116E,0x1165,0x1175], char: 0x1170}, /* jungseong u + eo + i = we */
{phonemes: [0x110B,0x1100], char: 0x1101}, /* choseong ieung + gieug = ssang_gieug */
{phonemes: [0x110B,0x1103], char: 0x1104}, /* choseong ieung + dieud = ssang_dieud */
{phonemes: [0x110B,0x1107], char: 0x1108}, /* choseong ieung + bieub = ssang_bieub */
{phonemes: [0x110B,0x1109], char: 0x110A}, /* choseong ieung + sieus = ssang_sieus */
{phonemes: [0x110B,0x110C], char: 0x110D}, /* choseong ieung + jieuj = ssang_jieuj */
{phonemes: [0x1112,0x1100], char: 0x110F}, /* choseong hieuh + gieug = kieuk */
{phonemes: [0x1112,0x1103], char: 0x1110}, /* choseong hieuh + dieud = tieut */
{phonemes: [0x1112,0x1107], char: 0x1111}, /* choseong hieuh + bieub = pieup */
{phonemes: [0x1112,0x110C], char: 0x110E}, /* choseong hieuh + jieuj = chieuch */
{phonemes: [0x1161,0x1175], char: 0x1162}, /* jungseong a + i = ae */
{phonemes: [0x1161,0x1165], char: 0x116D}, /* jungseong a + eo = yo */
{phonemes: [0x1165,0x1161], char: 0x116D}, /* jungseong eo + a = yo */
{phonemes: [0x1165,0x1167], char: 0x1164}, /* jungseong eo + yeo = yae */
{phonemes: [0x1169,0x1175], char: 0x116C}, /* jungseong o + i = oe */
{phonemes: [0x1169,0x1161], char: 0x116A}, /* jungseong o + a = wa */
{phonemes: [0x1169,0x1166], char: 0x1168}, /* jungseong o + e = ye */
{phonemes: [0x1169,0x1165], char: 0x1163}, /* jungseong o + eo = ya */
{phonemes: [0x1169,0x1169], char: 0x116D}, /* jungseong o + o = yo */
{phonemes: [0x1169,0x116E], char: 0x1172}, /* jungseong o + u = yu */
{phonemes: [0x116A,0x1175], char: 0x116B}, /* jungseong wa + i = wae */
{phonemes: [0x1169,0x1167], char: 0x1164}, /* jungseong o + yeo = yae */
{phonemes: [0x116F,0x1175], char: 0x1170}, /* jungseong weo + i = we */
{phonemes: [0x116E,0x1165], char: 0x116F}, /* jungseong u + eo = weo */
{phonemes: [0x116E,0x1166], char: 0x1170}, /* jungseong u + e = we */
{phonemes: [0x116E,0x1175], char: 0x1171}, /* jungseong u + i = wi */
{phonemes: [0x1173,0x1175], char: 0x1174}, /* jungseong eu + i = eui */
{phonemes: [0x119E,0x119E], char: 0x11A2}, /* jungseong alae_a + alae_a = ssang_alae_a */
{phonemes: [0x11A8,0x11BA], char: 0x11AA}, /* jongseong gieug + sieus = gieug-sieus */
{phonemes: [0x11AB,0x11AB], char: 0x11AD}, /* jongseong nieun + nieun = nieun-hieuh */
{phonemes: [0x11AB,0x11AF], char: 0x11AC}, /* jongseong nieun + lieul = nieun-jieuj */
{phonemes: [0x11AB,0x11B7], char: 0x11C0}, /* jongseong nieun + mieum = tieut */
{phonemes: [0x11AB,0x11C2], char: 0x11AD}, /* jongseong nieun + hieuh = nieun-hieuh */
{phonemes: [0x11AF,0x11A8], char: 0x11B0}, /* jongseong lieul + gieug = lieul-gieug */
{phonemes: [0x11AF,0x11B7], char: 0x11B1}, /* jongseong lieul + mieum = lieul-mieum */
{phonemes: [0x11AF,0x11B8], char: 0x11B2}, /* jongseong lieul + bieub = lieul-bieub */
{phonemes: [0x11AF,0x11BA], char: 0x11B3}, /* jongseong lieul + sieus = lieul-sieus */
{phonemes: [0x11B7,0x11A8], char: 0x11B0}, /* jongseong mieum + gieug = lieul-gieug */
{phonemes: [0x11B8,0x11BA], char: 0x11B9}, /* jongseong bieub + sieus = bieub-sieus */
{phonemes: [0x11BB,0x11A8], char: 0x11BF}, /* jongseong ssang_sieus + gieug = kieuk */
{phonemes: [0x11BB,0x11AB], char: 0x11C2}, /* jongseong ssang_sieus + nieun = hieuh */
{phonemes: [0x11BB,0x11AF], char: 0x11BD}, /* jongseong ssang_sieus + lieul = jieuj */
{phonemes: [0x11BB,0x11B7], char: 0x11AE}, /* jongseong ssang_sieus + mieum = dieud */
{phonemes: [0x11BB,0x11B8], char: 0x11C1}, /* jongseong ssang_sieus + bieub = pieup */
{phonemes: [0x11BB,0x11BA], char: 0x11BE}, /* jongseong ssang_sieus + sieus = chieuch */
{phonemes: [0x11BB,0x11BC], char: 0x11B6}, /* jongseong ssang_sieus + ieung = lieul-hieuh */
{phonemes: [0x11BC,0x11A8], char: 0x11A9}, /* jongseong ieung + gieug = ssang_egieug */
{phonemes: [0x11BC,0x11AB], char: 0x11AD}, /* jongseong ieung + nieun = nieun-hieuh */
{phonemes: [0x11BC,0x11B7], char: 0x11B4}, /* jongseong ieung + mieum = lieul-tikeut */
{phonemes: [0x11BC,0x11B8], char: 0x11B5}, /* jongseong ieung + bieub = lieul-pieup */
{phonemes: [0x11BC,0x11BA], char: 0x11BB}, /* jongseong ieung + sieus = ssang_sieus */
{phonemes: [0x11BC,0x11BB], char: 0x11B6}, /* jongseong ieung + ssang_sieus = lieul-hieuh */
{phonemes: [0x11C2,0x11A8], char: 0x11BF}, /* jongseong hieuh + gieug = kieuk */
{phonemes: [0x11C2,0x11AE], char: 0x11C0}, /* jongseong hieuh + dieud = tieut */
{phonemes: [0x11C2,0x11B8], char: 0x11C1} /* jongseong hieuh + bieub = pieup */
];
K3_Semoe_2016_combination_table = [
{phonemes: [0x1169,0x1161,0x1175], char: 0x116B}, /* jungseong o + a + i = wae */
{phonemes: [0x11A8,0x11B7,0x11BB], char: 0x11AA}, /* jongseong gieug + mieum + ssang_sieus = gieug-sieus */
{phonemes: [0x11AF,0x11B8,0x11BB], char: 0x11C0}, /* jongseong lieul + bieub + ssang_sieus = tieut */
{phonemes: [0x11AF,0x11BA,0x11BB], char: 0x11A9}, /* jongseong lieul + sieus + ssang_sieus = ssang_egieug */
{phonemes: [0x11B8,0x11BA,0x11BB], char: 0x11B1}, /* jongseong bieub + sieus + ssang_sieus = lieul-mieum */
{phonemes: [0x110B,0x1100], char: 0x1101}, /* choseong ieung + gieug = ssang_gieug */
{phonemes: [0x110B,0x1103], char: 0x1104}, /* choseong ieung + dieud = ssang_dieud */
{phonemes: [0x110B,0x1107], char: 0x1108}, /* choseong ieung + bieub = ssang_bieub */
{phonemes: [0x110B,0x1109], char: 0x110A}, /* choseong ieung + sieus = ssang_sieus */
{phonemes: [0x110B,0x110C], char: 0x110D}, /* choseong ieung + jieuj = ssang_jieuj */
{phonemes: [0x1112,0x1100], char: 0x110F}, /* choseong hieuh + gieug = kieuk */
{phonemes: [0x1112,0x1103], char: 0x1110}, /* choseong hieuh + dieud = tieut */
{phonemes: [0x1112,0x1107], char: 0x1111}, /* choseong hieuh + bieub = pieup */
{phonemes: [0x1112,0x110C], char: 0x110E}, /* choseong hieuh + jieuj = chieuch */
{phonemes: [0x1161,0x1175], char: 0x1162}, /* jungseong a + i = ae */
{phonemes: [0x1161,0x1165], char: 0x116D}, /* jungseong a + eo = yo */
{phonemes: [0x1165,0x1161], char: 0x116D}, /* jungseong eo + a = yo */
{phonemes: [0x1169,0x1175], char: 0x116C}, /* jungseong o + i = oe */
{phonemes: [0x1169,0x1161], char: 0x116A}, /* jungseong o + a = wa */
{phonemes: [0x1169,0x1166], char: 0x1168}, /* jungseong o + e = ye */
{phonemes: [0x1169,0x1165], char: 0x1163}, /* jungseong o + eo = ya */
{phonemes: [0x1169,0x1169], char: 0x116D}, /* jungseong o + o = yo */
{phonemes: [0x1169,0x116E], char: 0x1172}, /* jungseong o + u = yu */
{phonemes: [0x116A,0x1175], char: 0x116B}, /* jungseong wa + i = wae */
{phonemes: [0x1169,0x1167], char: 0x1164}, /* jungseong o + yeo = yae */
{phonemes: [0x116F,0x1175], char: 0x1170}, /* jungseong weo + i = we */
{phonemes: [0x116E,0x1165], char: 0x116F}, /* jungseong u + eo = weo */
{phonemes: [0x116E,0x1166], char: 0x1170}, /* jungseong u + e = we */
{phonemes: [0x116E,0x1175], char: 0x1171}, /* jungseong u + i = wi */
{phonemes: [0x1173,0x1175], char: 0x1174}, /* jungseong eu + i = eui */
{phonemes: [0x11A8,0x11BA], char: 0x11AA}, /* jongseong gieug + sieus = gieug-sieus */
{phonemes: [0x11AB,0x11AB], char: 0x11AD}, /* jongseong nieun + nieun = nieun-hieuh */
{phonemes: [0x11AB,0x11AF], char: 0x11AC}, /* jongseong nieun + lieul = nieun-jieuj */
{phonemes: [0x11AB,0x11B7], char: 0x11C0}, /* jongseong nieun + mieum = tieut */
{phonemes: [0x11AB,0x11C2], char: 0x11AD}, /* jongseong nieun + hieuh = nieun-hieuh */
{phonemes: [0x11AF,0x11A8], char: 0x11B0}, /* jongseong lieul + gieug = lieul-gieug */
{phonemes: [0x11AF,0x11B7], char: 0x11B1}, /* jongseong lieul + mieum = lieul-mieum */
{phonemes: [0x11AF,0x11B8], char: 0x11B2}, /* jongseong lieul + bieub = lieul-bieub */
{phonemes: [0x11AF,0x11BA], char: 0x11B3}, /* jongseong lieul + sieus = lieul-sieus */
{phonemes: [0x11AF,0x11BC], char: 0x11A8}, /* jongseong lieul + ieung = gieug */
{phonemes: [0x11B7,0x11A8], char: 0x11B0}, /* jongseong mieum + gieug = lieul-gieug */
{phonemes: [0x11B8,0x11BA], char: 0x11B9}, /* jongseong bieub + sieus = bieub-sieus */
{phonemes: [0x11BB,0x11A8], char: 0x11BF}, /* jongseong ssang_sieus + gieug = kieuk */
{phonemes: [0x11BB,0x11AB], char: 0x11C2}, /* jongseong ssang_sieus + nieun = hieuh */
{phonemes: [0x11BB,0x11AF], char: 0x11BD}, /* jongseong ssang_sieus + lieul = jieuj */
{phonemes: [0x11BB,0x11B7], char: 0x11AE}, /* jongseong ssang_sieus + mieum = dieud */
{phonemes: [0x11BB,0x11B8], char: 0x11C1}, /* jongseong ssang_sieus + bieub = pieup */
{phonemes: [0x11BB,0x11BA], char: 0x11BE}, /* jongseong ssang_sieus + sieus = chieuch */
{phonemes: [0x11BB,0x11BC], char: 0x11B6}, /* jongseong ssang_sieus + ieung = lieul-hieuh */
{phonemes: [0x11BC,0x11A8], char: 0x11A9}, /* jongseong ieung + gieug = ssang_egieug */
{phonemes: [0x11BC,0x11AB], char: 0x11AD}, /* jongseong ieung + nieun = nieun-hieuh */
{phonemes: [0x11BC,0x11B7], char: 0x11B4}, /* jongseong ieung + mieum = lieul-tikeut */
{phonemes: [0x11BC,0x11B8], char: 0x11B5}, /* jongseong ieung + bieub = lieul-pieup */
{phonemes: [0x11BC,0x11BA], char: 0x11BB}, /* jongseong ieung + sieus = ssang_sieus */
{phonemes: [0x11BC,0x11BB], char: 0x11B6}, /* jongseong ieung + ssang_sieus = lieul-hieuh */
{phonemes: [0x11C2,0x11A8], char: 0x11BF}, /* jongseong hieuh + gieug = kieuk */
{phonemes: [0x11C2,0x11AE], char: 0x11C0}, /* jongseong hieuh + dieud = tieut */
{phonemes: [0x11C2,0x11B8], char: 0x11C1} /* jongseong hieuh + bieub = pieup */
];
K3_Semoe_2017_combination_table = [
{phonemes: [0x1169,0x1161,0x1175], char: 0x116B}, /* jungseong o + a + i = wae */
{phonemes: [0x11A8,0x11B7,0x11BB], char: 0x11AA}, /* jongseong gieug + mieum + ssang_sieus = gieug-sieus */
{phonemes: [0x11AF,0x11B8,0x11BB], char: 0x11C0}, /* jongseong lieul + bieub + ssang_sieus = tieut */
{phonemes: [0x11AF,0x11BA,0x11BB], char: 0x11A9}, /* jongseong lieul + sieus + ssang_sieus = ssang_egieug */
{phonemes: [0x11B8,0x11BA,0x11BB], char: 0x11B1}, /* jongseong bieub + sieus + ssang_sieus = lieul-mieum */
{phonemes: [0x110B,0x1100], char: 0x1101}, /* choseong ieung + gieug = ssang_gieug */
{phonemes: [0x110B,0x1103], char: 0x1104}, /* choseong ieung + dieud = ssang_dieud */
{phonemes: [0x110B,0x1107], char: 0x1108}, /* choseong ieung + bieub = ssang_bieub */
{phonemes: [0x110B,0x1109], char: 0x110A}, /* choseong ieung + sieus = ssang_sieus */
{phonemes: [0x110B,0x110C], char: 0x110D}, /* choseong ieung + jieuj = ssang_jieuj */
{phonemes: [0x1112,0x1100], char: 0x110F}, /* choseong hieuh + gieug = kieuk */
{phonemes: [0x1112,0x1103], char: 0x1110}, /* choseong hieuh + dieud = tieut */
{phonemes: [0x1112,0x1107], char: 0x1111}, /* choseong hieuh + bieub = pieup */
{phonemes: [0x1112,0x110C], char: 0x110E}, /* choseong hieuh + jieuj = chieuch */
{phonemes: [0x1161,0x1175], char: 0x1162}, /* jungseong a + i = ae */
{phonemes: [0x1161,0x1165], char: 0x116D}, /* jungseong a + eo = yo */
{phonemes: [0x1165,0x1161], char: 0x116D}, /* jungseong eo + a = yo */
{phonemes: [0x1169,0x1175], char: 0x116C}, /* jungseong o + i = oe */
{phonemes: [0x1169,0x1161], char: 0x116A}, /* jungseong o + a = wa */
{phonemes: [0x1169,0x1166], char: 0x1168}, /* jungseong o + e = ye */
{phonemes: [0x1169,0x1165], char: 0x1163}, /* jungseong o + eo = ya */
{phonemes: [0x1169,0x1169], char: 0x116D}, /* jungseong o + o = yo */
{phonemes: [0x1169,0x116E], char: 0x1172}, /* jungseong o + u = yu */
{phonemes: [0x116A,0x1175], char: 0x116B}, /* jungseong wa + i = wae */
{phonemes: [0x1169,0x1167], char: 0x1164}, /* jungseong o + yeo = yae */
{phonemes: [0x116F,0x1175], char: 0x1170}, /* jungseong weo + i = we */
{phonemes: [0x116E,0x1165], char: 0x116F}, /* jungseong u + eo = weo */
{phonemes: [0x116E,0x1166], char: 0x1170}, /* jungseong u + e = we */
{phonemes: [0x116E,0x1175], char: 0x1171}, /* jungseong u + i = wi */
{phonemes: [0x1173,0x1175], char: 0x1174}, /* jungseong eu + i = eui */
{phonemes: [0x11A8,0x11BA], char: 0x11AA}, /* jongseong gieug + sieus = gieug-sieus */
{phonemes: [0x11AB,0x11AB], char: 0x11AD}, /* jongseong nieun + nieun = nieun-hieuh */
{phonemes: [0x11AB,0x11AF], char: 0x11AC}, /* jongseong nieun + lieul = nieun-jieuj */
{phonemes: [0x11AB,0x11B7], char: 0x11C0}, /* jongseong nieun + mieum = tieut */
{phonemes: [0x11AB,0x11C2], char: 0x11AD}, /* jongseong nieun + hieuh = nieun-hieuh */
{phonemes: [0x11AF,0x11A8], char: 0x11B0}, /* jongseong lieul + gieug = lieul-gieug */
{phonemes: [0x11AF,0x11B7], char: 0x11B1}, /* jongseong lieul + mieum = lieul-mieum */
{phonemes: [0x11AF,0x11B8], char: 0x11B2}, /* jongseong lieul + bieub = lieul-bieub */
{phonemes: [0x11AF,0x11BA], char: 0x11B3}, /* jongseong lieul + sieus = lieul-sieus */
{phonemes: [0x11AF,0x11BC], char: 0x11A8}, /* jongseong lieul + ieung = gieug */
{phonemes: [0x11B7,0x11A8], char: 0x11B0}, /* jongseong mieum + gieug = lieul-gieug */
{phonemes: [0x11B8,0x11BA], char: 0x11B9}, /* jongseong bieub + sieus = bieub-sieus */
{phonemes: [0x11BB,0x11A8], char: 0x11BF}, /* jongseong ssang_sieus + gieug = kieuk */
{phonemes: [0x11BB,0x11AB], char: 0x11C2}, /* jongseong ssang_sieus + nieun = hieuh */
{phonemes: [0x11BB,0x11AF], char: 0x11BD}, /* jongseong ssang_sieus + lieul = jieuj */
{phonemes: [0x11BB,0x11B7], char: 0x11AE}, /* jongseong ssang_sieus + mieum = dieud */
{phonemes: [0x11BB,0x11B8], char: 0x11C1}, /* jongseong ssang_sieus + bieub = pieup */
{phonemes: [0x11BB,0x11BA], char: 0x11BE}, /* jongseong ssang_sieus + sieus = chieuch */
{phonemes: [0x11BB,0x11BC], char: 0x11B6}, /* jongseong ssang_sieus + ieung = lieul-hieuh */
{phonemes: [0x11BC,0x11A8], char: 0x11A9}, /* jongseong ieung + gieug = ssang_egieug */
{phonemes: [0x11BC,0x11AB], char: 0x11AD}, /* jongseong ieung + nieun = nieun-hieuh */
{phonemes: [0x11BC,0x11B7], char: 0x11B4}, /* jongseong ieung + mieum = lieul-tikeut */
{phonemes: [0x11BC,0x11B8], char: 0x11B5}, /* jongseong ieung + bieub = lieul-pieup */
{phonemes: [0x11BC,0x11BA], char: 0x11BB}, /* jongseong ieung + sieus = ssang_sieus */
{phonemes: [0x11BC,0x11BB], char: 0x11B6}, /* jongseong ieung + ssang_sieus = lieul-hieuh */
{phonemes: [0x11C2,0x11A8], char: 0x11BF}, /* jongseong hieuh + gieug = kieuk */
{phonemes: [0x11C2,0x11AE], char: 0x11C0}, /* jongseong hieuh + dieud = tieut */
{phonemes: [0x11C2,0x11B8], char: 0x11C1} /* jongseong hieuh + bieub = pieup */
];
K3_Semoe_2017_moachigi_multikey_abbreviation_table = [
{keys: ['J','K'], chars: [-1]}, /* 기호 확장 상태 ① */
{keys: ['J','L'], chars: [-2]}, /* 기호 확장 상태 ② */
{keys: ['J',':'], chars: [-3]}, /* 기호 확장 상태 ③ */
{keys: ['k','u'], chars: [0xADF8,0xB7EC,0xB098]}, /* 그러나 */
{keys: ['u','w'], chars: [0x11B8,0xB2C8,0xB2E4]}, /* ㅂ니다 */
{keys: [',','u','w'], chars: [0x11B8,0xB2C8,0xB2E4,0x2E,0x20]}, /* ㅂ니다. */
{keys: ['j','w'], chars: [0xC785,0xB2E4,0x2E,0x20]}, /* 입니다. */
{keys: ['h','w'], chars: [0xD569,0xB2C8,0xB2E4,0x2E,0x20]}, /* 합니다. */
{keys: ['h','l','s'], chars: [0xD558,0xC9C0,0xB9CC,0x20]}, /* 하지만 */
{keys: ['m','n'], chars: [0x21,0x20]}, /* ! */
{keys: ['i','u'], chars: [0x2C,0x20]}, /* , */
{keys: ['i','o'], chars: [0x2E,0x20]}, /* . */
{keys: ['u','y'], chars: [0x3F,0x20]}, /* ? */
/* 첫소리 ㄱ 조합 */
{keys: ['k','x'], chars: [0xAD6D,0xAC00]}, /* 국가 */
{keys: ['k','s','x'], chars: [0xAD6D,0xBBFC]}, /* 국민 */
{keys: ['e','k','s'], chars: [0xAC74,0xBB3C]}, /* 건물 */
{keys: ['a','k','s'], chars: [0xACF5,0xAC04]}, /* 공간 */
{keys: ['e','k'], chars: [0xACB0,0xACFC]}, /* 결과 */
{keys: ['k','x','z'], chars: [0xACB0,0xAD6D]}, /* 결국 */
{keys: ['e','k','x'], chars: [0xACB0,0xAD6D]}, /* 결국 */
{keys: ['k','z'], chars: [0xAC1C,0xB150]}, /* 개념 */
{keys: ['c','g','k'], chars: [0xACC4,0xAE09]}, /* 계급 */
{keys: ['k','q'], chars: [0xADF8,0xAC83]}, /* 그것 */
{keys: ['a','k'], chars: [0xAD11,0xACE0]}, /* 광고 */
{keys: [';','j','k','w'], chars: [0xAE4A,0xC774]}, /* 깊이 */
{keys: ['g','k','t'], chars: [0xADF8,0xB140]}, /* 그녀 */
{keys: ['b','f','k'], chars: [0xAD6C,0xB098]}, /* 구나 */
{keys: ['f','k','p'], chars: [0xAD6C,0xB098]}, /* 구나 */
{keys: ['b','f','k','x'], chars: [0xAD6D,0xB0B4]}, /* 국내 */
{keys: ['f','k','p','x'], chars: [0xAD6D,0xB0B4]}, /* 국내 */
{keys: ['g','k','v'], chars: [0xADF8,0xACF3]}, /* 그곳 */
{keys: ['.','g','k'], chars: [0xADF8,0xACF3]}, /* 그곳 */
{keys: ['c','k','t'], chars: [0xACBD,0xACC4]}, /* 경계 */
{keys: ['c','d','k'], chars: [0xAE30,0xACC4]}, /* 기계 */
{keys: ['f','g','k','s'], chars: [0xADF8,0xB9CC]}, /* 그만 */
{keys: ['j','k','x'], chars: [0xAD50,0xC721]}, /* 교육 */
{keys: ['j','k','s','x'], chars: [0xC57D,0xAC04]}, /* 약간 */
{keys: ['j','k','s'], chars: [0xC778,0xAC04]}, /* 인간 */
{keys: ['a','j','k','s'], chars: [0xACF5,0xC5F0]}, /* 공연 */
{keys: ['e','j','k'], chars: [0xC5BC,0xAD74]}, /* 얼굴 */
{keys: ['a','j','k','w'], chars: [0xACF5,0xC5C5]}, /* 공업 */
{keys: ['j','k','w'], chars: [0xAE30,0xC5C5]}, /* 기업 */
{keys: ['j','k','q'], chars: [0xC774,0xAC83]}, /* 이것 */
{keys: ['a','j','k'], chars: [0xACBD,0xC6B0]}, /* 경우 */
{keys: ['j','k','s','z'], chars: [0xAC19,0xC774,0x20]}, /* 같이 */
{keys: ['b','j','k','t'], chars: [0xC5F0,0xAD6C]}, /* 연구 */
{keys: ['j','k','p','t'], chars: [0xC5F0,0xAD6C]}, /* 연구 */
{keys: ['b','e','j','k','t'], chars: [0xACA8,0xC6B8]}, /* 겨울 */
{keys: ['e','j','k','p','t'], chars: [0xACA8,0xC6B8]}, /* 겨울 */
{keys: ['g','j','k','t'], chars: [0xC5F0,0xADF9]}, /* 연극 */
{keys: ['g','j','k','s','t'], chars: [0xC758,0xACAC]}, /* 의견 */
{keys: ['b','f','j','k'], chars: [0xC694,0xAD6C]}, /* 요구 */
{keys: ['f','j','k','p'], chars: [0xC694,0xAD6C]}, /* 요구 */
{keys: ['b','e','f','j','k'], chars: [0xAC1C,0xC6D4]}, /* 개월 */
{keys: ['e','f','j','k','p'], chars: [0xAC1C,0xC6D4]}, /* 개월 */
{keys: ['c','j','k','r'], chars: [0xAED8,0xC11C,0x20]}, /* 께서 */
{keys: ['c','d','j','k','z'], chars: [0xAC8C,0xC784]}, /* 게임 */
{keys: ['f','g','j','k'], chars: [0xC744,0xAE4C,0x3F,0x20]}, /* 을까? */
{keys: ['e','f','g','j','k'], chars: [0xAC00,0xC744]}, /* 가을 */
{keys: ['f','g','j','k','z'], chars: [0xAC00,0xB054]}, /* 가끔 */
{keys: ['g','j','k','r'], chars: [0xAC70,0xC758,0x20]}, /* 거의 */
{keys: ['k','l','x'], chars: [0xC911,0xAD6D]}, /* 중국 */
{keys: ['k','l','s'], chars: [0xCE5C,0xAD6C]}, /* 친구 */
{keys: ['a','k','l','s'], chars: [0xAD1C,0xCC2E]}, /* 괜찮 */
{keys: ['e','k','l'], chars: [0xACBD,0xCC30]}, /* 경찰 */
{keys: ['k','l','z'], chars: [0xC9C0,0xAE08]}, /* 지금 */
{keys: ['k','l','w'], chars: [0xAC11,0xC790,0xAE30,0x20]}, /* 갑자기 */
{keys: ['k','l','q'], chars: [0xAC70,0xC9D3]}, /* 거짓 */
{keys: ['a','k','l'], chars: [0xAC00,0xC7A5,0x20]}, /* 가장 */
{keys: ['f','k','l'], chars: [0xC790,0xAE30]}, /* 자기 */
{keys: ['f','k','l','x'], chars: [0xC791,0xAC00]}, /* 작가 */
{keys: ['f','k','l','r','z'], chars: [0xAC10,0xC815]}, /* 감정 */
{keys: ['a','f','k','l','s'], chars: [0xC7A5,0xAD00]}, /* 장관 */
{keys: ['d','f','k','l'], chars: [0xAC00,0xC9C0]}, /* 가지 */
{keys: ['a','d','f','k','l'], chars: [0xACBD,0xC7C1]}, /* 경쟁 */
{keys: ['k','l','r','x'], chars: [0xAC71,0xC815]}, /* 걱정 */
{keys: ['k','l','r','s'], chars: [0xC870,0xAC74]}, /* 조건 */
{keys: ['k','l','r','z'], chars: [0xAC80,0xCC30]}, /* 검찰 */
{keys: ['a','k','l','r'], chars: [0xAC00,0xC815]}, /* 가정 */
{keys: ['c','k','l'], chars: [0xAD6C,0xCCB4,0xC801]}, /* 구체적 */
{keys: ['k','l','t'], chars: [0xAC00,0xC838]}, /* 가져 */
{keys: ['e','k','l','t'], chars: [0xACB0,0xC815]}, /* 결정 */
{keys: ['a','k','l','t'], chars: [0xACBD,0xC81C]}, /* 경제 */
{keys: ['c','k','l','v'], chars: [0xCCB4,0xACC4]}, /* 체계 */
{keys: ['k','l','v'], chars: [0xCD5C,0xACE0]}, /* 최고 */
{keys: ['k','l','v','x'], chars: [0xAC00,0xC871]}, /* 가족 */
{keys: ['k','l','s','v'], chars: [0xAE30,0xC874]}, /* 기존 */
{keys: ['a','k','l','v'], chars: [0xAC01,0xC885]}, /* 각종 */
{keys: ['.','f','k','l'], chars: [0xACFC,0xC815]}, /* 과정 */
{keys: ['f','k','l','v'], chars: [0xACFC,0xC815]}, /* 과정 */
{keys: ['.','f','k','l','s'], chars: [0xAD00,0xC810]}, /* 관점 */
{keys: ['f','k','l','s','v'], chars: [0xAD00,0xC810]}, /* 관점 */
{keys: ['.','a','f','k','l'], chars: [0xAC15,0xC870]}, /* 강조 */
{keys: ['a','f','k','l','v'], chars: [0xAC15,0xC870]}, /* 강조 */
{keys: ['d','k','l','s','v'], chars: [0xCD5C,0xADFC,0x20]}, /* 최근 */
{keys: ['.','d','k','l','s'], chars: [0xCD5C,0xADFC,0x20]}, /* 최근 */
{keys: ['.','k','l','v'], chars: [0xC885,0xAD50]}, /* 종교 */
{keys: ['f','k','l','r'], chars: [0xC885,0xAD50]}, /* 종교 */
{keys: ['a','f','k','l','r'], chars: [0xAD50,0xC7A5]}, /* 교장 */
{keys: ['.','a','k','l','v'], chars: [0xAD50,0xC7A5]}, /* 교장 */
{keys: ['b','k','l'], chars: [0xAD6C,0xC870]}, /* 구조 */
{keys: ['b','k','l','r'], chars: [0xC804,0xAD6D]}, /* 전국 */
{keys: ['k','l','p','r'], chars: [0xC804,0xAD6D]}, /* 전국 */
{keys: ['b','k','l','s'], chars: [0xAE30,0xC900]}, /* 기준 */
{keys: ['b','k','l','r','s'], chars: [0xC815,0xAD8C]}, /* 정권 */
{keys: ['k','l','p','r','s'], chars: [0xC815,0xAD8C]}, /* 정권 */
{keys: ['b','c','k','l','x'], chars: [0xAD6D,0xC81C]}, /* 국제 */
{keys: ['c','k','l','p','x'], chars: [0xAD6D,0xC81C]}, /* 국제 */
{keys: ['d','k','l','v','x'], chars: [0xADC0,0xC871]}, /* 귀족 */
{keys: ['.','d','k','l','x'], chars: [0xADC0,0xC871]}, /* 귀족 */
{keys: ['b','k','l','v'], chars: [0xADDC,0xC815]}, /* 규정 */
{keys: ['k','l','p','v'], chars: [0xADDC,0xC815]}, /* 규정 */
{keys: ['.','b','k','l'], chars: [0xADDC,0xC815]}, /* 규정 */
{keys: ['d','g','k','l'], chars: [0xADF8,0xB807,0xC9C0]}, /* 그렇지 */
{keys: ['g','k','l','s'], chars: [0xADFC,0xCC98]}, /* 근처 */
{keys: ['g','k','l','z'], chars: [0xC790,0xAE08]}, /* 자금 */
{keys: ['d','k','l'], chars: [0xAE4C,0xC9C0,0x20]}, /* 까지 */
{keys: ['c','f','k','l'], chars: [0xACFC,0xC81C]}, /* 과제 */
/* 첫소리 ㄴ 조합 */
{keys: ['e','u'], chars: [0xADF8,0xB0A0]}, /* 그날 */
{keys: ['u','z'], chars: [0xB0A8,0xC131]}, /* 남성 */
{keys: ['b','f','u'], chars: [0xB204,0xB098]}, /* 누나 */
{keys: ['f','p','u'], chars: [0xB204,0xB098]}, /* 누나 */
{keys: ['g','r','u'], chars: [0xB290,0xB0D0,0x3F,0x20]}, /* 느냐? */
{keys: ['i','s','u'], chars: [0xB294,0xB370]}, /* 는데 */
{keys: ['a','i','u'], chars: [0xB178,0xB3D9]}, /* 노동 */
{keys: ['f','i','u'], chars: [0xB098,0xD0C0]}, /* 나타 */
{keys: ['i','r','u'], chars: [0xB354,0xB2C8,0x20]}, /* 더니 */
{keys: ['i','s','t','u'], chars: [0xB144,0xB300]}, /* 년대 */
{keys: ['i','u','v'], chars: [0xB610,0xB294,0x20]}, /* 또는 */
{keys: ['a','i','u','v'], chars: [0xB3D9,0xB124]}, /* 동네 */
{keys: ['d','i','u'], chars: [0xB2C8,0xB2E4,0x2E,0x20]}, /* 니다. */
/* 첫소리 ㄷ 조합 */
{keys: ['i','x'], chars: [0xB300,0xD559]}, /* 대학 */
{keys: ['i','s'], chars: [0xB2E4,0xB978,0x20]}, /* 다른 */
{keys: ['i','w'], chars: [0xB300,0xB2F5]}, /* 대답 */
{keys: ['i','q'], chars: [0xB4EF,0xC774,0x20]}, /* 듯이 */
{keys: ['a','i'], chars: [0xB2E4,0xC591]}, /* 다양 */
{keys: ['b','f','i'], chars: [0xBB34,0xB300]}, /* 무대 */
{keys: ['f','i','p'], chars: [0xBB34,0xB300]}, /* 무대 */
{keys: ['c','f','i','s'], chars: [0xB2E8,0xCCB4]}, /* 단체 */
{keys: ['f','g','i'], chars: [0xB9CC,0xB4E4]}, /* 만들 */
{keys: ['i','k','x'], chars: [0xAE30,0xB3C5,0xAD50]}, /* 기독교 */
{keys: ['i','k','s'], chars: [0xAC00,0xC6B4,0xB370]}, /* 가운데 */
{keys: ['i','k','x','z'], chars: [0xAE4C,0xB2ED]}, /* 까닭 */
{keys: ['e','i','k','x'], chars: [0xAE4C,0xB2ED]}, /* 까닭 */
{keys: ['i','k','z'], chars: [0xAC10,0xB3C5]}, /* 감독 */
{keys: ['a','i','k'], chars: [0xACF5,0xB3D9]}, /* 공동 */
{keys: ['f','i','k'], chars: [0xB2E4,0xAC00]}, /* 다가 */
{keys: ['c','i','k','s'], chars: [0xB2E8,0xACC4]}, /* 단계 */
{keys: ['e','i','k'], chars: [0xAC08,0xB4F1]}, /* 갈등 */
{keys: ['f','g','i','k'], chars: [0xADF8,0xB54C]}, /* 그때 */
{keys: ['g','i','k','r','s'], chars: [0xAC70,0xB4E0]}, /* 거든 */
{keys: ['i','k','r','s'], chars: [0xB358,0xAC00,0x3F,0x20]}, /* 던가? */
{keys: ['c','i','k','v'], chars: [0xACC4,0xB2E8]}, /* 계단 */
{keys: ['d','f','i','k','v'], chars: [0xACE0,0xB300]}, /* 고대 */
{keys: ['.','d','f','i','k'], chars: [0xACE0,0xB300]}, /* 고대 */
{keys: ['a','i','k','v'], chars: [0xACE0,0xB3D9]}, /* 고통 */
{keys: ['f','i','k','v'], chars: [0xB2E4,0xACE0]}, /* 다고 */
{keys: ['.','f','i','k'], chars: [0xB2E4,0xACE0]}, /* 다고 */
{keys: ['d','i','k','v'], chars: [0xAE30,0xB3C4]}, /* 기도 */
{keys: ['.','d','i','k'], chars: [0xAE30,0xB3C4]}, /* 기도 */
{keys: ['g','i','k'], chars: [0xADF8,0xB300]}, /* 그대 */
{keys: ['g','i','k','s'], chars: [0xADFC,0xB370,0x20]}, /* 근데 */
{keys: ['d','i','k'], chars: [0xAE30,0xB2E4]}, /* 기다 */
{keys: ['i','j','x'], chars: [0xB354,0xC6B1,0x20]}, /* 더욱 */
{keys: ['i','j','s'], chars: [0xB54C,0xBB38]}, /* 때문 */
{keys: ['a','i','j','s'], chars: [0xB3D9,0xC548]}, /* 동안 */
{keys: ['e','i','j','x'], chars: [0xB3C5,0xC77C]}, /* 독일 */
{keys: ['i','j','x','z'], chars: [0xB3C5,0xC77C]}, /* 독일 */
{keys: ['i','z'], chars: [0xB2E4,0xC74C]}, /* 다음 */
{keys: ['a','i','j'], chars: [0xC6B4,0xB3D9]}, /* 운동 */
{keys: [';','i','j','s'], chars: [0xC5B4,0xB5BB]}, /* 어떻 */
{keys: ['c','g','i','j'], chars: [0xC740,0xB370]}, /* 은데 */
{keys: ['c','f','i','j'], chars: [0xC5D0,0xB2E4,0x20]}, /* 에다 */
{keys: ['f','g','j'], chars: [0xC544,0xB4E4]}, /* 아들 */
{keys: ['i','m','x'], chars: [0xB3C4,0xB85D,0x20]}, /* 도록 */
{keys: ['e','i','m'], chars: [0xB2EC,0xB9AC]}, /* 달리 */
{keys: ['f','i','m'], chars: [0xB530,0xB77C,0x20]}, /* 따라 */
{keys: ['e','f','i','m'], chars: [0xB2EC,0xB77C]}, /* 달라 */
{keys: ['d','f','i','m'], chars: [0xB300,0xB85C,0x20]}, /* 대로 */
{keys: ['i','m','r'], chars: [0xB530,0xB77C,0xC11C,0x20]}, /* 따라서 */
{keys: ['c','i','m'], chars: [0xD154,0xB808,0xBE44,0xC804]}, /* 텔레비전 */
{keys: ['i','m','v'], chars: [0xB77C,0xB3C4,0x20]}, /* 라도 */
{keys: ['f','i','m','r'], chars: [0xB354,0xB77C,0x2E,0x20]}, /* 더라. */
{keys: ['g','i','m'], chars: [0xADF8,0xB300,0xB85C,0x20]}, /* 그대로 */
{keys: ['d','i','m'], chars: [0xB2E4,0xB9AC]}, /* 다리 */
{keys: ['g','i','m','r'], chars: [0xB4DC,0xB7EC]}, /* 드러 */
{keys: ['i','n','x'], chars: [0xC18D,0xB3C4]}, /* 속도 */
{keys: ['i','n','s'], chars: [0xB2F9,0xC2E0]}, /* 당신 */
{keys: ['a','i','n'], chars: [0xB2F9,0xC2DC]}, /* 당시 */
{keys: ['f','i','n'], chars: [0xC2DC,0xB2E4,0x2E,0x20]}, /* 시다. */
{keys: ['a','f','i','n'], chars: [0xB300,0xC0C1]}, /* 대상 */
{keys: ['d','f','i','n'], chars: [0xB2E4,0xC2DC,0x20]}, /* 다시 */
{keys: ['a','d','f','i','n'], chars: [0xB3D9,0xC0DD]}, /* 동생 */
{keys: ['i','n','v'], chars: [0xB3C4,0xC2DC]}, /* 도시 */
{keys: ['a','i','n','v'], chars: [0xB3D9,0xC2DC]}, /* 동시 */
{keys: ['b','i','n'], chars: [0xC218,0xB3C4]}, /* 수도 */
{keys: ['d','i','n'], chars: [0xC2DC,0xB300]}, /* 시대 */
{keys: ['d','i','n','s'], chars: [0xB300,0xC2E0]}, /* 대신 */
{keys: ['d','f','i','l','x'], chars: [0xB300,0xCC45]}, /* 대책 */
{keys: ['i','l','s'], chars: [0xB4E0,0xC9C0,0x20]}, /* 든지 */
{keys: ['i','l','w'], chars: [0xC9D1,0xB2E8]}, /* 집단 */
{keys: ['f','i','l','v'], chars: [0xCCAD,0xC640,0xB300]}, /* 청와대 */
{keys: ['.','f','i','l'], chars: [0xCCAD,0xC640,0xB300]}, /* 청와대 */
{keys: ['f','i','l'], chars: [0xC790,0xB3D9,0xCC28]}, /* 자동차 */
{keys: ['f','i','l','s'], chars: [0xB2E8,0xC9C0]}, /* 단지 */
{keys: ['d','f','i','l'], chars: [0xC81C,0xB300,0xB85C,0x20]}, /* 제대로 */
{keys: ['i','l','r','s'], chars: [0xC804,0xB3D9]}, /* 전통 */
{keys: ['a','i','l','r'], chars: [0xC815,0xB2F9]}, /* 정당 */
{keys: ['c','i','l'], chars: [0xC81C,0xB3C4]}, /* 제도 */
{keys: ['i','l','v'], chars: [0xD1A0,0xC9C0]}, /* 토지 */
{keys: ['b','i','l'], chars: [0xD22C,0xC7C1]}, /* 투쟁 */
{keys: ['a','b','i','l'], chars: [0xB300,0xC911]}, /* 대중 */
{keys: ['b','f','i','l'], chars: [0xD22C,0xC790]}, /* 투자 */
{keys: ['f','i','l','p'], chars: [0xD22C,0xC790]}, /* 투자 */
{keys: ['g','i','l','x'], chars: [0xD2B9,0xC9D5]}, /* 특징 */
{keys: ['a','g','i','l'], chars: [0xB4F1,0xC7A5]}, /* 등장 */
{keys: ['d','i','l'], chars: [0xC9C0,0xB3C4]}, /* 지도 */
{keys: ['c','f','i','l'], chars: [0xB300,0xCCB4]}, /* 대체 */
/* 첫소리 ㄹ 조합 */
{keys: ['m','s'], chars: [0xC774,0xB7F0,0x20]}, /* 이런 */
{keys: ['e','m'], chars: [0xB2EC,0xB7EC]}, /* 달러 */
{keys: ['m','z'], chars: [0xC5EC,0xB984]}, /* 여름 */
{keys: ['k','m','x'], chars: [0xAD8C,0xB825]}, /* 권력 */
{keys: ['k','m','s'], chars: [0xADF8,0xB7F0,0x20]}, /* 그런 */
{keys: ['e','k','m'], chars: [0xC774,0xB370,0xC62C,0xB85C,0xAE30]}, /* 이데올로기 */
{keys: ['k','m','z'], chars: [0xADF8,0xB7A8]}, /* 그램 */
{keys: ['k','m','w'], chars: [0xADF8,0xB8F9]}, /* 그룹 */
{keys: [';','k','m','s'], chars: [0xADF8,0xB807]}, /* 그렇 */
{keys: ['f','k','m'], chars: [0xADF8,0xB7EC,0xB098,0x20]}, /* 그러나 */
{keys: ['f','k','m','x'], chars: [0xAC00,0xB77D]}, /* 가락 */
{keys: ['d','f','k','m'], chars: [0xADF8,0xB798]}, /* 그래 */
{keys: ['k','m','t','v'], chars: [0xACE0,0xB824]}, /* 고려 */
{keys: ['.','k','m','t'], chars: [0xACE0,0xB824]}, /* 고려 */
{keys: ['k','m','r'], chars: [0xADF8,0xB798,0xC11C,0x20]}, /* 그래서 */
{keys: ['k','m','r','z'], chars: [0xADF8,0xB7FC,0x20]}, /* 그럼 */
{keys: ['c','k','m'], chars: [0xADF8,0xB7F0,0xB370,0x20]}, /* 그런데 */
{keys: ['k','m','t'], chars: [0xB824,0xACE0,0x20]}, /* 려고 */
{keys: ['k','m','v'], chars: [0xADF8,0xB9AC,0xACE0,0x20]}, /* 그리고 */
{keys: ['k','m','v','x'], chars: [0xAE30,0xB85D]}, /* 기록 */
{keys: ['f','k','m','v'], chars: [0xB77C,0xACE0]}, /* 라고 */
{keys: ['.','f','k','m'], chars: [0xB77C,0xACE0]}, /* 라고 */
{keys: ['k','m','s','t'], chars: [0xAD00,0xB828]}, /* 관련 */
{keys: ['f','k','m','s','v'], chars: [0xAD00,0xB9AC]}, /* 관리 */
{keys: ['.','f','k','m','s'], chars: [0xAD00,0xB9AC]}, /* 관리 */
{keys: ['b','k','m'], chars: [0xACE0,0xAD6C,0xB824]}, /* 고구려 */
{keys: ['g','k','m'], chars: [0xADF8,0xB798,0xB3C4,0x20]}, /* 그래도 */
{keys: ['d','g','k','m'], chars: [0xADF8,0xB9AC]}, /* 그리 */
{keys: ['b','g','k','m'], chars: [0xAD6C,0xB984]}, /* 구름 */
{keys: ['g','k','m','p'], chars: [0xAD6C,0xB984]}, /* 구름 */
{keys: ['d','k','m','z'], chars: [0xADF8,0xB9BC]}, /* 그림 */
{keys: ['f','g','k','m'], chars: [0xAC00,0xB974]}, /* 가르 */
{keys: ['m','s','y'], chars: [0xBB3C,0xB860,0x20]}, /* 물론 */
{keys: ['e','m','y'], chars: [0xBA40,0xB9AC,0x20]}, /* 멀리 */
{keys: ['a','m','y'], chars: [0xBA85,0xB839]}, /* 명령 */
{keys: ['f','m','s','y'], chars: [0xB9C8,0xB828]}, /* 마련 */
{keys: ['e','f','m','y'], chars: [0xC57C,0xB9D0,0xB85C,0x20]}, /* 야말로 */
{keys: ['m','r','y'], chars: [0xBA38,0xB9AC]}, /* 머리 */
{keys: ['m','t','y'], chars: [0xB824,0xBA74,0x20]}, /* 려면 */
{keys: ['m','s','t','y'], chars: [0xB77C,0xBA74]}, /* 라면 */
{keys: ['b','m','y'], chars: [0xC544,0xBB34,0xB7F0,0x20]}, /* 아무런 */
{keys: ['b','e','m','y'], chars: [0xBB3C,0xB9AC]}, /* 물리 */
{keys: ['b','d','m','y'], chars: [0xBB34,0xB9AC]}, /* 무리 */
{keys: ['d','m','p','y'], chars: [0xBB34,0xB9AC]}, /* 무리 */
{keys: ['g','m','y'], chars: [0xBBC0,0xB85C,0x20]}, /* 므로 */
{keys: ['m','o','x'], chars: [0xBE44,0xB85D,0x20]}, /* 비록 */
{keys: ['e','m','o'], chars: [0xBE68,0xB9AC,0x20]}, /* 빨리 */
{keys: ['m','o','z'], chars: [0xBC14,0xB78C]}, /* 바람 */
{keys: ['m','o','q'], chars: [0xBE44,0xB86F]}, /* 비롯 */
{keys: ['a','m','o'], chars: [0xD504,0xB791,0xC2A4]}, /* 프랑스 */
{keys: ['f','m','o'], chars: [0xBC14,0xB77C]}, /* 바라 */
{keys: ['m','o','r','w'], chars: [0xBC95,0xB960]}, /* 법률 */
{keys: ['e','m','o','t'], chars: [0xBCC4,0xB85C,0x20]}, /* 별로 */
{keys: ['m','o','v'], chars: [0xBC14,0xB85C,0x20]}, /* 바로 */
{keys: ['l','m','s'], chars: [0xC804,0xB7B5]}, /* 전략 */
{keys: ['l','m','z'], chars: [0xCC98,0xB7FC,0x20]}, /* 처럼 */
{keys: ['a','l','m'], chars: [0xC885,0xB958]}, /* 종류 */
{keys: ['f','l','m'], chars: [0xC790,0xB8CC]}, /* 자료 */
{keys: ['d','f','l','m'], chars: [0xC7AC,0xB8CC]}, /* 재료 */
{keys: ['l','m','r'], chars: [0xCC98,0xB9AC]}, /* 처리 */
{keys: ['c','l','m','v'], chars: [0xCC28,0xB840]}, /* 차례 */
{keys: ['b','l','m'], chars: [0xC8FC,0xB85C,0x20]}, /* 주로 */
{keys: ['d','l','m'], chars: [0xC790,0xB9AC]}, /* 자리 */
{keys: ['a','h','m'], chars: [0xD6CC,0xB96D]}, /* 훌륭 */
{keys: ['f','h','m'], chars: [0xD558,0xB8E8]}, /* 하루 */
{keys: ['h','m','r'], chars: [0xD5C8,0xB9AC]}, /* 허리 */
{keys: ['g','m','o'], chars: [0xD504,0xB85C]}, /* 프로 */
{keys: ['d','h','m'], chars: [0xCC28,0xB77C,0xB9AC,0x20]}, /* 차라리 */
{keys: ['m','u','x'], chars: [0xB178,0xB825]}, /* 노력 */
{keys: ['a','m','u'], chars: [0xB2A5,0xB825]}, /* 능력 */
{keys: ['f','m','u'], chars: [0xB098,0xB77C]}, /* 나라 */
{keys: ['d','f','m','u'], chars: [0xB0B4,0xB824]}, /* 내려 */
{keys: ['m','u','v'], chars: [0xB178,0xB798]}, /* 노래 */
{keys: ['m','u','z'], chars: [0xB098,0xB984]}, /* 나름 */
/* 첫소리 ㅁ 조합 */
{keys: ['x','y'], chars: [0xBBF8,0xAD6D]}, /* 미국 */
{keys: ['s','x','y'], chars: [0xB9CC,0xC57D]}, /* 만약 */
{keys: ['s','y'], chars: [0xBB38,0xD654]}, /* 문화 */
{keys: ['e','s','y'], chars: [0xBB3C,0xAC74]}, /* 물건 */
{keys: ['a','s','y'], chars: [0xB9CE,0xC774,0x20]}, /* 많이 */
{keys: [';','y','z'], chars: [0xBBFF,0xC74C]}, /* 믿음 */
{keys: ['e','y'], chars: [0xBB3C,0xC9C8]}, /* 물질 */
{keys: ['y','z'], chars: [0xB9C8,0xC74C]}, /* 마음 */
{keys: ['w','y'], chars: [0xC5C5,0xBB34]}, /* 업무 */
{keys: ['q','y'], chars: [0xBB34,0xC5C7]}, /* 무엇 */
{keys: [';','y'], chars: [0xB9DB,0xC788]}, /* 맛있 */
{keys: ['a','y'], chars: [0xBAA8,0xC591]}, /* 모양 */
{keys: ['b','f','y'], chars: [0xB9E4,0xC6B0,0x20]}, /* 매우 */
{keys: ['f','p','y'], chars: [0xB9E4,0xC6B0,0x20]}, /* 매우 */
{keys: ['g','v','y'], chars: [0xBAA8,0xB4E0,0x20]}, /* 모든 */
{keys: ['.','g','y'], chars: [0xBAA8,0xB4E0,0x20]}, /* 모든 */
{keys: ['c','d','y'], chars: [0xBA54,0xC77C]}, /* 메일 */
{keys: ['f','g','y'], chars: [0xB9C8,0xC744]}, /* 마을 */
{keys: ['o','x','y'], chars: [0xBAA9,0xD45C]}, /* 목표 */
{keys: ['a','o','y'], chars: [0xBD84,0xBA85]}, /* 분명 */
{keys: ['o','s','y'], chars: [0xBC18,0xBA74]}, /* 반면 */
{keys: ['b','o','y'], chars: [0xBD80,0xBAA8]}, /* 부모 */
{keys: ['l','x','y'], chars: [0xBBFC,0xC871]}, /* 민족 */
{keys: ['l','s','x','y'], chars: [0xB9CC,0xC871]}, /* 만족 */
{keys: ['l','s','y'], chars: [0xC9C0,0xB9CC,0x20]}, /* 지만 */
{keys: ['e','l','y'], chars: [0xC815,0xB9D0]}, /* 정말 */
{keys: ['l','y','z'], chars: [0xB9C8,0xCE68,0x20]}, /* 마침 */
{keys: ['f','l','s','y'], chars: [0xB9C8,0xCC2C,0xAC00,0xC9C0]}, /* 마찬가지 */
{keys: ['f','l','x','y'], chars: [0xB9C8,0xC9C0,0xB9C9]}, /* 마지막 */
{keys: ['d','f','l','y'], chars: [0xC7AC,0xBBF8]}, /* 재미 */
{keys: ['l','r','y'], chars: [0xB9C8,0xC800,0x20]}, /* 마저 */
{keys: ['l','r','s','y'], chars: [0xBA3C,0xC800]}, /* 먼저 */
{keys: ['c','l','y'], chars: [0xBB38,0xC81C]}, /* 문제 */
{keys: ['l','t','y'], chars: [0xBA70,0xCE60]}, /* 며칠 */
{keys: ['l','s','t','y'], chars: [0xC790,0xBA74,0x20]}, /* 자면 */
{keys: ['l','v','x','y'], chars: [0xBAA9,0xC801]}, /* 목적 */
{keys: ['b','l','y'], chars: [0xC8FC,0xBBFC]}, /* 주민 */
{keys: ['b','l','r','s','y'], chars: [0xC804,0xBB38]}, /* 전문 */
{keys: ['l','p','r','s','y'], chars: [0xC804,0xBB38]}, /* 전문 */
{keys: ['d','l','y'], chars: [0xB9C8,0xCE58,0x20]}, /* 마치 */
{keys: ['d','l','s','y'], chars: [0xBBFC,0xC8FC]}, /* 민주 */
{keys: ['d','e','l','y'], chars: [0xC9C8,0xBB38]}, /* 질문 */
{keys: ['a','b','l','y'], chars: [0xBBFC,0xC911]}, /* 민중 */
{keys: ['e','u','y'], chars: [0xB208,0xBB3C]}, /* 눈물 */
{keys: ['a','u','y'], chars: [0xB18D,0xBBFC]}, /* 농민 */
{keys: ['b','f','u','y'], chars: [0xB098,0xBB34]}, /* 나무 */
{keys: ['f','p','u','y'], chars: [0xB098,0xBB34]}, /* 나무 */
{keys: ['r','u','y'], chars: [0xB108,0xBB34,0x20]}, /* 너무 */
{keys: ['b','u','y'], chars: [0xC8FC,0xBA38,0xB2C8]}, /* 주머니 */
/* 첫소리 ㅂ 조합 */
{keys: ['a','o','x'], chars: [0xBC16,0xC5D0,0x20]}, /* 밖에 */
{keys: ['o','s'], chars: [0xBD80,0xBD84]}, /* 부분 */
{keys: [';','o','z'], chars: [0xBC1B,0xC544]}, /* 받아 */
{keys: ['e','o'], chars: [0xBC1C,0xD45C]}, /* 발표 */
{keys: ['e','o','w'], chars: [0xBD88,0xBC95]}, /* 불법 */
{keys: ['o','w'], chars: [0xBC29,0xBC95]}, /* 방법 */
{keys: ['e','k','o'], chars: [0xAC1C,0xBC1C]}, /* 개발 */
{keys: ['a','k','o'], chars: [0xACF5,0xBD80]}, /* 공부 */
{keys: ['e','f','k','o'], chars: [0xBC1C,0xACAC]}, /* 발견 */
{keys: ['k','o','r'], chars: [0xCEE4,0xD53C]}, /* 커피 */
{keys: ['a','k','o','t'], chars: [0xD3C9,0xAC00]}, /* 평가 */
{keys: ['k','o','s','v'], chars: [0xAE30,0xBCF8]}, /* 기본 */
{keys: ['a','k','o','v'], chars: [0xACF5,0xD3EC]}, /* 공포 */
{keys: ['f','k','o','v'], chars: [0xBD88,0xACFC]}, /* 불과 */
{keys: ['.','f','k','o'], chars: [0xBD88,0xACFC]}, /* 불과 */
{keys: ['b','k','o'], chars: [0xBD88,0xAD6C]}, /* 불구 */
{keys: ['b','e','k','o'], chars: [0xBD88,0xAD50]}, /* 불교 */
{keys: ['d','k','o'], chars: [0xAE30,0xBD84]}, /* 기분 */
{keys: ['i','o','x'], chars: [0xD2B9,0xBCC4]}, /* 특별 */
{keys: ['i','o','s'], chars: [0xB300,0xBD80,0xBD84]}, /* 대부분 */
{keys: ['e','i','o'], chars: [0xBC1C,0xB2EC]}, /* 발달 */
{keys: ['i','o','z'], chars: [0xB2F4,0xBC30]}, /* 담배 */
{keys: ['a','i','o'], chars: [0xBC14,0xD0D5]}, /* 바탕 */
{keys: ['f','i','o'], chars: [0xBC14,0xB2E4]}, /* 바다 */
{keys: ['g','i','o','s'], chars: [0xBC18,0xB4DC,0xC2DC,0x20]}, /* 반드시 */
{keys: ['d','f','i','o','s'], chars: [0xBC18,0xB300]}, /* 반대 */
{keys: ['i','o','r'], chars: [0xD37C,0xC13C,0xD2B8]}, /* 퍼센트 */
{keys: ['i','o','v'], chars: [0xBCF4,0xB2E4,0x20]}, /* 보다 */
{keys: ['f','i','o','s','v'], chars: [0xBC18,0xB3C4]}, /* 반도 */
{keys: ['.','f','i','o','s'], chars: [0xBC18,0xB3C4]}, /* 반도 */
{keys: ['f','i','o','r'], chars: [0xB300,0xD45C]}, /* 대표 */
{keys: ['.','i','o','v'], chars: [0xB300,0xD45C]}, /* 대표 */
{keys: ['b','i','o'], chars: [0xBD80,0xD130,0x20]}, /* 부터 */
{keys: ['g','i','o'], chars: [0xBD80,0xB4DC]}, /* 부드 */
{keys: ['d','g','i','o'], chars: [0xD2F0,0xBE0C,0xC774]}, /* 티브이 */
{keys: ['j','o','x'], chars: [0xD30C,0xC545]}, /* 파악 */
{keys: ['j','o','s'], chars: [0xC774,0xBC88,0x20]}, /* 이번 */
{keys: ['e','j','o','s'], chars: [0xC77C,0xBC18]}, /* 일반 */
{keys: ['a','j','o','s'], chars: [0xBC29,0xC548]}, /* 방안 */
{keys: ['e','j','o'], chars: [0xC77C,0xBCF8]}, /* 일본 */
{keys: ['j','o','q'], chars: [0xBC97,0xC5B4]}, /* 벗어 */
{keys: ['a','j','o'], chars: [0xBCD1,0xC6D0]}, /* 병원 */
{keys: ['b','f','j','o'], chars: [0xBC30,0xC6B0]}, /* 배우 */
{keys: ['f','j','o','p'], chars: [0xBC30,0xC6B0]}, /* 배우 */
{keys: ['n','o','x'], chars: [0xC0C8,0xBCBD]}, /* 새벽 */
{keys: ['n','o','s'], chars: [0xBD80,0xC0B0]}, /* 부산 */
{keys: ['e','n','o'], chars: [0xC0B4,0xD3B4]}, /* 살펴 */
{keys: ['n','o','q'], chars: [0xBE44,0xC2B7]}, /* 비슷 */
{keys: ['a','n','o'], chars: [0xBC29,0xC2DD]}, /* 방식 */
{keys: ['e','f','n','o'], chars: [0xBC1C,0xC0DD]}, /* 발생 */
{keys: ['a','f','n','o'], chars: [0xBC29,0xC1A1]}, /* 방송 */
{keys: ['n','o','r','s'], chars: [0xC120,0xBC30]}, /* 선배 */
{keys: ['e','n','o','r'], chars: [0xBC8C,0xC368,0x20]}, /* 벌써 */
{keys: ['n','o','v'], chars: [0xC18C,0xBE44]}, /* 소비 */
{keys: ['b','n','o'], chars: [0xBD80,0xC0C1]}, /* 부상 */
{keys: ['b','n','o','s'], chars: [0xBD84,0xC11D]}, /* 분석 */
{keys: ['g','n','o','r'], chars: [0xBC84,0xC2A4]}, /* 버스 */
{keys: ['l','o','x'], chars: [0xC791,0xD488]}, /* 작품 */
{keys: ['l','o','s'], chars: [0xC8FC,0xBCC0]}, /* 주변 */
{keys: ['e','l','o'], chars: [0xBC1C,0xC804]}, /* 발전 */
{keys: ['l','o','z'], chars: [0xBC29,0xCE68]}, /* 방침 */
{keys: ['a','l','o'], chars: [0xC815,0xBD80]}, /* 정부 */
{keys: ['f','l','o'], chars: [0xC544,0xBC84,0xC9C0]}, /* 아버지 */
{keys: ['d','f','l','o'], chars: [0xC9C0,0xBC30]}, /* 지배 */
{keys: ['d','f','l','o','x'], chars: [0xBC31,0xC81C]}, /* 백제 */
{keys: ['l','o','r','s'], chars: [0xBC88,0xC9F8]}, /* 번째 */
{keys: ['e','f','l','o','r'], chars: [0xC7AC,0xBC8C]}, /* 재벌 */
{keys: ['l','o','v'], chars: [0xC815,0xBCF4]}, /* 정보 */
{keys: ['c','l','o'], chars: [0xC81C,0xD488]}, /* 제품 */
{keys: ['l','o','s','t'], chars: [0xD3B8,0xC9C0]}, /* 편지 */
{keys: ['b','l','o','x'], chars: [0xBD80,0xC871]}, /* 부족 */
{keys: ['l','o','s','v'], chars: [0xC790,0xBCF8]}, /* 자본 */
{keys: ['b','l','o','s'], chars: [0xC900,0xBE44]}, /* 준비 */
{keys: ['b','f','l','o'], chars: [0xBD80,0xC790]}, /* 부자 */
{keys: ['f','l','o','p'], chars: [0xBD80,0xC790]}, /* 부자 */
{keys: ['b','l','o','r'], chars: [0xBD80,0xC815]}, /* 부정 */
{keys: ['l','o','p','r'], chars: [0xBD80,0xC815]}, /* 부정 */
{keys: ['a','f','l','o'], chars: [0xC9C0,0xBC29]}, /* 지방 */
{keys: ['o','u','z'], chars: [0xB0A8,0xD3B8]}, /* 남편 */
/* 첫소리 ㅅ 조합 */
{keys: ['n','x'], chars: [0xC2DD,0xC0AC]}, /* 식사 */
{keys: ['n','s'], chars: [0xC120,0xC0DD]}, /* 선생 */
{keys: ['a','n','s'], chars: [0xC0DD,0xC0B0]}, /* 생산 */
{keys: ['e','n'], chars: [0xC0AC,0xC2E4]}, /* 사실 */
{keys: ['n','z'], chars: [0xC0AC,0xB78C]}, /* 사람 */
{keys: ['n','w'], chars: [0xC2B5,0xB2C8,0xB2E4,0x2E,0x20]}, /* 습니다. */
{keys: ['a','n'], chars: [0xC138,0xC0C1]}, /* 세상 */
{keys: ['b','f','n'], chars: [0xC218,0xC0AC]}, /* 수사 */
{keys: ['f','n','p'], chars: [0xC218,0xC0AC]}, /* 수사 */
{keys: ['c','f','n'], chars: [0xC138,0xC694]}, /* 세요 */
{keys: ['k','n','x'], chars: [0xC0DD,0xAC01]}, /* 생각 */
{keys: ['k','n','s'], chars: [0xC2DC,0xAC04]}, /* 시간 */
{keys: ['e','k','n'], chars: [0xAE30,0xC220]}, /* 기술 */
{keys: ['k','n','x','z'], chars: [0xC0BC,0xAD6D]}, /* 삼국 */
{keys: ['k','n','z'], chars: [0xAD00,0xC2EC]}, /* 관심 */
{keys: ['a','k','n'], chars: [0xC131,0xACA9]}, /* 성격 */
{keys: ['f','k','n','s'], chars: [0xC0AC,0xAC74]}, /* 사건 */
{keys: ['f','k','n','z'], chars: [0xAC10,0xC0AC]}, /* 감사 */
{keys: ['a','f','k','n'], chars: [0xC0C1,0xAD00]}, /* 상관 */
{keys: ['f','k','n'], chars: [0xAE30,0xC0AC]}, /* 기사 */
{keys: ['d','f','k','n','x'], chars: [0xC2DC,0xAC01]}, /* 시각 */
{keys: ['a','d','f','k','n'], chars: [0xAE30,0xC0C1]}, /* 기상 */
{keys: ['k','n','r'], chars: [0xACE0,0xC11C,0x20]}, /* 고서 */
{keys: ['k','n','r','s'], chars: [0xC120,0xAC70]}, /* 선거 */
{keys: ['e','k','n','r'], chars: [0xAC74,0xC124]}, /* 건설 */
{keys: ['a','k','n','r'], chars: [0xC131,0xACBD]}, /* 성경 */
{keys: ['c','k','n'], chars: [0xC138,0xACC4]}, /* 세계 */
{keys: ['e','k','n','t'], chars: [0xACB0,0xC2EC]}, /* 결심 */
{keys: ['a','k','n','t'], chars: [0xACBD,0xC0C1]}, /* 경상 */
{keys: ['k','n','v','x'], chars: [0xACC4,0xC18D]}, /* 계속 */
{keys: ['k','n','v'], chars: [0xC0AC,0xACE0]}, /* 사고 */
{keys: ['a','k','n','v'], chars: [0xC131,0xACF5]}, /* 성공 */
{keys: ['a','f','k','n','v'], chars: [0xACF5,0xC0AC]}, /* 공사 */
{keys: ['.','a','f','k','n'], chars: [0xACF5,0xC0AC]}, /* 공사 */
{keys: ['f','k','n','r'], chars: [0xAD50,0xC218]}, /* 교수 */
{keys: ['.','k','n','v'], chars: [0xAD50,0xC218]}, /* 교수 */
{keys: ['a','b','k','n'], chars: [0xAD6C,0xC131]}, /* 구성 */
{keys: ['f','k','n','r'], chars: [0xAD50,0xC0AC]}, /* 교사 */
{keys: ['.','k','n','v'], chars: [0xAD50,0xC0AC]}, /* 교사 */
{keys: ['b','k','n','s'], chars: [0xC21C,0xAC04]}, /* 순간 */
{keys: ['b','k','n','v'], chars: [0xC218,0xACE0]}, /* 수고 */
{keys: ['k','n','p','v'], chars: [0xC218,0xACE0]}, /* 수고 */
{keys: ['.','b','k','n'], chars: [0xC218,0xACE0]}, /* 수고 */
{keys: ['g','k','n'], chars: [0xC4F0,0xB808,0xAE30]}, /* 쓰레기 */
{keys: ['e','g','k','n'], chars: [0xAE00,0xC384]}, /* 글쎄 */
{keys: ['g','k','n','z'], chars: [0xAC00,0xC2B4]}, /* 가슴 */
{keys: ['c','d','k','n'], chars: [0xC138,0xAE30]}, /* 세기 */
{keys: ['d','k','n','s'], chars: [0xC2E0,0xACBD]}, /* 신경 */
{keys: ['d','k','n','z'], chars: [0xC2EC,0xAC01]}, /* 심각 */
{keys: ['c','k','n','r'], chars: [0xC5D0,0xAC8C,0xC11C,0x20]}, /* 에게서 */
{keys: ['c','k','n','v'], chars: [0xC2DC,0xACC4]}, /* 시계 */
{keys: ['.','c','k','n'], chars: [0xC2DC,0xACC4]}, /* 시계 */
{keys: ['n','s','u'], chars: [0xC544,0xB098,0xC6B4,0xC11C]}, /* 아나운서 */
{keys: ['n','u','z'], chars: [0xC120,0xC0DD,0xB2D8]}, /* 선생님 */
{keys: ['a','n','u'], chars: [0xAC00,0xB2A5,0xC131]}, /* 가능성 */
{keys: ['f','n','u'], chars: [0xC0AC,0xB0B4]}, /* 사내 */
{keys: ['d','f','n','u','z'], chars: [0xB0C4,0xC0C8]}, /* 냄새 */
{keys: ['n','t','u'], chars: [0xC18C,0xB140]}, /* 소녀 */
{keys: ['n','s','t','u'], chars: [0xC18C,0xB144]}, /* 소년 */
{keys: ['n','s','u','v'], chars: [0xC190,0xB2D8]}, /* 손님 */
{keys: ['a','n','u','v'], chars: [0xB18D,0xC0AC]}, /* 농사 */
{keys: ['b','n','u','v'], chars: [0xB274,0xC2A4]}, /* 뉴스 */
{keys: ['.','b','n','u'], chars: [0xB274,0xC2A4]}, /* 뉴스 */
{keys: ['m','n','x'], chars: [0xC138,0xB825]}, /* 세력 */
{keys: ['m','n','s'], chars: [0xC2E0,0xB77C]}, /* 신라 */
{keys: ['a','m','n'], chars: [0xC0AC,0xB791]}, /* 사랑 */
{keys: ['f','m','n'], chars: [0xC0AC,0xB77C]}, /* 사라 */
{keys: ['d','f','m','n'], chars: [0xC0C8,0xB85C,0x20]}, /* 새로 */
{keys: ['m','n','r','v'], chars: [0xC11C,0xB85C,0x20]}, /* 서로 */
{keys: ['.','m','n','r'], chars: [0xC11C,0xB85C,0x20]}, /* 서로 */
{keys: ['m','n','r'], chars: [0xB85C,0xC368,0x20]}, /* 로써 */
{keys: ['m','n','s','t'], chars: [0xC18C,0xB828]}, /* 소련 */
{keys: ['m','n','v'], chars: [0xB85C,0xC11C,0x20]}, /* 로서 */
{keys: ['b','m','n'], chars: [0xC218,0xB85D,0x20]}, /* 수록 */
{keys: ['b','d','m','n'], chars: [0xC218,0xB9AC]}, /* 수리 */
{keys: ['d','m','n','p'], chars: [0xC218,0xB9AC]}, /* 수리 */
{keys: ['g','m','n'], chars: [0xC2A4,0xC2A4,0xB85C]}, /* 스스로 */
{keys: ['d','m','n'], chars: [0xC18C,0xB9AC]}, /* 소리 */
{keys: ['n','x','y'], chars: [0xBAA9,0xC18C,0xB9AC]}, /* 목소리 */
{keys: ['n','s','y'], chars: [0xBB34,0xC2A8,0x20]}, /* 무슨 */
{keys: ['e','n','s','y'], chars: [0xC120,0xBB3C]}, /* 선물 */
{keys: ['e','n','y'], chars: [0xB9D0,0xC500]}, /* 말씀 */
{keys: ['n','x','y','z'], chars: [0xBAA9,0xC228]}, /* 목숨 */
{keys: ['n','w','y'], chars: [0xBAA8,0xC2B5]}, /* 모습 */
{keys: ['a','n','y'], chars: [0xC0DD,0xBA85]}, /* 생명 */
{keys: ['e','n','r','y'], chars: [0xC124,0xBA85]}, /* 설명 */
{keys: ['n','s','t','y'], chars: [0xBA74,0xC11C,0x20]}, /* 면서 */
{keys: ['b','d','n','y'], chars: [0xBB34,0xC2DC]}, /* 무시 */
{keys: ['d','n','p','y'], chars: [0xBB34,0xC2DC]}, /* 무시 */
{keys: ['d','n','y'], chars: [0xC2DC,0xBBFC]}, /* 시민 */
{keys: ['d','n','x','y'], chars: [0xC2DD,0xBB3C]}, /* 식물 */
{keys: ['d','n','s','y'], chars: [0xC2E0,0xBB38]}, /* 신문 */
{keys: ['h','n','x'], chars: [0xD559,0xC0DD]}, /* 학생 */
{keys: ['h','n','s'], chars: [0xD604,0xC0C1]}, /* 현상 */
{keys: ['e','h','n'], chars: [0xD6E8,0xC52C,0x20]}, /* 훨씬 */
{keys: ['e','h','n','x'], chars: [0xD655,0xC2E4]}, /* 확실 */
{keys: ['h','n','x','z'], chars: [0xD655,0xC2E4]}, /* 확실 */
{keys: ['h','n','z'], chars: [0xC2DC,0xD5D8]}, /* 시험 */
{keys: ['a','h','n'], chars: [0xC0DD,0xD65C]}, /* 생활 */
{keys: ['f','h','n'], chars: [0xC0AC,0xD68C]}, /* 사회 */
{keys: ['d','f','h','n'], chars: [0xC0C1,0xD0DC]}, /* 상태 */
{keys: ['a','d','f','h','n'], chars: [0xD589,0xC0AC]}, /* 행사 */
{keys: ['c','h','n','s'], chars: [0xC13C,0xD2F0]}, /* 센티 */
{keys: ['h','n','s','t'], chars: [0xD604,0xC2E4]}, /* 현실 */
{keys: ['a','h','n','x'], chars: [0xD615,0xC2DD]}, /* 형식 */
{keys: ['a','f','h','n','v'], chars: [0xC0C1,0xD669]}, /* 상황 */
{keys: ['.','a','f','h','n'], chars: [0xC0C1,0xD669]}, /* 상황 */
{keys: ['d','h','n','v'], chars: [0xD68C,0xC0AC]}, /* 회사 */
{keys: ['.','d','h','n'], chars: [0xD68C,0xC0AC]}, /* 회사 */
{keys: ['c','h','n','v'], chars: [0xC218,0xD61C]}, /* 수혜 */
{keys: ['.','c','h','n'], chars: [0xC218,0xD61C]}, /* 수혜 */
{keys: ['d','e','h','n'], chars: [0xC2E4,0xCC9C]}, /* 실천 */
{keys: ['j','n','x'], chars: [0xC5ED,0xC0AC]}, /* 역사 */
{keys: ['j','n','s','x'], chars: [0xC778,0xC2DD]}, /* 인식 */
{keys: ['j','n','s'], chars: [0xC6B0,0xC120,0x20]}, /* 우선 */
{keys: ['a','j','n','s'], chars: [0xC778,0xC0DD]}, /* 인생 */
{keys: ['e','j','n'], chars: [0xC11C,0xC6B8]}, /* 서울 */
{keys: ['e','j','n','z'], chars: [0xC5F4,0xC2EC]}, /* 열심 */
{keys: ['j','n','s','w'], chars: [0xC0B0,0xC5C5]}, /* 산업 */
{keys: ['a','j','n'], chars: [0xC774,0xC0C1]}, /* 이상 */
{keys: ['b','f','j','n','z'], chars: [0xC2F8,0xC6C0]}, /* 싸움 */
{keys: ['f','j','n','p','z'], chars: [0xC2F8,0xC6C0]}, /* 싸움 */
{keys: ['c','e','j','n','r'], chars: [0xC138,0xC6D4]}, /* 세월 */
{keys: ['f','g','j','n'], chars: [0xC758,0xC0AC]}, /* 의사 */
{keys: ['c','j','n','r'], chars: [0xC5D0,0xC11C,0x20]}, /* 에서 */
{keys: ['l','n','x'], chars: [0xC2DC,0xC791]}, /* 시작 */
{keys: ['l','n','s'], chars: [0xC790,0xC2E0]}, /* 자신 */
{keys: ['e','l','n','s'], chars: [0xC9C4,0xC2E4]}, /* 진실 */
{keys: ['e','l','n','r'], chars: [0xC9C8,0xC11C]}, /* 질서 */
{keys: ['l','n','z'], chars: [0xC911,0xC2EC]}, /* 중심 */
{keys: ['a','l','n'], chars: [0xC2DC,0xC7A5]}, /* 시장 */
{keys: ['f','l','n'], chars: [0xC790,0xC2DD]}, /* 자식 */
{keys: ['f','l','n','z'], chars: [0xC7A0,0xC2DC]}, /* 잠시 */
{keys: ['a','f','l','n'], chars: [0xC131,0xC7A5]}, /* 성장 */
{keys: ['d','f','l','n'], chars: [0xC7AC,0xC0B0]}, /* 재산 */
{keys: ['a','d','f','l','n'], chars: [0xC9C0,0xC0C1]}, /* 지상 */
{keys: ['l','n','r'], chars: [0xC544,0xC800,0xC528]}, /* 아저씨 */
{keys: ['l','n','r','s'], chars: [0xC870,0xC120]}, /* 조선 */
{keys: ['a','l','n','s'], chars: [0xC815,0xC2E0]}, /* 정신 */
{keys: ['c','l','n'], chars: [0xC790,0xC138]}, /* 자세 */
{keys: ['l','n','v'], chars: [0xC870,0xC0AC]}, /* 조사 */
{keys: ['a','f','l','n','r'], chars: [0xC0AC,0xC815]}, /* 사정 */
{keys: ['b','l','n'], chars: [0xC218,0xC900]}, /* 수준 */
{keys: ['d','l','n','x'], chars: [0xC9C0,0xC2DD]}, /* 지식 */
{keys: ['d','l','n','s'], chars: [0xC0AC,0xC9C4]}, /* 사진 */
{keys: ['d','e','l','n'], chars: [0xC2E4,0xC81C]}, /* 실제 */
{keys: ['c','d','l','n'], chars: [0xC81C,0xC2DC]}, /* 제시 */
/* 첫소리 ㅇ 조합 */
{keys: ['j','x'], chars: [0xC74C,0xC545]}, /* 음악 */
{keys: ['j','s'], chars: [0xC5B4,0xB5A4,0x20]}, /* 어떤 */
{keys: ['e','j','s'], chars: [0xC778,0xBB3C]}, /* 인물 */
{keys: ['e','j'], chars: [0xC77C,0xC5B4]}, /* 일어 */
{keys: ['j','x','z'], chars: [0xC6C0,0xC9C1]}, /* 움직 */
{keys: ['j','z'], chars: [0xC74C,0xC2DD]}, /* 음식 */
{keys: ['j','q','w'], chars: [0xC5C6,0xC774,0x20]}, /* 없이 */
{keys: ['j','q','z'], chars: [0xC6C3,0xC74C]}, /* 웃음 */
{keys: ['a','j'], chars: [0xC5EC,0xC131]}, /* 여성 */
{keys: [';','j','s'], chars: [0xC774,0xB807]}, /* 이렇 */
{keys: ['b','j','t'], chars: [0xC601,0xAD6D]}, /* 영국 */
{keys: ['j','p','t'], chars: [0xC601,0xAD6D]}, /* 영국 */
{keys: ['b','s','t','y'], chars: [0xC6B4,0xBA85]}, /* 운명 */
{keys: ['p','s','t','y'], chars: [0xC6B4,0xBA85]}, /* 운명 */
{keys: ['g','j','t'], chars: [0xC73C,0xBA70,0x20]}, /* 으며 */
{keys: ['g','j','s','t'], chars: [0xC73C,0xBA74,0x20]}, /* 으면 */
{keys: ['b','f','j'], chars: [0xC544,0xBB34]}, /* 아무 */
{keys: ['f','j','p'], chars: [0xC544,0xBB34]}, /* 아무 */
{keys: ['c','g','j'], chars: [0xC740,0xD61C]}, /* 은혜 */
{keys: ['c','f','j'], chars: [0xC5D0,0xC694,0x2E,0x20]}, /* 에요. */
{keys: ['c','j','r'], chars: [0xC608,0xC694,0x2E,0x20]}, /* 예요. */
{keys: ['c','d','j','l'], chars: [0xC774,0xC81C]}, /* 이제 */
{keys: ['j','l','r','t'], chars: [0xC5EC,0xC804]}, /* 여전 */
{keys: ['m','r','t'], chars: [0xC5EC,0xB7EC,0x20]}, /* 여러 */
{keys: ['j','r','t'], chars: [0xC601,0xC5B4]}, /* 영어 */
{keys: ['g','j','r'], chars: [0xC758,0xC6D0]}, /* 의원 */
{keys: ['j','m','s'], chars: [0xC774,0xB860]}, /* 이론 */
{keys: ['e','j','m'], chars: [0xC54C,0xB824]}, /* 알려 */
{keys: ['j','m','z'], chars: [0xC774,0xB984]}, /* 이름 */
{keys: ['j','m','w'], chars: [0xC720,0xB7FD]}, /* 유럽 */
{keys: ['f','j','m'], chars: [0xC544,0xB77C]}, /* 아라 */
{keys: ['f','j','m','s'], chars: [0xC774,0xB780]}, /* 이란 */
{keys: ['d','f','j','m'], chars: [0xC544,0xB798]}, /* 아래 */
{keys: ['j','m','r','s'], chars: [0xC5B8,0xB860]}, /* 언론 */
{keys: ['e','j','m','r'], chars: [0xB9AC,0xC5BC]}, /* 리얼 */
{keys: ['j','m','v'], chars: [0xC624,0xB798]}, /* 오래 */
{keys: ['e','j','m','v'], chars: [0xC62C,0xB77C]}, /* 올라 */
{keys: ['b','j','m'], chars: [0xC6B0,0xB9AC]}, /* 우리 */
{keys: ['b','j','m','r','s'], chars: [0xC6D0,0xB9AC]}, /* 원리 */
{keys: ['j','m','p','r','s'], chars: [0xC6D0,0xB9AC]}, /* 원리 */
{keys: ['b','j','m','v'], chars: [0xC778,0xB958]}, /* 인류 */
{keys: ['g','j','m'], chars: [0xC73C,0xB85C,0x20]}, /* 으로 */
{keys: ['g','j','m','s'], chars: [0xC5BC,0xB978,0x20]}, /* 얼른 */
{keys: ['f','g','j','m'], chars: [0xC544,0xB984]}, /* 아름 */
{keys: ['b','f','m'], chars: [0xC544,0xBB34,0xB9AC,0x20]}, /* 아무리 */
{keys: ['f','m','p'], chars: [0xC544,0xBB34,0xB9AC,0x20]}, /* 아무리 */
{keys: ['d','j','m','s'], chars: [0xC5B4,0xB9B0,0x20]}, /* 어린 */
{keys: ['g','j','m','r'], chars: [0xC5B4,0xB978]}, /* 어른 */
{keys: ['h','j','x'], chars: [0xC5ED,0xD560]}, /* 역할 */
{keys: ['h','j','s','x'], chars: [0xD655,0xC778]}, /* 확인 */
{keys: ['h','j','s'], chars: [0xCC28,0xC6D0]}, /* 차원 */
{keys: ['e','h','j'], chars: [0xC62C,0xD574]}, /* 올해 */
{keys: ['h','j','z'], chars: [0xCC45,0xC784]}, /* 책임 */
{keys: ['c','h','j'], chars: [0xC5C5,0xCCB4]}, /* 업체 */
{keys: ['a','h','j'], chars: [0xC601,0xD654]}, /* 영화 */
{keys: ['f','h','j'], chars: [0xC544,0xD30C,0xD2B8]}, /* 아파트 */
{keys: ['f','h','j','s'], chars: [0xB610,0xD55C,0x20]}, /* 또한 */
{keys: ['d','f','h','j'], chars: [0xCC28,0xC774]}, /* 차이 */
{keys: ['a','d','f','h','j'], chars: [0xD589,0xC704]}, /* 행위 */
{keys: ['h','j','r','z'], chars: [0xC704,0xD5D8]}, /* 위험 */
{keys: ['h','j','t'], chars: [0xC5EC,0xC804,0xD788]}, /* 여전히 */
{keys: ['h','j','t','w'], chars: [0xC704,0xD611]}, /* 위협 */
{keys: ['a','h','j','r'], chars: [0xC5C4,0xCCAD,0x20]}, /* 엄청 */
{keys: ['a','h','j','t'], chars: [0xC601,0xD5A5]}, /* 영향 */
{keys: ['h','j','v'], chars: [0xC624,0xD788,0xB824,0x20]}, /* 오히려 */
{keys: ['h','j','v','x'], chars: [0xD639,0xC740,0x20]}, /* 혹은 */
{keys: ['a','h','j','v'], chars: [0xB3D9,0xC77C]}, /* 통일 */
{keys: ['f','h','j','s','v'], chars: [0xC644,0xC804,0xD788,0x20]}, /* 완전히 */
{keys: ['.','f','h','j','s'], chars: [0xC644,0xC804,0xD788,0x20]}, /* 완전히 */
{keys: ['d','h','j','v'], chars: [0xD68C,0xC758]}, /* 회의 */
{keys: ['.','d','h','j'], chars: [0xD68C,0xC758]}, /* 회의 */
{keys: ['b','h','j'], chars: [0xC774,0xD6C4]}, /* 이후 */
{keys: ['b','h','j','r','s'], chars: [0xC6D0,0xD55C]}, /* 원한 */
{keys: ['h','j','p','r','s'], chars: [0xC6D0,0xD55C]}, /* 원한 */
{keys: ['b','d','h','j','s'], chars: [0xC704,0xC6D0,0xD68C]}, /* 위원회 */
{keys: ['d','h','j','p','s'], chars: [0xC704,0xC6D0,0xD68C]}, /* 위원회 */
{keys: ['b','h','j','v'], chars: [0xC624,0xD6C4]}, /* 오후 */
{keys: ['.','b','h','j'], chars: [0xC624,0xD6C4]}, /* 오후 */
{keys: ['h','j','p','v'], chars: [0xC624,0xD6C4]}, /* 오후 */
{keys: ['g','h','j','s'], chars: [0xC740,0xD589]}, /* 은행 */
{keys: ['d','h','j'], chars: [0xC774,0xD574]}, /* 이해 */
{keys: ['j','r','s','u'], chars: [0xC5B8,0xB2C8]}, /* 언니 */
{keys: ['e','j','u'], chars: [0xC5BC,0xB9C8,0xB098,0x20]}, /* 얼마나 */
{keys: ['j','u','z'], chars: [0xB118,0xC5B4]}, /* 넘어 */
{keys: ['a','j','u'], chars: [0xB0B4,0xC6A9]}, /* 내용 */
{keys: ['f','j','u'], chars: [0xC774,0xB098,0x20]}, /* 이나 */
{keys: ['e','f','j','u'], chars: [0xC774,0xB0A0]}, /* 이날 */
{keys: ['d','f','j','u'], chars: [0xC544,0xB0B4]}, /* 아내 */
{keys: ['j','r','u'], chars: [0xC5B4,0xBA38,0xB2C8]}, /* 어머니 */
{keys: ['j','u','v'], chars: [0xC624,0xB298]}, /* 오늘 */
{keys: ['a','j','u','v'], chars: [0xB18D,0xC5C5]}, /* 농업 */
{keys: ['e','g','j','u'], chars: [0xB298,0xC5B4]}, /* 늘어 */
{keys: ['d','g','j','u'], chars: [0xC73C,0xB2C8]}, /* 으니 */
{keys: ['d','j','u'], chars: [0xB2C8,0xAE4C]}, /* 니까 */
{keys: ['d','e','j','u'], chars: [0xB0B4,0xC77C]}, /* 내일 */
{keys: ['d','j','u','z'], chars: [0xB290,0xB08C]}, /* 느낌 */
/* 첫소리 ㅈ~ㅎ 조합 */
{keys: ['l','x'], chars: [0xC870,0xC9C1]}, /* 조직 */
{keys: ['a','l','x'], chars: [0xC9C1,0xC7A5]}, /* 직장 */
{keys: ['l','w','x'], chars: [0xC9C1,0xC811,0x20]}, /* 직접 */
{keys: ['l','s'], chars: [0xC874,0xC7AC]}, /* 존재 */
{keys: ['a','l','s'], chars: [0xC804,0xC7C1]}, /* 전쟁 */
{keys: ['e','l'], chars: [0xC2DC,0xC808]}, /* 시절 */
{keys: ['l','z'], chars: [0xC870,0xAE08]}, /* 조금 */
{keys: ['l','q'], chars: [0xC800,0xAC83]}, /* 저것 */
{keys: ['a','l'], chars: [0xC815,0xB3C4]}, /* 정도 */
{keys: [';','l','s'], chars: [0xC88B,0xC544]}, /* 좋아 */
{keys: ['b','f','l'], chars: [0xC790,0xC8FC]}, /* 자주 */
{keys: ['f','l','p'], chars: [0xC790,0xC8FC]}, /* 자주 */
{keys: ['a','b','f','l'], chars: [0xC8FC,0xC7A5]}, /* 주장 */
{keys: ['a','f','l','p'], chars: [0xC8FC,0xC7A5]}, /* 주장 */
{keys: ['c','f','l'], chars: [0xC790,0xCCB4]}, /* 자체 */
{keys: ['c','l','r'], chars: [0xC804,0xCCB4]}, /* 전체 */
{keys: ['c','d','l'], chars: [0xC9C0,0xD61C]}, /* 지혜 */
{keys: ['j','l','x'], chars: [0xC9C0,0xC5ED]}, /* 지역 */
{keys: ['a','j','l','x'], chars: [0xC791,0xC6A9]}, /* 작용 */
{keys: ['j','l','s'], chars: [0xC790,0xC5F0]}, /* 자연 */
{keys: ['a','j','l','s'], chars: [0xC778,0xC815]}, /* 인정 */
{keys: ['e','j','l'], chars: [0xC81C,0xC77C]}, /* 제일 */
{keys: ['j','l','x','z'], chars: [0xC8FD,0xC74C]}, /* 죽음 */
{keys: ['a','j','l','w'], chars: [0xC785,0xC7A5]}, /* 입장 */
{keys: ['j','l','z'], chars: [0xC544,0xCE68]}, /* 아침 */
{keys: ['j','l','w'], chars: [0xC791,0xC5C5]}, /* 작업 */
{keys: ['a','j','l'], chars: [0xC911,0xC694]}, /* 중요 */
{keys: [';','e','j','l'], chars: [0xCC3E,0xC544]}, /* 찾아 */
{keys: ['b','f','j','l'], chars: [0xC544,0xC8FC,0x20]}, /* 아주 */
{keys: ['f','j','l','p'], chars: [0xC544,0xC8FC,0x20]}, /* 아주 */
{keys: ['a','b','f','j','l'], chars: [0xC911,0xC559]}, /* 중앙 */
{keys: ['a','f','j','l','p'], chars: [0xC911,0xC559]}, /* 중앙 */
{keys: ['b','g','j','l'], chars: [0xC8FC,0xC758]}, /* 주의 */
{keys: ['g','j','l','p'], chars: [0xC8FC,0xC758]}, /* 주의 */
{keys: ['b','g','j','l'], chars: [0xC8FC,0xC758]}, /* 주의 */
{keys: ['g','j','l','p'], chars: [0xC8FC,0xC758]}, /* 주의 */
{keys: ['c','j','l','r'], chars: [0xC5B4,0xC81C]}, /* 어제 */
{keys: ['c','j','l','r','s'], chars: [0xC5B8,0xC81C]}, /* 언제 */
{keys: ['a','c','j','l','r'], chars: [0xC608,0xC815]}, /* 예정 */
{keys: ['f','g','j','l','z'], chars: [0xC694,0xC998]}, /* 요즘 */
{keys: ['g','j','l','r'], chars: [0xC815,0xC758]}, /* 정의 */
{keys: ['h','l','x'], chars: [0xC815,0xCC45]}, /* 정책 */
{keys: ['a','h','l','x'], chars: [0xC815,0xD655]}, /* 정확 */
{keys: ['a','h','l','s'], chars: [0xD604,0xC7A5]}, /* 현장 */
{keys: ['e','h','l','s'], chars: [0xCD9C,0xC2E0]}, /* 출신 */
{keys: ['h','l','z'], chars: [0xCC98,0xC74C]}, /* 처음 */
{keys: ['h','l','w'], chars: [0xC870,0xD569]}, /* 조합 */
{keys: ['a','h','l'], chars: [0xC815,0xCE58]}, /* 정치 */
{keys: ['h','l','r','t'], chars: [0xC804,0xD600,0x20]}, /* 전혀 */
{keys: ['h','k','x'], chars: [0xAC1C,0xD601]}, /* 개혁 */
{keys: ['h','k','s'], chars: [0xD658,0xACBD]}, /* 환경 */
{keys: ['e','h','k','s'], chars: [0xACB0,0xD63C]}, /* 결혼 */
{keys: ['h','k','z'], chars: [0xCEF4,0xD4E8,0xD130]}, /* 컴퓨터 */
{keys: ['a','k','r','t'], chars: [0xACBD,0xD5D8]}, /* 경험 */
{keys: ['c','f','h','k'], chars: [0xD55C,0xACC4]}, /* 한계 */
{keys: ['c','d','h','k'], chars: [0xCF00,0xC774]}, /* 케이 */
{keys: ['c','d','h','k','x'], chars: [0xACC4,0xD68D]}, /* 계획 */
{keys: ['h','k','r','t'], chars: [0xACBD,0xD5A5]}, /* 경향 */
{keys: ['f','g','h','k'], chars: [0xD55C,0xAE00]}, /* 한글 */
{keys: ['h','i','x'], chars: [0xD2B9,0xD788,0x20]}, /* 특히 */
{keys: ['a','i','n','x'], chars: [0xD2B9,0xC131]}, /* 특성 */
{keys: ['h','i','s','x'], chars: [0xB300,0xD55C,0xBBFC,0xAD6D]}, /* 대한민국 */
{keys: ['h','i','s'], chars: [0xB300,0xD55C]}, /* 대한 */
{keys: ['a','h','i','s'], chars: [0xB3D9,0xC2E0]}, /* 통신 */
{keys: ['e','h','i'], chars: [0xD65C,0xB3D9]}, /* 활동 */
{keys: ['h','i','z'], chars: [0xD68C,0xB2F4]}, /* 회담 */
{keys: ['a','i','m'], chars: [0xB300,0xB3D9,0xB839]}, /* 대통령 */
{keys: ['c','f','h','i'], chars: [0xD55C,0xD14C,0x20]}, /* 한테 */
{keys: ['a','h','o','x'], chars: [0xD589,0xBCF5]}, /* 행복 */
{keys: ['h','o','s'], chars: [0xBD81,0xD55C]}, /* 북한 */
{keys: ['e','h','o'], chars: [0xD544,0xC694]}, /* 필요 */
{keys: ['n','o','z'], chars: [0xC0C1,0xD488]}, /* 상품 */
{keys: ['a','h','o'], chars: [0xD45C,0xC815]}, /* 표정 */
{keys: ['h','o','r','t'], chars: [0xD45C,0xD604]}, /* 표현 */
{keys: ['l','u','x'], chars: [0xC791,0xB144]}, /* 작년 */
{keys: ['l','s','u'], chars: [0xB294,0xC9C0,0x20]}, /* 는지 */
{keys: ['l','u','z'], chars: [0xC8FC,0xB2D8]}, /* 주님 */
{keys: ['a','l','u'], chars: [0xB098,0xC911]}, /* 나중 */
{keys: ['f','l','s','u'], chars: [0xC9C0,0xB09C,0x20]}, /* 지난 */
{keys: ['l','r','u'], chars: [0xC800,0xB141]}, /* 저녁 */
{keys: ['d','l','u'], chars: [0xC9C0,0xB098]}, /* 지나 */
{keys: ['h','x'], chars: [0xD559,0xAD50]}, /* 학교 */
{keys: ['h','s'], chars: [0xD55C,0xAD6D]}, /* 한국 */
{keys: ['e','h'], chars: [0xD560,0xBA38,0xB2C8]}, /* 할머니 */
{keys: ['h','z'], chars: [0xD568,0xAED8,0x20]}, /* 함께 */
{keys: ['a','h'], chars: [0xD56D,0xC0C1,0x20]}, /* 항상 */
{keys: ['h','x','y'], chars: [0xBB38,0xD559]}, /* 문학 */
{keys: ['h','y','z'], chars: [0xB9CC,0xD07C,0x20]}, /* 만큼 */
{keys: ['a','h','y'], chars: [0xD76C,0xB9DD]}, /* 희망 */
{keys: ['g','h','x','y'], chars: [0xCE21,0xBA74]}, /* 측면 */
{keys: ['d','h','y'], chars: [0xBBF8,0xD130]}, /* 미터 */
{keys: ['h','u','x'], chars: [0xD559,0xB144]}, /* 학년 */
{keys: ['e','h','u'], chars: [0xD558,0xB298]}, /* 하늘 */
{keys: ['h','u','z'], chars: [0xD558,0xB098,0xB2D8]}, /* 하나님 */
{keys: ['f','h','u'], chars: [0xD558,0xB098]}, /* 하나 */
{keys: ['a','h','r','u'], chars: [0xCCAD,0xC18C,0xB144]}, /* 청소년 */
//160823 약어 추가
{keys: ['a','b','f','j','l','s'], chars: [0xC704,0xC6D0,0xC7A5]}, /* 위원장 */
{keys: ['a','f','j','l','p','s'], chars: [0xC704,0xC6D0,0xC7A5]}, /* 위원장 */
{keys: ['a','h','i'], chars: [0xD589,0xB3D9]}, /* 행동 */
{keys: ['a','i','k','s'], chars: [0xADF8,0xB3D9,0xC548]}, /* 그동안 */
{keys: ['j','n','w'], chars: [0xC0AC,0xC5C5]}, /* 사업 */
{keys: ['d','f','l','s','u'], chars: [0xC9C0,0xB09C,0xD574]}, /* 지난해 */
{keys: ['d','k','n'], chars: [0xC2DC,0xAE30]}, /* 시기 */
{keys: ['g','k','m','r'], chars: [0xADF8,0xB7EC]}, /* 그러 */
{keys: ['e','f','l','o'], chars: [0xD560,0xC544,0xBC84,0xC9C0]}, /* 할아버지 */
{keys: ['a','f','k','l'], chars: [0xACF5,0xC7A5]}, /* 공장 */
{keys: ['i','j','z'], chars: [0xB3C4,0xC6C0]}, /* 도움 */
{keys: ['f','j','u','v'], chars: [0xC624,0xB298,0xB0A0]}, /* 오늘날 */
{keys: ['.','f','j','u'], chars: [0xC624,0xB298,0xB0A0]}, /* 오늘날 */
{keys: ['c','d','e','j','l'], chars: [0xC81C,0xC77C]}, /* 제일 */
{keys: ['e','h','k'], chars: [0xACB0,0xCF54,0x20]}, /* 결코 */
{keys: ['b','d','k','l'], chars: [0xC9C0,0xAD6C]}, /* 지구 */
{keys: ['d','k','l','p'], chars: [0xC9C0,0xAD6C]}, /* 지구 */
{keys: ['f','n','o','v'], chars: [0xC18C,0xBE44,0xC790]}, /* 소비자 */
{keys: ['.','f','n','o'], chars: [0xC18C,0xBE44,0xC790]}, /* 소비자 */
{keys: ['a','f','i','u'], chars: [0xB178,0xB3D9,0xC790]}, /* 노동자 */
{keys: ['b','d','h','j'], chars: [0xC704,0xCE58]}, /* 위치 */
{keys: ['d','h','j','p'], chars: [0xC704,0xCE58]}, /* 위치 */
{keys: ['j','q'], chars: [0xC774,0xC6C3]}, /* 이웃 */
{keys: ['f','k','l','s'], chars: [0xAD00,0xACC4,0xC790]}, /* 관계자 */
{keys: ['d','k','l','v'], chars: [0xCD08,0xAE30]}, /* 초기 */
{keys: ['.','d','k','l'], chars: [0xCD08,0xAE30]}, /* 초기 */
{keys: ['a','h','k'], chars: [0xACE0,0xD5A5]}, /* 고향 */
{keys: [';','s','u'], chars: [0xB0B4,0xB193]}, /* 내놓 */
{keys: ['b','g','k','l'], chars: [0xADF8,0xC911]}, /* 그중 */
{keys: ['g','k','l','p'], chars: [0xADF8,0xC911]}, /* 그중 */
{keys: ['d','j','s','u'], chars: [0xB178,0xC778]}, /* 노인 */
{keys: ['a','f','l','n'], chars: [0xC0AC,0xC7A5]}, /* 사장 */
{keys: ['a','l','n','r'], chars: [0xC131,0xC7A5]}, /* 성장 */
{keys: ['a','l','n','v'], chars: [0xC7A5,0xC18C]}, /* 장소 */
{keys: ['f','i','o','s'], chars: [0xD310,0xB2E8]}, /* 판단 */
{keys: ['d','k','m'], chars: [0xAC70,0xB9AC]}, /* 거리 */
{keys: ['b','o','y','z'], chars: [0xBD80,0xBAA8,0xB2D8]}, /* 부모님 */
{keys: ['b','e','l','n'], chars: [0xC218,0xCD9C]}, /* 수출 */
{keys: ['d','m','y'], chars: [0xBBF8,0xB9AC,0x20]}, /* 미리 */
{keys: ['f','m','y'], chars: [0xB9C8,0xB9AC]}, /* 마리 */
{keys: ['d','f','m','y'], chars: [0xBBF8,0xB798]}, /* 미래 */
{keys: ['l','r','x','y'], chars: [0xBB34,0xCC99,0x20]}, /* 무척 */
{keys: ['a','d','k','o'], chars: [0xBE44,0xD589,0xAE30]}, /* 비행기 */
{keys: ['d','f','j','m','s'], chars: [0xC6D0,0xB798]}, /* 원래 */
{keys: ['b','m','o'], chars: [0xBFCC,0xB9AC]}, /* 뿌리 */
{keys: ['g','k','m','t'], chars: [0xADF8,0xB9AC,0xD558,0xC5EC,0x20]}, /* 그리하여 */
{keys: ['j','l','s','x'], chars: [0xC9C1,0xC6D0]}, /* 직원 */
{keys: ['b','o','u'], chars: [0xB0B4,0xBD80]}, /* 내부 */
{keys: ['f','i','n','v'], chars: [0xB2E4,0xC18C,0x20]}, /* 다소 */
{keys: ['.','f','i','n'], chars: [0xB2E4,0xC18C,0x20]}, /* 다소 */
{keys: ['c','f','k'], chars: [0xAC00,0xAC8C]}, /* 가게 */
{keys: ['g','i','k','x'], chars: [0xAC00,0xB4DD,0x20]}, /* 가득 */
{keys: ['b','f','k','l','s'], chars: [0xC7A5,0xAD70]}, /* 장군 */
{keys: ['f','k','l','p','s'], chars: [0xC7A5,0xAD70]}, /* 장군 */
{keys: ['b','t','y'], chars: [0xBB34,0xC5ED]}, /* 무역 */
{keys: ['p','t','y'], chars: [0xBB34,0xC5ED]}, /* 무역 */
{keys: ['b','f','i','o'], chars: [0xBD80,0xB2F4]}, /* 부담 */
{keys: ['f','i','o','p'], chars: [0xBD80,0xB2F4]}, /* 부담 */
{keys: ['a','l','o','s'], chars: [0xCDA9,0xBD84]}, /* 충분 */
{keys: ['a','d','l','o','s'], chars: [0xCDA9,0xBD84,0xD788,0x20]}, /* 충분히 */
{keys: ['c','l','u'], chars: [0xC5D0,0xB108,0xC9C0]}, /* 에너지 */
{keys: ['a','l','m','r'], chars: [0xC815,0xB9AC]}, /* 정리 */
{keys: ['j','l','s','w'], chars: [0xC9D1,0xC548]}, /* 집안 */
{keys: ['d','f','k','o'], chars: [0xBC30,0xACBD]}, /* 배경 */
{keys: ['b','i','n','s'], chars: [0xB2E8,0xC21C]}, /* 단순 */
{keys: ['b','d','i','n','s'], chars: [0xB2E8,0xC21C,0xD788,0x20]}, /* 단순히 */
{keys: ['d','i','n','p','s'], chars: [0xB2E8,0xC21C,0xD788,0x20]}, /* 단순히 */
{keys: ['f','i','n','s'], chars: [0xC218,0xB2E8]}, /* 수단 */
{keys: ['l','o','w'], chars: [0xBC95,0xCE59]}, /* 법칙 */
{keys: ['k','n','v','z'], chars: [0xC18C,0xAE08]}, /* 소금 */
{keys: ['f','k','l','r','s'], chars: [0xC790,0xC804,0xAC70]}, /* 자전거 */
{keys: ['d','l','o','v'], chars: [0xBC94,0xC8C4]}, /* 범죄 */
{keys: ['.','d','l','o'], chars: [0xBC94,0xC8C4]}, /* 범죄 */
{keys: ['m','s','u'], chars: [0xB17C,0xB9AC]}, /* 논리 */
{keys: ['f','g','i','m'], chars: [0xB4DC,0xB77C,0xB9C8]}, /* 드라마 */
{keys: ['g','h','l','r'], chars: [0xC800,0xD76C]}, /* 저희 */
{keys: ['g','h','r','u'], chars: [0xB108,0xD76C]}, /* 너희 */
{keys: ['a','f','i','k','r'], chars: [0xAD50,0xB3D9]}, /* 교통 */
{keys: ['.','a','i','k','v'], chars: [0xAD50,0xB3D9]}, /* 교통 */
{keys: ['e','j','l','z'], chars: [0xC80A,0xC740]}, /* 젊은 */
{keys: ['k','n','s','v'], chars: [0xC190,0xAC00,0xB77D]}, /* 손가락 */
{keys: ['g','k','l','r'], chars: [0xC801,0xADF9]}, /* 적극 */
{keys: ['g','k','l','r','x'], chars: [0xC801,0xADF9,0xC801]}, /* 적극적 */
{keys: ['d','f','o','y'], chars: [0xD310,0xB9E4]}, /* 판매 */
{keys: ['a','h','n','r','t'], chars: [0xD615,0xC131]}, /* 형성 */
{keys: ['a','l','s','y'], chars: [0xC7A5,0xBA74]}, /* 장면 */
{keys: ['d','u','y'], chars: [0xB098,0xBA38,0xC9C0]}, /* 나머지 */
{keys: ['e','n','u'], chars: [0xB0A0,0xC528]}, /* 날씨 */
{keys: ['a','d','f','k','o'], chars: [0xAC1C,0xBC29]}, /* 개방 */
{keys: ['j','o','r','t'], chars: [0xC5EC,0xB7EC,0xBD84]}, /* 여러분 */
{keys: ['d','f','h','u','v'], chars: [0xC65C,0xB0D0,0xD558,0xBA74,0x20]}, /* 왜냐하면 */
{keys: ['.','d','f','h','u'], chars: [0xC65C,0xB0D0,0xD558,0xBA74,0x20]}, /* 왜냐하면 */
{keys: ['d','f','i','k'], chars: [0xAE30,0xB300]}, /* 기대 */
{keys: ['f','i','l','x'], chars: [0xB3C4,0xCC29]}, /* 도착 */
{keys: ['c','n','o','r'], chars: [0xC18C,0xD504,0xD2B8,0xC6E8,0xC5B4]}, /* 소프트웨어 */
{keys: ['f','g','i','j'], chars: [0xB530,0xB73B]}, /* 따뜻 */
{keys: ['c','i','n'], chars: [0xC138,0xB300]}, /* 세대 */
{keys: ['b','k','l','x'], chars: [0xCD95,0xAD6C]}, /* 축구 */
{keys: ['a','h','t','u'], chars: [0xD615,0xB2D8]}, /* 형님 */
{keys: ['a','f','i','l'], chars: [0xB2F9,0xC7A5]}, /* 당장 */
{keys: ['b','t','w','y'], chars: [0xBB34,0xB835]}, /* 무렵 */
{keys: ['p','t','w','y'], chars: [0xBB34,0xB835]}, /* 무렵 */
{keys: ['e','f','n','y'], chars: [0xC0AC,0xBB3C]}, /* 사물 */
{keys: ['e','l','o','r','s'], chars: [0xC77C,0xBC18,0xC801]}, /* 일반적 */
{keys: ['f','i','o','x'], chars: [0xBC14,0xB2E5]}, /* 바닥 */
{keys: ['b','k','n'], chars: [0xAD50,0xC218]}, /* 교수 */
{keys: ['d','f','k','n'], chars: [0xC0C8,0xB07C]}, /* 새끼 */
{keys: ['d','g','n','o'], chars: [0xC11C,0xBE44,0xC2A4]}, /* 서비스 */
{keys: ['i','n','r','s'], chars: [0xC120,0xD0DD]}, /* 선택 */
{keys: ['f','i','k','s'], chars: [0xAC04,0xB2E8]}, /* 간단 */
{keys: ['c','f','i','k'], chars: [0xAC8C,0xB2E4,0xAC00,0x20]}, /* 게다가 */
{keys: ['a','g','i','k'], chars: [0xACE0,0xB4F1]}, /* 고등 */
{keys: ['c','h','y'], chars: [0xB9E4,0xCCB4]}, /* 매체 */
{keys: ['f','l','o','w'], chars: [0xBCF5,0xC7A1]}, /* 복잡 */
{keys: ['c','h','l','r'], chars: [0xCCB4,0xD5D8]}, /* 체험 */
{keys: ['b','k','n','x'], chars: [0xAD6C,0xC18D]}, /* 구속 */
{keys: ['d','f','i','m','v'], chars: [0xB54C,0xB85C,0x20]}, /* 때로 */
{keys: ['.','d','f','i','m'], chars: [0xB54C,0xB85C,0x20]}, /* 때로 */
{keys: ['l','r','t'], chars: [0xC5B4,0xCA4C,0xBA74,0x20]}, /* 어쩌면 */
{keys: ['k','o','x'], chars: [0xADF9,0xBCF5]}, /* 극복 */
{keys: ['e','o','y'], chars: [0xBE44,0xBC00]}, /* 비밀 */
{keys: ['j','t','u'], chars: [0xC774,0xB150]}, /* 이념 */
{keys: ['l','q','y'], chars: [0xC798,0xBABB]}, /* 잘못 */
{keys: ['f','n','o','x'], chars: [0xBC15,0xC0AC]}, /* 박사 */
{keys: ['b','l','r','s'], chars: [0xC804,0xBB38,0xAC00]}, /* 전문가 */
{keys: ['l','p','r','s'], chars: [0xC804,0xBB38,0xAC00]}, /* 전문가 */
{keys: ['b','f','k','l'], chars: [0xC790,0xAFB8,0x20]}, /* 자꾸 */
{keys: ['f','k','l','p'], chars: [0xC790,0xAFB8,0x20]}, /* 자꾸 */
{keys: ['d','f','h','j','v'], chars: [0xD574,0xC678]}, /* 해외 */
{keys: ['.','d','f','h','j'], chars: [0xD574,0xC678]}, /* 해외 */
{keys: ['d','n','v','y'], chars: [0xBBF8,0xC18C]}, /* 미소 */
{keys: ['.','d','n','y'], chars: [0xBBF8,0xC18C]}, /* 미소 */
{keys: ['a','i','o','v'], chars: [0xBCF4,0xB3D9]}, /* 보통 */
{keys: ['d','i','n','x'], chars: [0xC2DD,0xB2F9]}, /* 식당 */
{keys: ['d','j','m'], chars: [0xC774,0xB798]}, /* 이래 */
{keys: ['b','c','h','j'], chars: [0xCCB4,0xC721]}, /* 체육 */
{keys: ['c','h','j','p'], chars: [0xCCB4,0xC721]}, /* 체육 */
{keys: ['g','i','v'], chars: [0xB3C5,0xD2B9]}, /* 독특 */
{keys: ['.','g','i'], chars: [0xB3C5,0xD2B9]}, /* 독특 */
{keys: ['c','m','n','v'], chars: [0xC0AC,0xB840]}, /* 사례 */
{keys: ['a','k','o','s'], chars: [0xD3C9,0xADE0]}, /* 평균 */
{keys: ['a','d','k','l','z'], chars: [0xAC10,0xC815]}, /* 김정 */
{keys: ['h','m','s'], chars: [0xD6C8,0xB828]}, /* 훈련 */
{keys: ['h','m','z'], chars: [0xD750,0xB984]}, /* 흐름 */
{keys: ['s','u','y'], chars: [0xB17C,0xBB38]}, /* 논문 */
{keys: ['a','j','s'], chars: [0xC624,0xB7AB,0xB3D9,0xC548]}, /* 오랫동안 */
{keys: ['m','o','v','x'], chars: [0xD3ED,0xB825]}, /* 폭력 */
{keys: ['h','n','v','x'], chars: [0xD639,0xC2DC,0x20]}, /* 혹시 */
{keys: ['d','m','o','v','x'], chars: [0xC62C,0xB9BC,0xD53D]}, /* 올림픽 */
{keys: ['.','d','m','o','x'], chars: [0xC62C,0xB9BC,0xD53D]}, /* 올림픽 */
{keys: ['a','c','k','l'], chars: [0xC81C,0xACF5]}, /* 제공 */
{keys: ['a','f','g','k','l'], chars: [0xC99D,0xAC00]}, /* 증가 */
{keys: ['i','l','r','x'], chars: [0xC801,0xC5B4,0xB3C4,0x20]}, /* 적어도 */
{keys: ['b','k','m','r','s'], chars: [0xAD8C,0xB9AC]}, /* 권리 */
{keys: ['k','m','p','r','s'], chars: [0xAD8C,0xB9AC]}, /* 권리 */
{keys: ['j','m','r','t','z'], chars: [0xC5B4,0xB824,0xC6C0]}, /* 어려움 */
{keys: ['b','f','j','l','s'], chars: [0xC790,0xC6D0]}, /* 자원 */
{keys: ['f','j','l','p','s'], chars: [0xC790,0xC6D0]}, /* 자원 */
{keys: ['b','c','h','y'], chars: [0xBB3C,0xCCB4]}, /* 물체 */
{keys: ['c','h','p','y'], chars: [0xBB3C,0xCCB4]}, /* 물체 */
{keys: ['a','d','o','y'], chars: [0xBD84,0xBA85,0xD788,0x20]}, /* 분명히 */
{keys: ['b','f','j','q'], chars: [0xC544,0xBB34,0xAC83]}, /* 아무것 */
{keys: ['f','j','p','q'], chars: [0xC544,0xBB34,0xAC83]}, /* 아무것 */
{keys: ['a','n','o','t'], chars: [0xD3C9,0xC18C]}, /* 평소 */
{keys: ['b','f','i','k'], chars: [0xB354,0xAD6C,0xB098,0x20]}, /* 더구나 */
{keys: ['f','i','k','p'], chars: [0xB354,0xAD6C,0xB098,0x20]}, /* 더구나 */
{keys: ['g','i','l','r'], chars: [0xC5B4,0xCA0C,0xB4E0,0x20]}, /* 어쨌든 */
{keys: ['b','i','l','x'], chars: [0xC8FC,0xD0DD]}, /* 주택 */
{keys: ['e','s','u'], chars: [0xB208,0xAE38]}, /* 눈길 */
{keys: ['g','i','n'], chars: [0xC2A4,0xD2B8,0xB808,0xC2A4]}, /* 스트레스 */
{keys: ['e','m','u'], chars: [0xB110,0xB9AC,0x20]}, /* 널리 */
{keys: ['h','s','u','v'], chars: [0xB18D,0xCD0C]}, /* 농촌 */
{keys: ['d','f','l','n','v'], chars: [0xC18C,0xC7AC]}, /* 소재 */
{keys: ['.','d','f','l','n'], chars: [0xC18C,0xC7AC]}, /* 소재 */
{keys: ['a','f','l','y'], chars: [0xC804,0xB9DD]}, /* 전망 */
{keys: ['d','k','o','v'], chars: [0xD3EC,0xAE30]}, /* 포기 */
{keys: ['.','d','k','o'], chars: [0xD3EC,0xAE30]}, /* 포기 */
{keys: ['k','o','v'], chars: [0xBCF4,0xACE0]}, /* 보고 */
{keys: ['f','k','o','r'], chars: [0xBE44,0xAD50]}, /* 비교 */
{keys: ['.','k','o','v'], chars: [0xBE44,0xAD50]}, /* 비교 */
{keys: ['f','k','o','r','x'], chars: [0xBE44,0xAD50,0xC801]}, /* 비교적 */
{keys: ['.','k','o','v','x'], chars: [0xBE44,0xAD50,0xC801]}, /* 비교적 */
{keys: ['k','n','r','z'], chars: [0xAC80,0xC0AC]}, /* 검사 */
{keys: ['k','m','s','v'], chars: [0xACB0,0xB860]}, /* 결론 */
{keys: ['f','o','x','y'], chars: [0xBC15,0xBB3C,0xAD00]}, /* 박물관 */
{keys: ['n','v','y'], chars: [0xC18C,0xBB38]}, /* 소문 */
{keys: ['l','t','u'], chars: [0xC790,0xB140]}, /* 자녀 */
{keys: ['c','l','v','y'], chars: [0xC81C,0xBAA9]}, /* 제목 */
{keys: ['e','k','o','t'], chars: [0xD310,0xACB0]}, /* 판결 */
{keys: ['a','f','k','o'], chars: [0xAC00,0xBC29]}, /* 가방 */
{keys: ['b','i','k','s'], chars: [0xAD70,0xB300]}, /* 군대 */
{keys: ['g','h','y','z'], chars: [0xADF8,0xB9CC,0xD07C]}, /* 그만큼 */
{keys: ['a','d','f','i','o'], chars: [0xC0C1,0xB300,0xBC29]}, /* 상대방 */
{keys: ['b','k','n','r'], chars: [0xC11C,0xAD6C]}, /* 서구 */
{keys: ['k','n','p','r'], chars: [0xC11C,0xAD6C]}, /* 서구 */
{keys: ['e','k','n','v'], chars: [0xC2DC,0xACE8]}, /* 시골 */
{keys: ['d','l','m','v'], chars: [0xCE58,0xB8CC]}, /* 치료 */
{keys: ['.','d','l','m'], chars: [0xCE58,0xB8CC]}, /* 치료 */
{keys: ['b','o','s','y'], chars: [0xBD80,0xBB38]}, /* 부문 */
{keys: ['d','g','l','n'], chars: [0xC2DC,0xB9AC,0xC988]}, /* 시리즈 */
{keys: ['j','n','s','z'], chars: [0xC784,0xC2E0]}, /* 임신 */
{keys: ['d','k','n','x'], chars: [0xC2DD,0xAD6C]}, /* 식구 */
{keys: ['d','f','k','n','s'], chars: [0xAC1C,0xC120]}, /* 개선 */
{keys: ['m','o','x','z'], chars: [0xBC14,0xB78C,0xC9C1]}, /* 바람직 */
{keys: ['d','j','m','v'], chars: [0xB77C,0xB514,0xC624]}, /* 라디오 */
{keys: ['.','d','j','m'], chars: [0xB77C,0xB514,0xC624]}, /* 라디오 */
{keys: ['a','i','o','s','v'], chars: [0xBD80,0xB3D9,0xC0B0]}, /* 부동산 */
{keys: ['f','h','n','v'], chars: [0xC2E0,0xD654]}, /* 신화 */
{keys: ['.','f','h','n'], chars: [0xC2E0,0xD654]}, /* 신화 */
{keys: ['j','l','w','x'], chars: [0xC9C1,0xC5C5]}, /* 직업 */
{keys: ['o','z'], chars: [0xBC94,0xC704]}, /* 범위 */
{keys: ['a','f','l','n','v'], chars: [0xC870,0xC0C1]}, /* 조상 */
{keys: ['.','a','f','l','n'], chars: [0xC870,0xC0C1]}, /* 조상 */
{keys: ['e','h','l','x'], chars: [0xCCA0,0xD559]}, /* 철학 */
{keys: ['g','k','o','x'], chars: [0xADFC,0xBCF8,0xC801]}, /* 근본적 */
{keys: ['k','o','v','x'], chars: [0xBCF8,0xACA9,0xC801]}, /* 본격적 */
{keys: ['d','k','o','x'], chars: [0xAE30,0xBCF8,0xC801]}, /* 기본적 */
{keys: ['c','l','y','z'], chars: [0xBB38,0xC81C,0xC810]}, /* 문제점 */
{keys: ['c','d','j','s'], chars: [0xC778,0xC81C,0x20]}, /* 인제 */
{keys: ['a','k','l','x'], chars: [0xCDA9,0xACA9]}, /* 충격 */
{keys: ['g','k','o','z'], chars: [0xAE08,0xBC29,0x20]}, /* 금방 */
{keys: ['b','e','n','y'], chars: [0xBBF8,0xC220]}, /* 미술 */
{keys: ['d','f','n','o','x'], chars: [0xBC31,0xC131]}, /* 백성 */
{keys: ['a','f','h','n'], chars: [0xC0C1,0xB2F9,0xD788,0x20]}, /* 상당히 */
{keys: ['e','f','k','n'], chars: [0xC0C9,0xAE54]}, /* 색깔 */
{keys: ['a','b','t','y'], chars: [0xC720,0xBA85]}, /* 유명 */
{keys: ['a','p','t','y'], chars: [0xC720,0xBA85]}, /* 유명 */
{keys: ['j','k','s','v','x'], chars: [0xC678,0xAD6D,0xC778]}, /* 외국인 */
{keys: ['h','l','s','z'], chars: [0xD55C,0xCC38]}, /* 한참 */
{keys: ['b','f','k','n','s'], chars: [0xAD70,0xC0AC]}, /* 군사 */
{keys: ['f','k','n','p','s'], chars: [0xAD70,0xC0AC]}, /* 군사 */
{keys: ['b','c','l','n','x'], chars: [0xC219,0xC81C]}, /* 숙제 */
{keys: ['c','l','n','p','x'], chars: [0xC219,0xC81C]}, /* 숙제 */
{keys: ['c','f','j','n','s'], chars: [0xC608,0xC0B0]}, /* 예산 */
{keys: [';','e','j','k'], chars: [0xC628,0xAC16,0x20]}, /* 온갖 */
{keys: ['b','m','t'], chars: [0xC6B0,0xB824]}, /* 우려 */
{keys: ['m','p','t'], chars: [0xC6B0,0xB824]}, /* 우려 */
{keys: ['b','f','j','n','s'], chars: [0xC6B0,0xC0B0]}, /* 우산 */
{keys: ['f','j','n','p','s'], chars: [0xC6B0,0xC0B0]}, /* 우산 */
{keys: ['b','f','j','n'], chars: [0xC218,0xC694]}, /* 수요 */
{keys: ['f','j','n','p'], chars: [0xC218,0xC694]}, /* 수요 */
{keys: ['d','g','k','o'], chars: [0xAE30,0xC068]}, /* 기쁨 */
{keys: [';','w','y'], chars: [0xBB34,0xB98E]}, /* 무릎 */
{keys: ['c','i','n','z'], chars: [0xC2DC,0xC2A4,0xD15C]}, /* 시스템 */
{keys: ['d','e','f','l','u'], chars: [0xC9C0,0xB09C,0xB2EC]}, /* 지난달 */
{keys: ['f','h','j','z'], chars: [0xCC38,0xC5EC]}, /* 참여 */
{keys: ['e','g','j','k','r'], chars: [0xAC78,0xC74C]}, /* 걸음 */
{keys: ['b','k','t'], chars: [0xACA8,0xC6B0,0x20]}, /* 겨우 */
{keys: ['k','p','t'], chars: [0xACA8,0xC6B0,0x20]}, /* 겨우 */
{keys: ['d','f','l','n','x'], chars: [0xCC45,0xC0C1]}, /* 책상 */
{keys: ['g','i','n','x'], chars: [0xC18C,0xB4DD]}, /* 소득 */
{keys: ['d','i','l','z'], chars: [0xCE68,0xB300]}, /* 침대 */
{keys: ['f','g','i','n'], chars: [0xC2A4,0xD0C0]}, /* 스타 */
{keys: ['e','j','n','w'], chars: [0xC785,0xC220]}, /* 입술 */
{keys: ['a','b','k','l'], chars: [0xC911,0xAC04]}, /* 중간 */
{keys: ['n','v','w','y'], chars: [0xBAB9,0xC2DC,0x20]}, /* 몹시 */
{keys: ['b','g','i'], chars: [0xBB38,0xB4DD,0x20]}, /* 문득 */
{keys: ['g','i','p'], chars: [0xBB38,0xB4DD,0x20]}, /* 문득 */
{keys: ['g','n','o'], chars: [0xC2A4,0xD3EC,0xCE20]}, /* 스포츠 */
{keys: ['k','l','r'], chars: [0xC800,0xAE30]}, /* 저기 */
{keys: ['g','k','l'], chars: [0xADF8,0xC800,0x20]}, /* 그저 */
{keys: ['c','f','j','l','s'], chars: [0xC5B8,0xC820,0xAC00,0x20]}, /* 언젠가 */
{keys: ['c','f','j','n'], chars: [0xC608,0xC220,0xAC00]}, /* 예술가 */
{keys: ['g','i','j','v'], chars: [0xC758,0xB3C4]}, /* 의도 */
{keys: ['.','g','i','j'], chars: [0xC758,0xB3C4]}, /* 의도 */
{keys: ['f','h','s','y'], chars: [0xAC00,0xB9CC,0xD788,0x20]}, /* 가만히 */
{keys: ['b','f','h','i','s'], chars: [0xD55C,0xB450,0x20]}, /* 한두 */
{keys: ['f','h','i','p','s'], chars: [0xD55C,0xB450,0x20]}, /* 한두 */
{keys: ['d','f','h','j','s'], chars: [0xD55C,0xB54C]}, /* 한때 */
{keys: ['a','d','f','i','n','x'], chars: [0xC0C1,0xB300,0xC801]}, /* 상대적 */
{keys: ['d','n','o','x'], chars: [0xC2DD,0xD488]}, /* 식품 */
{keys: ['.','h','j','r','s'], chars: [0xD68C,0xC6D0]}, /* 회원 */
{keys: ['h','j','r','s','v'], chars: [0xD68C,0xC6D0]}, /* 회원 */
{keys: ['i','n','r','v'], chars: [0xB3C4,0xC11C,0xAD00]}, /* 도서관 */
{keys: ['.','i','n','r'], chars: [0xB3C4,0xC11C,0xAD00]}, /* 도서관 */
{keys: ['d','l','v','y'], chars: [0xC870,0xBBF8,0xB8CC]}, /* 조미료 */
{keys: ['.','d','l','y'], chars: [0xC870,0xBBF8,0xB8CC]}, /* 조미료 */
{keys: ['a','f','k','m'], chars: [0xAC15,0xB825]}, /* 강력 */
{keys: ['e','g','u','y'], chars: [0xB9C8,0xB298]}, /* 마늘 */
{keys: ['g','k','n','w'], chars: [0xC2B5,0xAD00]}, /* 습관 */
{keys: ['e','h','l'], chars: [0xC9C0,0xD558,0xCCA0]}, /* 지하철 */
{keys: ['h','s','t','y'], chars: [0xD654,0xBA74]}, /* 화면 */
{keys: ['f','i','o','r','x'], chars: [0xB300,0xD45C,0xC801]}, /* 대표적 */
{keys: ['.','i','o','v','x'], chars: [0xB300,0xD45C,0xC801]}, /* 대표적 */
{keys: ['b','l','o'], chars: [0xC8FC,0xBD80]}, /* 주부 */
{keys: ['d','l','m','s'], chars: [0xC9C4,0xB9AC]}, /* 진리 */
{keys: ['h','i','q','w'], chars: [0xD2C0,0xB9BC,0xC5C6]}, /* 틀림없 */
{keys: ['a','k','w'], chars: [0xACF5,0xAE09]}, /* 공급 */
{keys: ['a','i','m','v'], chars: [0xB3D9,0xB8CC]}, /* 동료 */
{keys: ['a','h','k','s'], chars: [0xADE0,0xD615]}, /* 균형 */
{keys: ['b','f','n','y'], chars: [0xC0AC,0xBB34]}, /* 사무 */
{keys: ['f','n','p','y'], chars: [0xC0AC,0xBB34]}, /* 사무 */
{keys: ['k','l','r','w'], chars: [0xC811,0xADFC]}, /* 접근 */
{keys: ['a','f','k','n','r'], chars: [0xAC1C,0xC131]}, /* 개성 */
{keys: ['f','h','n','x'], chars: [0xC0AC,0xD68C,0xC801]}, /* 사회적 */
{keys: ['d','f','h','n','x'], chars: [0xD575,0xC2EC]}, /* 핵심 */
{keys: ['a','f','o','y'], chars: [0xBC29,0xBB38]}, /* 방문 */
{keys: ['.','f','k','s','x'], chars: [0xAD00,0xAC1D]}, /* 관객 */
{keys: ['f','k','s','v','x'], chars: [0xAD00,0xAC1D]}, /* 관객 */
{keys: ['a','i','l','x'], chars: [0xB3D9,0xC791]}, /* 동작 */
{keys: ['d','e','m','y'], chars: [0xBC00,0xB9AC]}, /* 밀리 */
{keys: ['b','f','l','n'], chars: [0xC22B,0xC790]}, /* 숫자 */
{keys: ['f','l','n','p'], chars: [0xC22B,0xC790]}, /* 숫자 */
{keys: ['j','l','s','v','x'], chars: [0xC67C,0xCABD]}, /* 왼쪽 */
{keys: ['.','j','l','s','x'], chars: [0xC67C,0xCABD]}, /* 왼쪽 */
{keys: ['l','m','v','x'], chars: [0xC624,0xB978,0xCABD]}, /* 오른쪽 */
{keys: ['b','c','l','n'], chars: [0xC911,0xC138]}, /* 중세 */
{keys: ['c','l','n','p'], chars: [0xC911,0xC138]}, /* 중세 */
{keys: ['d','f','i','n','x'], chars: [0xD0DD,0xC2DC]}, /* 택시 */
{keys: ['a','h','i','w'], chars: [0xB3D9,0xD569]}, /* 통합 */
{keys: ['c','k','n','s','v'], chars: [0xACC4,0xC0B0]}, /* 계산 */
{keys: ['d','k','m','v'], chars: [0xAF2C,0xB9AC]}, /* 꼬리 */
{keys: ['.','d','k','m'], chars: [0xAF2C,0xB9AC]}, /* 꼬리 */
{keys: ['a','j','n','x'], chars: [0xC591,0xC2DD]}, /* 양식 */
{keys: ['d','k','l','s'], chars: [0xC804,0xAE30]}, /* 전기 */
{keys: ['b','d','l','n','x'], chars: [0xC8FC,0xC2DD]}, /* 주식 */
{keys: ['d','l','n','p','x'], chars: [0xC8FC,0xC2DD]}, /* 주식 */
{keys: ['j','k','q','w'], chars: [0xB04A,0xC784,0xC5C6]}, /* 끊임없 */
{keys: ['a','n','x'], chars: [0xC0C1,0xC2DD]}, /* 상식 */
{keys: ['b','g','k'], chars: [0xAD81,0xAE08]}, /* 궁금 */
{keys: ['g','k','p'], chars: [0xAD81,0xAE08]}, /* 궁금 */
{keys: ['l','w'], chars: [0xC7A1,0xC9C0]}, /* 잡지 */
{keys: ['b','k','o','r'], chars: [0xAC70,0xBD80]}, /* 거부 */
{keys: ['k','o','p','r'], chars: [0xAC70,0xBD80]}, /* 거부 */
{keys: ['f','k','o','s'], chars: [0xBC18,0xAC11]}, /* 반갑 */
{keys: ['j','o','s','w'], chars: [0xBC95,0xC6D0]}, /* 법원 */
{keys: ['d','i','o','v'], chars: [0xBE44,0xB514,0xC624]}, /* 비디오 */
{keys: ['.','d','i','o'], chars: [0xBE44,0xB514,0xC624]}, /* 비디오 */
{keys: ['g','j','n','t'], chars: [0xC5F0,0xC2B5]}, /* 연습 */
{keys: ['d','e','f','l','n'], chars: [0xD654,0xC7A5,0xC2E4]}, /* 화장실 */
{keys: ['d','m','n','x'], chars: [0xC2DD,0xB7C9]}, /* 식량 */
{keys: ['e','h','n','z'], chars: [0xC2E4,0xD5D8]}, /* 실험 */
{keys: ['i','m','s','v'], chars: [0xD1A0,0xB860]}, /* 토론 */
{keys: ['k','w'], chars: [0xACE0,0xAE09]}, /* 고급 */
{keys: ['e','l','x'], chars: [0xC801,0xC808]}, /* 적절 */
{keys: ['d','f','l','n','z'], chars: [0xCC38,0xC0AC]}, /* 참새 */
{keys: ['f','l','o','v'], chars: [0xD654,0xC7A5,0xD488]}, /* 화장품 */
{keys: ['.','f','l','o'], chars: [0xD654,0xC7A5,0xD488]}, /* 화장품 */
{keys: ['f','g','k','j','q'], chars: [0xAE68,0xB057]}, /* 깨끗 */
{keys: ['f','s','u','y'], chars: [0xB18D,0xC0B0,0xBB3C]}, /* 농산물 */
{keys: ['b','f','j','u'], chars: [0xB208,0xC55E]}, /* 눈앞 */
{keys: ['f','j','p','u'], chars: [0xB208,0xC55E]}, /* 눈앞 */
{keys: ['b','f','i','m'], chars: [0xC544,0xBB34,0xB798,0xB3C4,0x20]}, /* 아무래도 */
{keys: ['f','i','m','p'], chars: [0xC544,0xBB34,0xB798,0xB3C4,0x20]}, /* 아무래도 */
{keys: ['b','k','n','t'], chars: [0xC5F0,0xAD6C,0xC18C]}, /* 연구소 */
{keys: ['k','n','p','t'], chars: [0xC5F0,0xAD6C,0xC18C]}, /* 연구소 */
{keys: ['a','d','f','k','n','v'], chars: [0xACE0,0xC0DD]}, /* 고생 */
{keys: ['.','a','d','f','k','n'], chars: [0xACE0,0xC0DD]}, /* 고생 */
{keys: ['a','k','m','t'], chars: [0xAC00,0xB839,0x20]}, /* 가령 */
{keys: ['i','k','r'], chars: [0xAC70,0xB300]}, /* 거대 */
{keys: ['g','k','m','s'], chars: [0xADFC,0xB85C]}, /* 근로 */
{keys: ['l','o','r','z'], chars: [0xBC31,0xD654,0xC810]}, /* 백화점 */
{keys: ['j','m','s','t'], chars: [0xC5EC,0xB860]}, /* 여론 */
{keys: ['g','j','o','v'], chars: [0xC758,0xBCF5]}, /* 의복 */
{keys: ['.','g','j','o'], chars: [0xC758,0xBCF5]}, /* 의복 */
{keys: ['b','e','l','o'], chars: [0xCD9C,0xBC1C]}, /* 출발 */
{keys: ['h','n','r','s','t'], chars: [0xD604,0xC2E4,0xC801]}, /* 현실적 */
{keys: ['c','f','h','l'], chars: [0xD654,0xC81C]}, /* 화제 */
{keys: ['a','k','x'], chars: [0xACF5,0xACA9]}, /* 공격 */
{keys: ['b','e','f','y'], chars: [0xBB3C,0xAC00]}, /* 물가 */
{keys: ['e','f','p','y'], chars: [0xBB3C,0xAC00]}, /* 물가 */
{keys: ['a','b','l','n'], chars: [0xC18C,0xC911]}, /* 소중 */
{keys: ['b','g','j'], chars: [0xC758,0xBB38]}, /* 의문 */
{keys: ['g','j','p'], chars: [0xC758,0xBB38]}, /* 의문 */
{keys: ['d','e','k','m'], chars: [0xD0AC,0xB85C]}, /* 킬로 */
{keys: ['b','f','k','n'], chars: [0xAC00,0xC218]}, /* 가수 */
{keys: ['f','k','n','p'], chars: [0xAC00,0xC218]}, /* 가수 */
{keys: ['a','n','o','x'], chars: [0xBC29,0xC1A1,0xAD6D]}, /* 방송국 */
{keys: ['f','j','w','x'], chars: [0xC555,0xB825]}, /* 압력 */
{keys: ['d','j','s','x'], chars: [0xC778,0xB825]}, /* 인력 */
{keys: ['c','g','j','k'], chars: [0xC608,0xAE08]}, /* 예금 */
{keys: ['d','h','j','w'], chars: [0xC785,0xD559]}, /* 입학 */
{keys: ['a','l','m','r','v'], chars: [0xCC28,0xB7C9]}, /* 차량 */
{keys: ['.','a','l','m','r'], chars: [0xCC28,0xB7C9]}, /* 차량 */
{keys: ['b','e','f','h','l'], chars: [0xCD9C,0xC0B0]}, /* 출산 */
{keys: ['e','f','h','l','p'], chars: [0xCD9C,0xC0B0]}, /* 출산 */
{keys: ['b','l','n','x'], chars: [0xC120,0xC9C4,0xAD6D]}, /* 선진국 */
{keys: ['d','f','l','o','s'], chars: [0xC7AC,0xD310]}, /* 재판 */
{keys: ['a','f','l','s','y'], chars: [0xCC3D,0xBB38]}, /* 창문 */
{keys: ['l','n','r','x'], chars: [0xCC38,0xC11D]}, /* 참석 */
{keys: ['b','e','f','h','o'], chars: [0xBC1C,0xD718]}, /* 발휘 */
{keys: ['e','f','h','o','p'], chars: [0xBC1C,0xD718]}, /* 발휘 */
{keys: ['a','k','l','w'], chars: [0xC911,0xC18C,0xAE30,0xC5C5]}, /* 중소기업 */
{keys: ['l','r','w','x'], chars: [0xC9C1,0xC811,0xC801]}, /* 직접적 */
{keys: ['h','w','x'], chars: [0xD611,0xB825]}, /* 협력 */
{keys: ['f','g','k','n'], chars: [0xAC00,0xC2A4]}, /* 가스 */
{keys: ['f','h','v','y'], chars: [0xB9CC,0xD654]}, /* 만화 */
{keys: ['.','f','h','y'], chars: [0xB9CC,0xD654]}, /* 만화 */
{keys: ['e','l','n','x'], chars: [0xC0B4,0xC9DD,0x20]}, /* 살짝 */
{keys: ['e','k','l','s'], chars: [0xAD00,0xCC30]}, /* 관찰 */
{keys: ['u','x'], chars: [0xB140,0xC11D]}, /* 녀석 */
{keys: ['g','i','j','r'], chars: [0xB4DC,0xB514,0xC5B4,0x20]}, /* 드디어 */
{keys: ['g','k','l','x'], chars: [0xC790,0xADF9]}, /* 자극 */
{keys: ['d','l','n'], chars: [0xC9C0,0xC2DC]}, /* 지시 */
{keys: ['c','h','l','t'], chars: [0xD615,0xC81C]}, /* 형제 */
{keys: ['f','h','m','v'], chars: [0xD654,0xB824]}, /* 화려 */
{keys: ['.','f','h','m'], chars: [0xD654,0xB824]}, /* 화려 */
{keys: ['c','k','n','x'], chars: [0xC138,0xACC4,0xC801]}, /* 세계적 */
{keys: ['d','e','l','o'], chars: [0xBCF8,0xC9C8]}, /* 본질 */
{keys: ['d','n','o','s'], chars: [0xC2E0,0xBD84]}, /* 신분 */
{keys: ['b','d','n','o'], chars: [0xC2E0,0xBD80]}, /* 신부 */
{keys: ['d','n','o','p'], chars: [0xC2E0,0xBD80]}, /* 신부 */
{keys: ['e','j','l','w'], chars: [0xC878,0xC5C5]}, /* 졸업 */
{keys: ['h','o','z'], chars: [0xD3EC,0xD568]}, /* 포함 */
{keys: ['a','f','h','m'], chars: [0xD638,0xB791,0xC774]}, /* 호랑이 */
{keys: ['c','k','n','z'], chars: [0xC138,0xAE08]}, /* 세금 */
{keys: ['a','j','r','t'], chars: [0xC601,0xC591]}, /* 영양 */
{keys: ['d','i','m','w'], chars: [0xB3C5,0xB9BD]}, /* 독립 */
{keys: ['d','f','i','n','v'], chars: [0xB610,0xB2E4,0xC2DC,0x20]}, /* 또다시 */
{keys: ['.','d','f','i','n'], chars: [0xB610,0xB2E4,0xC2DC,0x20]}, /* 또다시 */
{keys: ['b','f','j','o','s'], chars: [0xC704,0xBC18]}, /* 위반 */
{keys: ['f','j','o','p','s'], chars: [0xC704,0xBC18]}, /* 위반 */
{keys: ['a','d','f','n','o'], chars: [0xD3C9,0xC0DD]}, /* 평생 */
{keys: ['a','b','f','k','l'], chars: [0xAD11,0xC8FC]}, /* 광주 */
{keys: ['a','f','k','l','p'], chars: [0xAD11,0xC8FC]}, /* 광주 */
{keys: ['f','j','m','r'], chars: [0xB7EC,0xC2DC,0xC544]}, /* 러시아 */
{keys: ['f','h','u','z'], chars: [0xB0A8,0xD55C]}, /* 남한 */
{keys: ['f','j','k','s','x'], chars: [0xAD00,0xC545]}, /* 관악 */
{keys: ['a','h','s'], chars: [0xD55C,0xAC15]}, /* 한강 */
{keys: ['g','j','m','t'], chars: [0xC73C,0xB824]}, /* 으려 */
{keys: ['c','d','j','k'], chars: [0xAE30,0xC5D0,0x20]}, /* 기에 */
{keys: ['b','i','k','r','s'], chars: [0xB354,0xAD70,0x2E,0x20]}, /* 더군. */
{keys: ['i','k','p','r','s'], chars: [0xB354,0xAD70,0x2E,0x20]}, /* 더군. */
{keys: ['d','f','i','u'], chars: [0xB2E4,0xB2C8]}, /* 다니 */
{keys: ['g','s','u','v'], chars: [0xACE0,0xB294,0x20]}, /* 고는 */
{keys: ['.','g','s','u'], chars: [0xACE0,0xB294,0x20]}, /* 고는 */
{keys: ['f','l','y'], chars: [0xB9C8,0xC790,0x20]}, /* 마자 */
{keys: ['a','d','k','l','v'], chars: [0xAD49,0xC7A5]}, /* 굉장 */
{keys: ['.','a','d','k','l'], chars: [0xAD49,0xC7A5]}, /* 굉장 */
{keys: ['k','l','t','x'], chars: [0xC790,0xACA9]}, /* 자격 */
{keys: ['c','f','l','x'], chars: [0xC81C,0xC791]}, /* 제작 */
{keys: ['a','l','w'], chars: [0xC9D1,0xC911]}, /* 집중 */
{keys: ['a','h','l','w'], chars: [0xC885,0xD569]}, /* 종합 */
{keys: ['e','f','l','y'], chars: [0xC8FC,0xB9D0]}, /* 주말 */
{keys: ['b','f','k','m'], chars: [0xAC00,0xB8E8]}, /* 가루 */
{keys: ['f','k','m','p'], chars: [0xAC00,0xB8E8]}, /* 가루 */
{keys: ['f','j','m','x'], chars: [0xC5F0,0xB77D]}, /* 연락 */
{keys: ['a','c','i','l','v'], chars: [0xB3D9,0xC81C]}, /* 통제 */
{keys: ['c','f','h','l','s'], chars: [0xC81C,0xD55C]}, /* 제한 */
{keys: ['c','h','j','r'], chars: [0xD5E4,0xC5B4]}, /* 헤어 */
{keys: ['e','j','l','s'], chars: [0xCD9C,0xC5F0]}, /* 출연 */
{keys: ['b','f','l','y'], chars: [0xB9C8,0xC8FC,0x20]}, /* 마주 */
{keys: ['f','l','p','y'], chars: [0xB9C8,0xC8FC,0x20]}, /* 마주 */
{keys: ['a','c','f','k','l'], chars: [0xAC15,0xC81C]}, /* 강제 */
{keys: ['d','f','n','u'], chars: [0xB09A,0xC2DC]}, /* 낚시 */
{keys: ['b','l','s','y'], chars: [0xBB38,0xC790]}, /* 문자 */
{keys: ['k','s','z'], chars: [0xAD00,0xB150]}, /* 관념 */
{keys: ['e','n','o','s'], chars: [0xC2E0,0xBC1C]}, /* 신발 */
{keys: ['d','k','l','z'], chars: [0xAC10,0xCE58]}, /* 김치 */
{keys: ['a','d','i','k'], chars: [0xB3D9,0xAE30]}, /* 동기 */
{keys: ['e','f','g','i','n'], chars: [0xC2A4,0xD0C0,0xC77C]}, /* 스타일 */
{keys: ['l','n','r','z'], chars: [0xC2DC,0xC810]}, /* 시점 */
{keys: ['a','l','n','z'], chars: [0xC2EC,0xC7A5]}, /* 심장 */
{keys: ['d','l','n','z'], chars: [0xC810,0xC2EC]}, /* 점심 */
{keys: ['j','i','w'], chars: [0xB3C4,0xC785]}, /* 도입 */
{keys: ['g','j','m','z'], chars: [0xC74C,0xB8CC]}, /* 음료 */
{keys: ['f','g','j','l'], chars: [0xC758,0xC790]}, /* 의자 */
{keys: ['f','g','j','l'], chars: [0xC758,0xC790]}, /* 의자 */
{keys: ['c','f','h','y'], chars: [0xCE74,0xBA54,0xB77C]}, /* 카메라 */
{keys: ['d','m','o'], chars: [0xD3B8,0xB9AC]}, /* 편리 */
{keys: ['d','e','k','l'], chars: [0xAC70,0xCE60]}, /* 거칠 */
{keys: ['f','u','y'], chars: [0xB098,0xB9C8,0x20]}, /* 나마 */
{keys: ['f','o','u','z'], chars: [0xB0A8,0xBD80]}, /* 남부 */
{keys: ['e','m','v','y'], chars: [0xBAB0,0xB798,0x20]}, /* 몰래 */
{keys: ['g','k','l','v'], chars: [0xC870,0xADF8]}, /* 조그 */
{keys: ['.','g','k','l'], chars: [0xC870,0xADF8]}, /* 조그 */
{keys: ['b','e','k','l'], chars: [0xC904,0xAE30]}, /* 줄기 */
{keys: ['f','g','k','l','x'], chars: [0xADF9,0xC7A5]}, /* 극장 */
{keys: ['d','g','k','o','x'], chars: [0xBE44,0xADF9]}, /* 비극 */
{keys: [';','u','w'], chars: [0xB192,0xC774]}, /* 높이 */
{keys: ['f','h','n','s'], chars: [0xD55C,0xC228]}, /* 한숨 */
{keys: ['a','f','l','n','s'], chars: [0xC120,0xC7A5]}, /* 선장 */
{keys: ['g','i','k','v'], chars: [0xCF54,0xB4DC]}, /* 코드 */
{keys: ['.','g','i','k'], chars: [0xCF54,0xB4DC]}, /* 코드 */
{keys: ['h','r','t','u'], chars: [0xCCAD,0xB144]}, /* 청년 */
{keys: ['d','n','u'], chars: [0xC2DC,0xB0B4]}, /* 시내 */
{keys: ['d','e','n','u'], chars: [0xC2E4,0xB0B4]}, /* 실내 */
{keys: ['e','j','k','s'], chars: [0xC5F0,0xACB0]}, /* 연결 */
{keys: ['c','k','r'], chars: [0xACC4,0xC57D]}, /* 계약 */
{keys: ['b','g','k','o'], chars: [0xBD80,0xB044]}, /* 부끄 */
{keys: ['g','k','o','p'], chars: [0xBD80,0xB044]}, /* 부끄 */
{keys: ['b','h','n'], chars: [0xC218,0xD589]}, /* 수행 */
{keys: ['a','d','l','n','s'], chars: [0xC2E0,0xCCAD]}, /* 신청 */
{keys: ['b','f','o'], chars: [0xBC14,0xC704]}, /* 바위 */
{keys: ['f','o','p'], chars: [0xBC14,0xC704]}, /* 바위 */
{keys: ['c','l','r','x'], chars: [0xC804,0xCCB4,0xC801]}, /* 전체적 */
{keys: ['h','j','s','t'], chars: [0xD3B8,0xC548]}, /* 편안 */
{keys: ['d','f','m','x','y'], chars: [0xB9E4,0xB825]}, /* 매력 */
{keys: ['a','b','f','l','o'], chars: [0xBD80,0xC7A5]}, /* 부장 */
{keys: ['a','f','l','o','p'], chars: [0xBD80,0xC7A5]}, /* 부장 */
{keys: ['d','m','n','z'], chars: [0xC2EC,0xB9AC]}, /* 심리 */
{keys: ['e','f','i','l'], chars: [0xC804,0xB2EC]}, /* 전달 */
{keys: ['a','h','n','t'], chars: [0xD615,0xC0AC]}, /* 형사 */
{keys: ['f','i','k','z'], chars: [0xAC10,0xB3D9]}, /* 감동 */
{keys: ['b','f','m','y'], chars: [0xB9C8,0xB8E8]}, /* 마루 */
{keys: ['f','m','p','y'], chars: [0xB9C8,0xB8E8]}, /* 마루 */
{keys: ['a','l','n','x'], chars: [0xC131,0xC801]}, /* 성적 */
{keys: ['e','l','n'], chars: [0xC194,0xC9C1]}, /* 솔직 */
{keys: ['i','l','r','s','x'], chars: [0xC804,0xB3D9,0xC801]}, /* 전통적 */
{keys: ['d','i','n','v'], chars: [0xC2DC,0xB3C4]}, /* 시도 */
{keys: ['.','d','i','n'], chars: [0xC2DC,0xB3C4]}, /* 시도 */
{keys: ['f','k','l','z'], chars: [0xCC38,0xAC00]}, /* 참가 */
{keys: ['d','f','h','y'], chars: [0xCE58,0xB9C8]}, /* 치마 */
{keys: ['a','h','i','x'], chars: [0xD2B9,0xC815]}, /* 특정 */
{keys: ['d','k','n','v','x'], chars: [0xACF5,0xC2DD]}, /* 공식 */
{keys: ['.','d','k','n','x'], chars: [0xACF5,0xC2DD]}, /* 공식 */
{keys: ['a','d','k','l','s'], chars: [0xAE34,0xC7A5]}, /* 긴장 */
{keys: ['a','k','l','s','v'], chars: [0xC8FC,0xC778,0xACF5]}, /* 주인공 */
{keys: ['l','o','r'], chars: [0xBD80,0xCC98]}, /* 부처 */
{keys: ['e','i','l'], chars: [0xC808,0xB300]}, /* 절대 */
{keys: ['f','o','r','y'], chars: [0xD45C,0xBA74]}, /* 표면 */
{keys: ['.','o','v','y'], chars: [0xD45C,0xBA74]}, /* 표면 */
{keys: ['k','r','s','x'], chars: [0xAC1D,0xAD00,0xC801]}, /* 객관적 */
{keys: ['a','k','l','r','t'], chars: [0xACBD,0xC81C,0xC801]}, /* 경제적 */
{keys: ['k','n','r','x'], chars: [0xAD6C,0xC11D]}, /* 구석 */
{keys: ['a','i','z'], chars: [0xB2F4,0xB2F9]}, /* 담당 */
{keys: ['d','f','i','o'], chars: [0xBD80,0xB300]}, /* 부대 */
{keys: ['h','n','v'], chars: [0xCCAD,0xC18C]}, /* 청소 */
{keys: ['a','h','n','r'], chars: [0xC2DC,0xCCAD]}, /* 시청 */
{keys: ['a','g','k','l','r'], chars: [0xC99D,0xAC70]}, /* 증거 */
{keys: ['c','e','k','l','r'], chars: [0xACC4,0xC808]}, /* 계절 */
{keys: ['e','g','o','u'], chars: [0xBC14,0xB298]}, /* 바늘 */
{keys: ['d','f','k','l','s'], chars: [0xC804,0xAC1C]}, /* 전개 */
{keys: ['l','v','w','x'], chars: [0xC811,0xCD09]}, /* 접촉 */
{keys: ['.','l','w','x'], chars: [0xC811,0xCD09]}, /* 접촉 */
{keys: ['b','f','h','o','s'], chars: [0xD6C4,0xBC18]}, /* 후반 */
{keys: ['f','h','o','p','s'], chars: [0xD6C4,0xBC18]}, /* 후반 */
{keys: ['d','e','h','j'], chars: [0xC77C,0xCE58]}, /* 일치 */
{keys: ['a','i','s'], chars: [0xB2F9,0xC5F0]}, /* 당연 */
{keys: ['d','i','o'], chars: [0xB300,0xBE44]}, /* 대비 */
{keys: ['l','u','v','x'], chars: [0xB0A8,0xCABD]}, /* 남쪽 */
{keys: ['a','b','f','n'], chars: [0xC218,0xC0C1]}, /* 수상 */
{keys: ['a','f','n','p'], chars: [0xC218,0xC0C1]}, /* 수상 */
{keys: ['k','l','s','z'], chars: [0xC7A0,0xAE50]}, /* 잠깐 */
{keys: ['i','l','x'], chars: [0xC801,0xB2F9]}, /* 적당 */
{keys: ['l','n','v','x'], chars: [0xC9C0,0xC18D]}, /* 지속 */
{keys: ['b','k','o','s'], chars: [0xAD6C,0xBD84]}, /* 구분 */
{keys: ['d','g','k','l','z'], chars: [0xAE08,0xC9C0]}, /* 금지 */
{keys: ['b','g','y'], chars: [0xC758,0xBB34]}, /* 의무 */
{keys: ['g','p','y'], chars: [0xC758,0xBB34]}, /* 의무 */
{keys: ['h','o','s','x'], chars: [0xD55C,0xBCF5]}, /* 한복 */
{keys: ['k','l','s','x'], chars: [0xAC74,0xCD95]}, /* 건축 */
{keys: ['k','o','s','z'], chars: [0xBC14,0xAE65]}, /* 바깥 */
{keys: ['d','l','o'], chars: [0xBC14,0xC9C0]}, /* 바지 */
{keys: ['f','k','o','s','v'], chars: [0xBCF4,0xAD00]}, /* 보관 */
{keys: ['.','f','k','o','s'], chars: [0xBCF4,0xAD00]}, /* 보관 */
{keys: [';','e','i','o'], chars: [0xBD80,0xB52A]}, /* 부딪 */
{keys: ['h','j','w'], chars: [0xC5F0,0xD569]}, /* 연합 */
{keys: ['f','l','n','r','s'], chars: [0xC0AC,0xC804]}, /* 사전 */
{keys: ['d','l','n','v','z'], chars: [0xC870,0xC2EC]}, /* 조심 */
{keys: ['.','d','l','n','z'], chars: [0xC870,0xC2EC]}, /* 조심 */
{keys: ['f','o','s','u'], chars: [0xBE44,0xB09C]}, /* 비난 */
{keys: ['d','k','n','s','v'], chars: [0xC2E0,0xACE0]}, /* 신고 */
{keys: ['.','d','k','n','s'], chars: [0xC2E0,0xACE0]}, /* 신고 */
{keys: ['a','c','f','j','n'], chars: [0xC608,0xC0C1]}, /* 예상 */
{keys: ['b','l','r','y'], chars: [0xC8FC,0xBA39]}, /* 주먹 */
{keys: ['l','p','r','y'], chars: [0xC8FC,0xBA39]}, /* 주먹 */
{keys: ['a','b','k','t'], chars: [0xAD6C,0xACBD]}, /* 구경 */
{keys: ['a','k','p','t'], chars: [0xAD6C,0xACBD]}, /* 구경 */
{keys: ['b','g','k','s'], chars: [0xADFC,0xBB34]}, /* 근무 */
{keys: ['g','k','p','s'], chars: [0xADFC,0xBB34]}, /* 근무 */
{keys: ['g','n','v'], chars: [0xC18C,0xC2A4]}, /* 소스 */
{keys: ['.','g','n'], chars: [0xC18C,0xC2A4]}, /* 소스 */
{keys: ['a','f','l','m'], chars: [0xC790,0xB791]}, /* 자랑 */
{keys: ['k','l','r','v','x'], chars: [0xACFC,0xD559,0xC801]}, /* 과학적 */
{keys: ['.','k','l','r','x'], chars: [0xACFC,0xD559,0xC801]}, /* 과학적 */
{keys: ['f','k','l','v','x'], chars: [0xACFC,0xD559,0xC790]}, /* 과학자 */
{keys: ['.','f','k','l','x'], chars: [0xACFC,0xD559,0xC790]}, /* 과학자 */
{keys: ['a','g','m','n'], chars: [0xC2B9,0xB9AC]}, /* 승리 */
{keys: ['d','f','k','l','x'], chars: [0xAC1C,0xC778,0xC801]}, /* 개인적 */
{keys: ['b','l','n','v'], chars: [0xC18C,0xC8FC]}, /* 소주 */
{keys: ['f','g','h','j'], chars: [0xC758,0xD559]}, /* 의학 */
{keys: ['e','l','s'], chars: [0xC9C4,0xCD9C]}, /* 진출 */
{keys: ['c','k','l','r'], chars: [0xC81C,0xAC70]}, /* 제거 */
{keys: ['g','k','m','z'], chars: [0xAE30,0xB984]}, /* 기름 */
{keys: ['b','g','i','n'], chars: [0xD2B9,0xC218]}, /* 특수 */
{keys: ['g','i','n','p'], chars: [0xD2B9,0xC218]}, /* 특수 */
{keys: ['e','x','y'], chars: [0xACE8,0xBAA9]}, /* 골목 */
{keys: ['f','h','j','x'], chars: [0xC720,0xD559]}, /* 유학 */
{keys: ['e','l','o','r'], chars: [0xCC98,0xBC8C]}, /* 처벌 */
{keys: ['c','f','k','o'], chars: [0xCE74,0xD398]}, /* 카페 */
{keys: ['i','k','v'], chars: [0xAC80,0xD1A0]}, /* 검토 */
{keys: ['m','v','y'], chars: [0xBAA8,0xB798]}, /* 모래 */
{keys: ['b','f','i','n'], chars: [0xB2E4,0xC218]}, /* 다수 */
{keys: ['f','i','n','p'], chars: [0xB2E4,0xC218]}, /* 다수 */
{keys: ['h','n','r','x'], chars: [0xD574,0xC11D]}, /* 해석 */
{keys: ['g','j','k','r','s'], chars: [0xADFC,0xC6D0]}, /* 근원 */
{keys: ['b','f','i','o','x'], chars: [0xBD80,0xD0C1]}, /* 부탁 */
{keys: ['f','i','o','p','x'], chars: [0xBD80,0xD0C1]}, /* 부탁 */
{keys: ['d','e','n','o'], chars: [0xC2E4,0xD328]}, /* 실패 */
{keys: ['b','j','s','t'], chars: [0xC6B0,0xC5F0]}, /* 우연 */
{keys: ['j','p','s','t'], chars: [0xC6B0,0xC5F0]}, /* 우연 */
{keys: ['a','l','m','v'], chars: [0xCD1D,0xB9AC]}, /* 총리 */
{keys: [';','q','u'], chars: [0xB208,0xBE5B]}, /* 눈빛 */
{keys: ['j','s','u','v'], chars: [0xB17C,0xC758]}, /* 논의 */
{keys: ['b','g','j','n'], chars: [0xC6B0,0xC2B9]}, /* 우승 */
{keys: ['g','j','n','p'], chars: [0xC6B0,0xC2B9]}, /* 우승 */
{keys: ['g','k','l','r','s'], chars: [0xC99D,0xAD8C]}, /* 증권 */
{keys: ['g','k','l','r','s'], chars: [0xC99D,0xAD8C]}, /* 증권 */
{keys: ['a','g','h','y'], chars: [0xD765,0xBBF8]}, /* 흥미 */
{keys: ['e','o','u'], chars: [0xBE44,0xB2D0]}, /* 비닐 */
{keys: ['a','d','f','n','y'], chars: [0xC0DD,0xBB3C]}, /* 생물 */
{keys: ['e','j','l'], chars: [0xC77C,0xC815]}, /* 일정 */
{keys: ['d','m','o','v'], chars: [0xD53C,0xB85C]}, /* 피로 */
{keys: ['.','d','m','o'], chars: [0xD53C,0xB85C]}, /* 피로 */
{keys: ['d','n','o','v'], chars: [0xBE44,0xB85C,0xC18C,0x20]}, /* 비로소 */
{keys: ['.','d','n','o'], chars: [0xBE44,0xB85C,0xC18C,0x20]}, /* 비로소 */
{keys: ['b','e','f','l'], chars: [0xC790,0xC728]}, /* 자율 */
{keys: ['e','f','l','p'], chars: [0xC790,0xC728]}, /* 자율 */
{keys: ['b','l','o','r','s'], chars: [0xC804,0xBD80]}, /* 전부 */
{keys: ['l','o','p','r','s'], chars: [0xC804,0xBD80]}, /* 전부 */
{keys: ['d','i','l','s'], chars: [0xC9C4,0xB2E8]}, /* 진단 */
{keys: ['b','i','k'], chars: [0xAD6C,0xB450]}, /* 구두 */
{keys: ['a','f','l','o','v'], chars: [0xBCF4,0xC7A5]}, /* 보장 */
{keys: ['.','a','f','l','o'], chars: [0xBCF4,0xC7A5]}, /* 보장 */
{keys: ['f','k','n','v'], chars: [0xC0AC,0xACFC]}, /* 사과 */
{keys: ['.','f','k','n'], chars: [0xC0AC,0xACFC]}, /* 사과 */
{keys: ['b','d','j','m'], chars: [0xC720,0xB9AC]}, /* 유리 */
{keys: ['d','j','m','p'], chars: [0xC720,0xB9AC]}, /* 유리 */
{keys: ['a','g','i','m'], chars: [0xB4F1,0xB85D]}, /* 등록 */
{keys: ['a','g','i','m'], chars: [0xB4F1,0xB85D]}, /* 등록 */
{keys: ['c','f','j','o'], chars: [0xC608,0xBC29]}, /* 예방 */
{keys: ['d','f','h','m','w'], chars: [0xD569,0xB9AC]}, /* 합리 */
{keys: ['g','i','k','r','w'], chars: [0xAC70,0xB4ED]}, /* 거듭 */
{keys: ['a','l','o','r'], chars: [0xC815,0xBE44]}, /* 정비 */
{keys: ['e','g','h','j'], chars: [0xC774,0xD2C0]}, /* 이틀 */
{keys: ['e','o','s'], chars: [0xBD88,0xC548]}, /* 불안 */
{keys: ['e','h','o','s'], chars: [0xBD88,0xD3B8]}, /* 불편 */
{keys: ['e','f','o','y'], chars: [0xBD88,0xC548]}, /* 불만 */
{keys: [';','h','s'], chars: [0xD558,0xC597]}, /* 하얗 */
//1701 약어 추가
{keys: ['g','h','u'], chars: [0xD558,0xB294,0x20]}, /* 하는 */
{keys: ['g','i','k','r'], chars: [0xB728,0xAC70,0xC6B4,0x20]}, /* 뜨거운 */
{keys: ['f','g','u'], chars: [0xB098,0xB294,0x20]}, /* 나는 */
{keys: ['c','u','y'], chars: [0xB9CC,0xB098,0xAC8C,0x20]}, /* 만나게 */
{keys: ['c','f','u'], chars: [0xB098,0xAC8C,0x20]}, /* 나게 */
{keys: ['d','i','u','v'], chars: [0xB418,0xB294,0x20]}, /* 되는 */
{keys: ['.','d','i','u'], chars: [0xB418,0xB294,0x20]}, /* 되는 */
{keys: ['b','j','m','s'], chars: [0xB85C,0xC6B4,0x20]}, /* 로운 */
{keys: ['g','u','y'], chars: [0xBAA8,0xB974,0xB294,0x20]}, /* 모르는 */
{keys: ['g','m','u'], chars: [0xB974,0xB294,0x20]}, /* 르는 */
{keys: ['d','g','m','u'], chars: [0xB9AC,0xB294,0x20]}, /* 리는 */
{keys: ['q','j','u'], chars: [0xC788,0xB294,0x20]}, /* 있는 */
{keys: [';','c','d','j'], chars: [0xC788,0xAC8C,0x20]}, /* 있게 */
{keys: ['j','q','u','w'], chars: [0xC5C6,0xB294,0x20]}, /* 없는 */
{keys: ['g','h','s','u'], chars: [0xCE58,0xB294,0x20]}, /* 치는 */
{keys: ['h','s','u'], chars: [0xD0A4,0xB294,0x20]}, /* 키는 */
{keys: ['c','f','h'], chars: [0xD558,0xAC8C,0x20]}, /* 하게 */
{keys: ['g','l','v'], chars: [0xC88B,0xC740,0x20]}, /* 좋은 */
{keys: ['.','g','l'], chars: [0xC88B,0xC740,0x20]}, /* 좋은 */
{keys: ['f','k','m','s'], chars: [0xCEE4,0xB2E4,0xB780,0x20]}, /* 커다란 */
{keys: ['k','s'], chars: [0xAE30,0xB294,0x20]}, /* 기는 */
{keys: ['f','g','k'], chars: [0xAC00,0xB294,0x20]}, /* 가는 */
{keys: ['f','g','k','z'], chars: [0xAC19,0xC740,0x20]}, /* 같은 */
{keys: ['b','g','j','k'], chars: [0xAFB8,0xB294,0x20]}, /* 꾸는 */
{keys: ['g','j','k','p'], chars: [0xAFB8,0xB294,0x20]}, /* 꾸는 */
{keys: ['d','f','g','u'], chars: [0xB0B4,0xB294,0x20]}, /* 내는 */
{keys: ['g','i','u'], chars: [0xB4DC,0xB294,0x20]}, /* 드는 */
{keys: ['f','g','m'], chars: [0xB77C,0xB294,0x20]}, /* 라는 */
{keys: ['c','f','m'], chars: [0xB77C,0xAC8C,0x20]}, /* 라게 */
{keys: ['j','m','r'], chars: [0xB7EC,0xC6B4,0x20]}, /* 러운 */
{keys: ['j','m','t'], chars: [0xB824,0xC6B4,0x20]}, /* 려운 */
{keys: ['d','f','m','n','s'], chars: [0xC0C8,0xB85C,0xC6B4,0x20]}, /* 새로운 */
{keys: ['b','h','s','y'], chars: [0xD765,0xBBF8,0xB85C,0xC6B4,0x20]}, /* 흥미로운 */
{keys: ['b','g','m'], chars: [0xB8E8,0xB294,0x20]}, /* 루는 */
{keys: ['g','m','p'], chars: [0xB8E8,0xB294,0x20]}, /* 루는 */
{keys: ['o','u','v'], chars: [0xBCF4,0xB294,0x20]}, /* 보는 */
{keys: ['c','k','o'], chars: [0xC058,0xAC8C,0x20]}, /* 쁘게 */
{keys: ['g','n','r'], chars: [0xC11C,0xB294,0x20]}, /* 서는 */
{keys: ['k','n','w'], chars: [0xC27D,0xAC8C,0x20]}, /* 쉽게 */
{keys: ['g','m','n','r'], chars: [0xC2A4,0xB7EC]}, /* 스러 */
{keys: ['g','m','n','r','s'], chars: [0xC2A4,0xB7EC,0xC6B4,0x20]}, /* 스러운 */
{keys: ['d','g','n','u'], chars: [0xC2DC,0xB294,0x20]}, /* 시는 */
{keys: ['a','f','g','j'], chars: [0xC54A,0xC740,0x20]}, /* 않은 */
{keys: ['a','f','g','u'], chars: [0xC54A,0xB294,0x20]}, /* 않는 */
{keys: ['j','s','u'], chars: [0xC624,0xB294,0x20]}, /* 오는 */
{keys: ['b','j','u'], chars: [0xC6B0,0xB294,0x20]}, /* 우는 */
{keys: ['g','j','u'], chars: [0xC774,0xB294,0x20]}, /* 이는 */
{keys: ['b','g','l'], chars: [0xC8FC,0xB294,0x20]}, /* 주는 */
{keys: ['g','l','p'], chars: [0xC8FC,0xB294,0x20]}, /* 주는 */
{keys: ['g','l','s','u'], chars: [0xC9C0,0xB294,0x20]}, /* 지는 */
{keys: ['c','d','h','l'], chars: [0xCE58,0xAC8C,0x20]}, /* 치게 */
{keys: ['g','k','o'], chars: [0xD504,0xAC8C,0x20]}, /* 프게 */
{keys: ['c','h','u'], chars: [0xD558,0xB294,0xB370]}, /* 하는데 */
{keys: ['c','g','h','k'], chars: [0xD06C,0xAC8C,0x20]}, /* 크게 */
{keys: ['f','g','o'], chars: [0xBC1B,0xC740,0x20]}, /* 받은 */
{keys: ['f','g','n'], chars: [0xC0AC,0xB294,0x20]}, /* 사는 */
{keys: ['g','r','y'], chars: [0xBA39,0xB294,0x20]}, /* 먹는 */
{keys: [';','n','w'], chars: [0xC2F6,0xC740,0x20]}, /* 싶은 */
{keys: ['f','g','l'], chars: [0xC791,0xC740,0x20]}, /* 작은 */
{keys: ['g','u','v'], chars: [0xB192,0xC740,0x20]}, /* 높은 */
{keys: ['.','g','u'], chars: [0xB192,0xC740,0x20]}, /* 높은 */
{keys: ['b','f','i','s'], chars: [0xB2E4,0xC6B4]}, /* 다운 */
{keys: ['f','i','p','s'], chars: [0xB2E4,0xC6B4]}, /* 다운 */
{keys: ['c','k','m','v'], chars: [0xB86D,0xAC8C,0x20]}, /* 롭게 */
{keys: ['g','s','u','y'], chars: [0xB9DE,0xB294,0x20]}, /* 맞는 */
{keys: ['m','n','w'], chars: [0xC2A4,0xB7FD]}, /* 스럽 */
{keys: ['g','n','u'], chars: [0xC4F0,0xB294,0x20]}, /* 쓰는 */
{keys: ['f','g','h','u'], chars: [0xCC3E,0xB294,0x20]}, /* 찾는 */
{keys: ['f','g','j','s'], chars: [0xC544,0xB294,0x20]}, /* 아는 */
{keys: [';','k','w'], chars: [0xAE4A,0xC740,0x20]}, /* 깊은 */
{keys: ['e','u','w'], chars: [0xB113,0xC740,0x20]}, /* 넓은 */
{keys: ['e','f','g','j','l'], chars: [0xC9E7,0xC740,0x20]}, /* 짧은 */
{keys: ['e','f','g','u'], chars: [0xB0AE,0xC740,0x20]}, /* 낮은 */
{keys: ['j','q','r','u','w'], chars: [0xC5C6,0xC560,0xB294,0x20]}, /* 없애는 */
{keys: ['g','k','r','z'], chars: [0xAC80,0xC740,0x20]}, /* 검은 */
{keys: ['e','g','k','l'], chars: [0xC990,0xAC70,0xC6B4,0x20]}, /* 즐거운 */
{keys: ['q','h','k'], chars: [0xD558,0xACA0]}, /* 하겠 */
{keys: ['q','h','j'], chars: [0xD558,0xC600]}, /* 하였 */
{keys: ['d','h','n','s'], chars: [0xD558,0xC2E0,0x20]}, /* 하신 */
{keys: ['q','h','n'], chars: [0xD558,0xC168]}, /* 하셨 */
{keys: ['q','i','k'], chars: [0xB418,0xACA0]}, /* 되겠 */
{keys: ['q','i','j'], chars: [0xB418,0xC5C8]}, /* 되었 */
{keys: ['e','i','j'], chars: [0xB4E4,0xC758,0x20]}, /* 들의 */
{keys: ['e','i'], chars: [0xB4E4,0xC774,0x20]}, /* 들이 */
{keys: ['e','i','s'], chars: [0xB4E4,0xC740,0x20]}, /* 들은 */
{keys: ['g','l','m'], chars: [0xC801,0xC73C,0xB85C,0x20]}, /* 적으로 */
{keys: ['l','s','x'], chars: [0xC801,0xC778,0x20]}, /* 적인 */
{keys: ['e','f','h','n'], chars: [0xD560,0x20,0xC218]}, /* 할 수 */
{keys: ['q','n'], chars: [0xC218,0x20,0xC788]}, /* 수 있 */
{keys: ['n','q','w'], chars: [0xC218,0x20,0xC5C6]}, /* 수 없 */
{keys: ['e','h','n','s'], chars: [0xD560,0x20,0xC218,0xB294,0x20]}, /* 할 수는 */
{keys: ['c','i','o','v'], chars: [0xB3C4,0x20,0xBD88,0xAD6C,0xD558,0xACE0,0x20]}, /* 도 불구하고 */
{keys: ['i','k','r','q'], chars: [0xAC83,0xB3C4,0x20]}, /* 것도 */
{keys: ['g','k','r'], chars: [0xAC83,0xC740,0x20]}, /* 것은 */
{keys: ['e','g','k','r'], chars: [0xAC83,0xC744,0x20]}, /* 것을 */
{keys: ['c','d','i','u'], chars: [0xB418,0xB294,0xB370]}, /* 되는데 */
{keys: ['f','g','i','u'], chars: [0xB294,0xB2E4,0x2E,0x20]}, /* 는다. */
{keys: ['f','g','s','u'], chars: [0xB294,0xAC00]}, /* 는가 */
{keys: ['g','i','s','u'], chars: [0xB2E4,0xB294,0x20]}, /* 다는 */
{keys: ['f','o','u'], chars: [0xBC1B,0xB294,0x20]}, /* 받는 */
{keys: ['d','f','o','u'], chars: [0xBCF4,0xB0B4]}, /* 보내 */
{keys: ['d','f','o','s','u'], chars: [0xBCF4,0xB0B8,0x20]}, /* 보낸 */
{keys: ['f','g','k','l'], chars: [0xAE00,0xC790]}, /* 글자 */
{keys: ['c','d','k','l'], chars: [0xC9C0,0xAC8C,0x20]}, /* 지게 */
{keys: ['c','d','h','n'], chars: [0xC790,0xC138,0xD788,0x20]}, /* 자세히 */
{keys: ['a','f','h','j','r'], chars: [0xC870,0xC6A9,0xD788,0x20]}, /* 조용히 */
{keys: ['.','a','h','j','v'], chars: [0xC870,0xC6A9,0xD788,0x20]}, /* 조용히 */
//1701 약어 수정
{keys: ['c','f','l','u'], chars: [0xC790,0xB124]}, /* 자네 */
{keys: ['c','j','r','s'], chars: [0xC5B8,0xC81C,0xB098,0x20]}, /* 언제나 */
{keys: ['d','h','n','s','v'], chars: [0xC2E0,0xD638]}, /* 신호 */
{keys: ['.','d','h','n','s'], chars: [0xC2E0,0xD638]}, /* 신호 */
{keys: ['e','g','i','r'], chars: [0xB4E4,0xC5B4]}, /* 들어 */
{keys: ['e','i','j','s'], chars: [0xC77C,0xB2E8,0x20]}, /* 일단 */
{keys: ['c','f','k','s'], chars: [0xAD00,0xACC4]}, /* 관계 */
{keys: ['a','g','r','u'], chars: [0xADF8,0xB0E5,0x20]}, /* 그냥 */
{keys: ['a','u'], chars: [0xAC00,0xB2A5]}, /* 가능 */
{keys: ['j','k','z'], chars: [0xAE08,0xC735]}, /* 금융 */
{keys: ['g','j','k','v'], chars: [0xC694,0xAE08]}, /* 요금 */
{keys: ['.','g','j','k'], chars: [0xC694,0xAE08]}, /* 요금 */
{keys: ['a','j','s','u'], chars: [0xC548,0xB155]}, /* 안녕 */
{keys: ['g','j','r','u'], chars: [0xC5B4,0xB290,0x20]}, /* 어느 */
{keys: ['f','g','k','l','z'], chars: [0xADF8,0xB9BC,0xC790]}, /* 그림자 */
{keys: ['g','k','r','s'], chars: [0xADFC,0xAC70]}, /* 근거 */
{keys: ['k','o','s'], chars: [0xBD84,0xC704,0xAE30]}, /* 분위기 */
{keys: ['i','l','z'], chars: [0xB2E4,0xC9D0]}, /* 다짐 */
{keys: ['d','l','n','s','v'], chars: [0xCD5C,0xC18C,0xD55C]}, /* 최소한 */
{keys: ['.','d','l','n','s'], chars: [0xCD5C,0xC18C,0xD55C]}, /* 최소한 */
{keys: ['e','i','n','r'], chars: [0xC124,0xD0D5]}, /* 설탕 */
{keys: ['b','f','h','s','y'], chars: [0xBB38,0xD654,0xC7AC]}, /* 문화재 */
{keys: ['f','h','p','s','y'], chars: [0xBB38,0xD654,0xC7AC]}, /* 문화재 */
{keys: ['b','f','s','y'], chars: [0xB300,0xBB38]}, /* 대문 */
{keys: ['f','p','s','y'], chars: [0xB300,0xBB38]}, /* 대문 */
{keys: ['g','m','r'], chars: [0xADF8,0xB7EC,0xBBC0,0xB85C,0x20]}, /* 그러므로 */
{keys: ['c','d','j'], chars: [0xC608,0xC678]}, /* 예외 */
{keys: ['e','j','q','u'], chars: [0xC61B,0xB0A0]}, /* 옛날 */
{keys: ['i','q','u'], chars: [0xC778,0xD130,0xB137]}, /* 인터넷 */
{keys: ['g','k','m','q'], chars: [0xADF8,0xB987]}, /* 그릇 */
{keys: ['k','m','q'], chars: [0xADF8,0xB7AC]}, /* 그랬 */
{keys: ['s','u'], chars: [0xB0B4,0xB144]} /* 내년 */
];
K3_test_abbreviation_table = [
{phonemes: [0x1109,0x11B8], chars: [0x1109,0x1173,0x11B8,0x1102,0x1175,0x1103,0x1161,0x2E,0x20]}, /* ㅅ *ㅂ : 습니다. */
{phonemes: [0x110B,0x11B8], chars: [0xC785,0x1102,0x1175,0x1103,0x1161,0x2E,0x20]}, /* ㅇ *ㅂ : 입니다. */
{phonemes: [0x1112,0x11B8], chars: [0x1112,0x1161,0x11B8,0x1102,0x1175,0x1103,0x1161,0x2E,0x20]}, /* ㅎ *ㅂ : 합니다. */
{phonemes: [0x1112,0x11AB], chars: [0x1112,0x1161,0x110C,0x1175,0xB9CC,0x20]}, /* ㅎ *ㄴ : 하지만 */
{phonemes: [0x110C,0x1107], chars: [0x110C,0x1165,0x11BC,0x1107,0x116E]} /* ㅈㅂ : 정부 */
];
K3_test_moachigi_hangeul_abbreviation_table = [
//{phonemes: [0x1109,0x11B8], chars: [0x1109,0x1173,0x11B8,0x1102,0x1175,0x1103,0x1161,0x2E,0x20]}, /* ㅅ *ㅂ : 습니다. */
];
K3_test_multikey_abbreviation_table = [
{class: ['이름1'], keys: ['e','k'], chars: [0xACB0,0xACFC]}, /* 결과 */
{class: ['이름1'], keys: ['k','x'], chars: [0xAD6D,0xAC00]}, /* 국가 */
{class: ['이름1'], keys: ['f','l','s','y'], chars: [0xB9C8,0xCC2C,0xAC00,0xC9C0]}, /* 마찬가지 */
{class: ['이름2'], keys: ['k','z'], chars: [0xAC1C,0xB150]}, /* 개념 */
{class: ['이름2'], keys: ['s','u'], chars: [0xB0B4,0xB144]}, /* 내년 */
{class: ['이름2'], keys: ['m','s','t','y'], chars: [0xB77C,0xBA74]}, /* 라면 */
{class: ['이름3'], keys: ['i','k','r'], chars: [0xAC70,0xB300]}, /* 거대 */
{class: ['이름3'], keys: ['l','m','r'], chars: [0xCC98,0xB9AC]}, /* 처리 */
{class: ['이름4'], keys: ['k','o','x'], chars: [0xADF9,0xBCF5]}, /* 극복 */
{class: ['이름4'], keys: ['a','f','o','y'], chars: [0xBC29,0xBB38]}, /* 방문 */
{class: ['풀이1'], keys: ['j','q','r','w'], chars: [0xC5C6,0xC560]}, /* 없애 */
{class: ['풀이2'], keys: ['j','k','q','w'], chars: [0xB04A,0xC784,0xC5C6]}, /* 끊임없 */
{prev_class: ['이름1','이름2','이름3'], keys: ['c'], chars: [0xC5D0,0x20]}, /* 에 */
{prev_class: ['이름1','이름3'], keys: ['e','y'], chars: [0xC57C,0xB9D0,0xB85C,0x20]}, /* 야말로 */
{prev_class: ['이름2','이름4'], keys: ['e','y'], chars: [0xC774,0xC57C,0xB9D0,0xB85C,0x20]}, /* 이야말로 */
{prev_class: ['이름1','이름2','이름3','이름4'], class: ['토2'], keys: ['l','m','z'], chars: [0xCC98,0xB7FC]}, /* 처럼 */
{prev_class: ['이름1','이름3'], class: ['토2'], keys: ['m','s','t'], chars: [0xB77C,0xBA74]}, /* 라면 */
{prev_class: ['이름2','이름4'], class: ['토2'], keys: ['m','s','t'], chars: [0xC774,0xB77C,0xBA74]}, /* 이라면 */
{prev_class: ['풀이1'], keys: ['u','w'], chars: [0x11B8,0xB2C8,0xB2E4,0x2E,0x20]}, /* ㅂ니다. */
{prev_class: ['풀이2'], keys: ['u','w'], chars: [0xC2B5,0xB2C8,0xB2E4,0x2E,0x20]}, /* 습니다. */
{prev_class: ['풀이1'], keys: ['u'], chars: [0xC73C,0xB2C8,0x20]}, /* 니 */
{prev_class: ['풀이2'], keys: ['u'], chars: [0xC73C,0xB2C8,0x20]}, /* 으니 */
{prev_class: ['풀이1'], class: ['끝1'], keys: ['d','f','u','k'], chars: [0xB2C8,0xAE4C]}, /* 니까 */
{prev_class: ['풀이2'], class: ['끝2'], keys: ['d','f','u','k'], chars: [0xC73C,0xB2C8,0xAE4C]}, /* 으니까 */
{prev_class: ['이름3','이름4'], keys: ['g','h','u'], chars: [0xD558,0xB294,0x20]}, /* 하는 */
{prev_class: ['이름1','이름3','풀이1','풀이2'], keys: ['l','s','y'], chars: [0xC9C0,0xB9CC,0x20]}, /* 지만 */
{prev_class: ['이름2','이름4'], keys: ['l','s','y'], chars: [0xC774,0xC9C0,0xB9CC,0x20]}, /* 이지만 */
{prev_class: ['토1','끝1'], keys: ['s'], chars: [0x11AB,0x20]}, /* ㄴ */
{prev_class: ['토2','끝2'], keys: ['s'], chars: [0xC740,0x20]}, /* 은 */
];
} // input_additional_combination_table_info()
qs9oq47c2kxh2xxlkbn6nw9tua86r3k
User:Namoroka/common.css
2
175101
740127
2026-05-02T02:37:16Z
Namoroka
19627
Created page with "@import url(https://test.wikipedia.org/w/index.php?title=User:Namoroka/ohi.css&action=raw&ctype=text/css);"
740127
css
text/css
@import url(https://test.wikipedia.org/w/index.php?title=User:Namoroka/ohi.css&action=raw&ctype=text/css);
0w214k4g61fv1a3qxzd6rbac4dvu8lx
740129
740127
2026-05-02T03:18:24Z
Namoroka
19627
Blanked the page
740129
css
text/css
phoiac9h4m842xq45sp7s6u21eteeq1
Store
0
175102
740131
2026-05-02T04:20:14Z
Supertian8
67751
Test alignment
740131
wikitext
text/x-wiki
[[file:example.jpg|thumb|right]]
[[category:coasters]]
ocjflq10stun0no1jjyyj6eglgywpls
Demo5
0
175104
740140
2026-05-02T11:44:02Z
Hridyesh hg3
73385
Created page with "demo0 demo1 demo2 demo3 demo4 demo5 demo6 demo7 demo9 demo10 demo11 demo12 demo13 [[Category:WikiClub Tech UIT]]"
740140
wikitext
text/x-wiki
demo0
demo1
demo2
demo3
demo4
demo5
demo6
demo7
demo9
demo10
demo11
demo12
demo13
[[Category:WikiClub Tech UIT]]
8or78mj5mi6a7b03pobsuc5gcf77m0n