CharInsert is an extension that enables you to provide editors with links they can click to insert snippets into wikitext. It can be used for anything from providing one-click buttons to insert a single special character to serving as a complete alternative to BoilerRoom or MultiBoilerplate, though I find it preferable even to either of these.
Before we continue, please note that “char” (as in “character”) is pronounced the same as “care,” and my title is a fantastic pun. Okay thanks.
(Note: All code in this post was originally written by me for Leaguepedia and is licensed under CC BY-SA 3.0.)
Base CharInsert functionality
CharInsert by default allows the user to click a piece of text to insert that piece of text. Spaces delineate separate pieces of text, but spaces can be escaped with <nowiki>
tags.
Here is a simple example:
<charinsert><includeonly></includeonly><noinclude>{{documentation}}</noinclude></charinsert>
In fact, this is the first CharInsert I ever made. If you save this at MediaWiki:Editnotice-10
(10 is the Template
namespace) then any time you edit a template (using the source editor, not VE) you will see a piece of text styled like a hyperlink with the text <includeonly></includeonly><noinclude>{{documentation}}</noinclude>
that you can click. Clicking it inserts the text into the textarea at the cursor.
We can slightly adjust this as follows, adding a +
sign:
<charinsert><includeonly>+</includeonly><noinclude>{{documentation}}</noinclude></charinsert>
And now after clicking the insert, our cursor will be delivered in between the <includeonly></includeonly>
at the +
. Neat!
If we wanted to put multiple inserts in the same row next to each other, we could simply put spaces in between them, inside of the same extension tags. The canonical example on the documentation page is as follows:
<charinsert>Á á Ć ć É é Í í Ĺ ĺ Ń ń Ó ó Ŕ ŕ Ś ś Ú ú Ý ý Ź ź</charinsert>
In this case, we can click the “Á” to print Á
, etc.
If, for some reason, we wanted to insert the literal text Á á Ć ć É é Í í Ĺ ĺ Ń ń Ó ó Ŕ ŕ Ś ś Ú ú Ý ý Ź ź
, we could accomplish that as follows:
<charinsert>Á<nowiki> </nowiki>á<nowiki> </nowiki>Ć<nowiki> </nowiki>ć<nowiki> </nowiki>É<nowiki> </nowiki>é<nowiki> </nowiki>Í<nowiki> </nowiki>í<nowiki> </nowiki>Ĺ<nowiki> </nowiki>ĺ<nowiki> </nowiki>Ń<nowiki> </nowiki>ń<nowiki> </nowiki>Ó<nowiki> </nowiki>ó<nowiki> </nowiki>Ŕ<nowiki> </nowiki>ŕ<nowiki> </nowiki>Ś<nowiki> </nowiki>ś<nowiki> </nowiki>Ú<nowiki> </nowiki>ú<nowiki> </nowiki>Ý<nowiki> </nowiki>ý<nowiki> </nowiki>Ź<nowiki> </nowiki>ź</charinsert>
You can see then, that elaborate CharInserts could be rather unwieldy to define; we will certainly want a wrapper that provides some basic escaping. Here is the escaping I do in Lua:
|
|
The first line escapes newlines, and the second escapes all sets of consecutive spaces. I’ve never needed to use a tab character in a CharInsert (at least not yet). The last two escapes undo the process of escaping brackets, since brackets are used as the delimiters for multi-line quotes in Lua, so I use the following to add a category: \[\[Category:Fake Cargo Attach Templates\]\]
and then need to restore it to the desired [[
and ]]
before printing.
So now, we’ve seen what CharInserts can do - but there’s still a pretty big hole missing.
Separate “label” attribute
What if we want to insert a giant wall of text? Say I want to let users input the following:
{{MatchSchedule|date= |time= |timezone= |dst=
|<!-- Do not change the order of team1 and team2!! -->
|team1= |team2=
|team1score= |team2score= |winner=
|pbp= |color=
|vodinterview= |with=
|stream= |reddit=
|game1={{MatchSchedule/Game
|blue= |red= |winner= |ssel= |ff=
|mh=
|recap=
|vodpb=
|vodstart=
|vodpost=
|vodhl=
|vodinterview=
|with=
|mvp=
}}
|game2={{MatchSchedule/Game
|blue= |red= |winner= |ssel= |ff=
|mh=
|recap=
|vodpb=
|vodstart=
|vodpost=
|vodhl=
|vodinterview=
|with=
|mvp=
}}
}}
After escaping, it will look like this:
{{MatchSchedule|date=<nowiki> </nowiki>|time=<nowiki> </nowiki>|timezone=<nowiki> </nowiki>|dst=<nowiki> </nowiki> |<!--<nowiki> </nowiki>Do<nowiki> </nowiki>not<nowiki> </nowiki>change<nowiki> </nowiki>the<nowiki> </nowiki>order<nowiki> </nowiki>of<nowiki> </nowiki>team1<nowiki> </nowiki>and<nowiki> </nowiki>team2!!<nowiki> </nowiki>--> |team1=<nowiki> </nowiki>|team2= |team1score=<nowiki> </nowiki>|team2score=<nowiki> </nowiki>|winner= |pbp=<nowiki> </nowiki>|color= |vodinterview=<nowiki> </nowiki>|with= |stream=<nowiki> </nowiki>|reddit= |game1={{MatchSchedule/Game <nowiki> </nowiki>|blue=<nowiki> </nowiki>|red=<nowiki> </nowiki>|winner=<nowiki> </nowiki>|ssel=<nowiki> </nowiki>|ff= <nowiki> </nowiki>|mh= <nowiki> </nowiki>|recap= <nowiki> </nowiki>|vodpb= <nowiki> </nowiki>|vodstart= <nowiki> </nowiki>|vodpost= <nowiki> </nowiki>|vodhl= <nowiki> </nowiki>|vodinterview= <nowiki> </nowiki>|with= <nowiki> </nowiki>|mvp= <nowiki> </nowiki>}} |game2={{MatchSchedule/Game <nowiki> </nowiki>|blue=<nowiki> </nowiki>|red=<nowiki> </nowiki>|winner=<nowiki> </nowiki>|ssel=<nowiki> </nowiki>|ff= <nowiki> </nowiki>|mh= <nowiki> </nowiki>|recap= <nowiki> </nowiki>|vodpb= <nowiki> </nowiki>|vodstart= <nowiki> </nowiki>|vodpost= <nowiki> </nowiki>|vodhl= <nowiki> </nowiki>|vodinterview= <nowiki> </nowiki>|with= <nowiki> </nowiki>|mvp= <nowiki> </nowiki>}} }}
Uh….no thanks. I don’t want my users seeing this, let alone having to know what happens when you click it. And it’s actually even worse than you might think:
The text in <pre>
is because I have some lines that start with a space, even though the space is escaped. You can still click it, and the desired text will input, but…..yeah, no.
So what we need is for a way to decouple the display text from the inserted text.
As it turns out, there’s a Phabricator thread discussing this that’s been open for just over a decade as of my writing this post. So I gave up on the hope of having built-in support for this, and within a couple hours of finding the topic, I’d written my own support for this feature with just a couple lines of JS and one CSS rule (along with some conventions about how I’d print these things in HTML):
|
|
|
|
Here’s what this does:
- The user creates a CharInsert using the
<charinsert>
tag. This results in a container with class.mw-charinsert-item
. The text given to the CharInsert should be the desired insertion text. - The Lua wrapper wraps each CharInsert in a
div
which is given an attributedata-ci-label
with the desired display text. - The Lua wrapper further adds a line that says
Loading...
or something similar, to prevent vertical jumps of content as the page loads. - Prior to this point, the css rule
.client-js .mw-charinsert-item { display:none; }
is in effect; this ensures that the user will not see a scary jumble of 100 lines of escaped wikitext. - Now that the substitution has been made, the
display
on.mw-charinsert-item
is reverted toinline-block
. - Finally, we hide the loading text.
Of course, this all assumes a bunch of specifics about my HTML, but since I’m going to be generating everything in Lua, that’s not an issue at all.
Deploying CharInserts
Before I show you my Lua code, let’s talk briefly about how to deploy CharInserts for use.
Interlude: Finding system messages
If you add the argument uselang=qqx
to the URL of any page (if there’s already one or more specified params, put &uselang=qqx
; if not, then ?uselang=qqx
) will show you the names of system messages instead of their contents.
So, say I edit my user page on Leaguepedia. Normally the url is https://lol.gamepedia.com/index.php?title=User:RheingoldRiver&action=edit
, but I want to see what system messages are present, so I’ll instead go to https://lol.gamepedia.com/index.php?title=User:RheingoldRiver&action=edit&uselang=qqx
. (Note that the ?
comes earlier in the URL, before the first parameter, which is title=User:RheingoldRiver
.)
Back to deploying CharInserts
So on my user page, I’m shown the following four system message names:
(editnotice-2)
(editnotice-2-RheingoldRiver)
(longpage-hint: (size-kilobytes), 1115)
(editpage-head-copy-warn)
The last one is included on every page on the entire wiki, but I’d rather avoid using the copyright warning to display editor tools. Instead I’m going to use the first message, which is editnotice-2
. If I get a list of all of my namespaces (doable either by going manually through Special:AllPages
or using the API), I can get a list of system messages I will have to display:
MediaWiki:Editnotice-0
(main)MediaWiki:Editnotice-1
(talk)MediaWiki:Editnotice-2
(user)MediaWiki:Editnotice-3
(user talk)- etc.
I’m going to place the same text on all of them, except for the first:
{{int:Editnotice-0}}
This internationalizes (translates) the edit notice for the main namespace and displays it on each other namespace’s edit notice.
At MediaWiki:Editnotice-0
I have the following:
Thanks for editing the wiki! Want to join our Discord server? {{DiscordURL}}
{{#invoke:CharInserts|main}}
The first line is unrelated to CharInsert; the second simply invokes my CharInserts module.
As it happens, I actually want my CharInserts to behave differently depending on namespace - I have some preloads that are template-only, some that are main-only, etc. However, I chose to do all of this handling inside of Lua, so in the system messages I’m merely invoking the same module on all pages.
Config files
See also this post about how to design nice config files.
As the last thing before we move to the actual Lua code, let’s look how I’ve arranged my config files. The goal is to have something that editors can update without knowing any Lua or programming at all.
As I mentioned, I have different behaviors depending on namespace. I also have different behaviors depending on pattern matches against the title of the current page. In particular, the following parameters are provided for each insert:
pattern
, a required pattern to appear somewhere in the title; if this is the empty string, then every title matches it by default;notpattern
, a pattern or list of patterns that must NOT appear in the title if the insert is to appear; this takes precedence overpattern
;label
, the human-readable text to display for an editor to click; andinsert
, the actual text (human-readable and unescaped) that the CharInsert will add to the page.
Because often notpattern
is a list of “pages with patterns that are handled specially,” at the start of most config files I define a constant called NOT_PATTERNS
that will be used as the notpattern
for many inserts. (This is also why config files as code are better than standalone non-code text files.)
Here is an excerpt from my Main
namespace CharInserts. Note that indentation style is not preserved mid-insert to avoid sending careless whitespace to the charinsert. At the same time, I deliberately arranged insert
to be the final item in each group, so that I didn’t need to change indentation after it.
|
|
Pagename
I want to point one final thing out about the peculiarities of escaping for CharInsert: Normally, if I want the title of a page inserted into that page, I can simply used {{subst:PAGENAME}}
or {{subst:BASEPAGENAME}}
or what have you. However, subst
does NOT work inside of <includeonly>
tags.
Therefore, for one of my inserts for the Template
namespace, when I want to invoke a Lua module with the same title as the template, I write the following:
<includeonly>{{#invoke:$PAGENAME$|main}}</includeonly><noinclude>{{documentation}}</noinclude>
the string $PAGENAME$
follows other conventions for formatting parameters into longer strings that I use elsewhere on the wiki, so part of my escaping/formatting is the following:
|
|
And the code in Module:SentenceUtil
for makeReplacements
is as follows:
|
|
(Note the recursive capacity of replacements
- while it’s not needed here, in other situations, it’s required.)
In the future, if I find other necessary pre-printing substitutions to make, I can simply append additional keys to the table replacements
in the first snippet above.
The Lua wrapper!
Finally, we need to turn our config file into HTML/wikitext laid out the way our JS expects. Here is the full code of the wrapper. Note the inclusion of the previous escaping/replacement snippets:
|
|