Module:Reaction
Appearance
From Meta, a Wikimedia project coordination wiki
Module documentation
You might want to create a documentation page for this Scribunto module.
Editors can experiment in this module’s sandbox (create | mirror) and testcases (create) pages.
Please add categories to the /doc subpage. Subpages of this module.
Editors can experiment in this module’s sandbox (create | mirror) and testcases (create) pages.
Please add categories to the /doc subpage. Subpages of this module.
-- This module implements {{Reaction}}. -- Maintainers: SunAfterRain, SuperGrey -- Repository: https://github.com/QZGao/Reaction -- Release: 2.2.3 -- Timestamp: 2026年01月12日T12:29:04.985Z -- <nowiki> localp={} localmIfexist -- Centralized text constants for the on-wiki fallback UI. localTEXT={ iconInvalidMessage="Unsupported icon input", tooltipSeparator=", ", tooltipSuffix=" reacted to this comment.", tooltipStamp="%s at %s", tooltipPrefixNoReactions="No one" } TEXT.tooltipNoReactions=TEXT.tooltipPrefixNoReactions..TEXT.tooltipSuffix -- Attempt to interpret iconInput as a File: title and return a file link if it exists. -- Otherwise, return false. localfunctionmayMakeFile(iconInput) localsuccess,title=pcall(mw.title.new,iconInput) ifsuccessandtitleandtitle.namespace==6then ifnotmIfexistthen mIfexist=require('Module:Ifexist') end -- if title.file.exists then ifmIfexist._pfFileExists(title)then returnstring.format('[[File:%s|x20px|link=]]',title.text) end end returnfalse end localjsonEncode=mw.text.jsonEncode -- Determine the displayed reaction count based on user input and actual count. localfunctionstripInputCount(inputCount,realCount) ifinputCount~=nilthen inputCount=mw.text.trim(inputCount) ifinputCount==""then return"0" else -- Example input allows 99+, so keep the trailing plus and drop leading zeros. localnum=mw.ustring.match(inputCount,"^0*(%d+%+?)$") ifnumthen returnnum end end end returntostring(realCount) end -- Remove all HTML tags from content. localfunctionunstripHTML(content) content=mw.ustring.gsub(content,"%s*<[^>]+>%s*","") returncontent end -- Custom unstrip function to keep whitelisted extension tags. localfunctionunstripMarkersCustom(content) -- from [[Module:Check_for_unknown_parameters]] # local function clean content=mw.ustring.gsub(content,"(127円[^127円]*%-(%l+)%-[^127円]*127円)",function(fullTag,tag) iftag=='nowiki'then -- unstrip nowiki content returnmw.text.unstripNoWiki(fullTag) elseiftag=='templatestyles'ortag=='math'ortag=='chem'then -- keep templatestyles and low-risk extension tags that interact with templates returnfullTag end -- discard all other tags entirely return"" end) returncontent end -- Extract all class attributes and return them as arrays of tokens. localfunctionextractHTMLClassLists(input) localresult={} -- 1) Quoted attributes: class="..." or class='...' for_,valininput:gmatch([[%f[%w]class%f[^%w]%s*=%s*(["'])(.-)%1]]) do local arr = {} for cls in val:gmatch("%S+") do arr[#arr + 1] = cls end result[#result + 1] = arr end -- 2) Unquoted attributes: class=xxx (stop at first delimiter) -- HTML forbids whitespace or "' = < > ` in unquoted values for val in input:gmatch([[%f[%w]class%f[^%w]%s*=%s*([^%s"'=<>`]+)]])do result[#result+1]={val} end returnresult end localinArray -- Validate that for every occurrence of requiredClass, dependentClass is also present. -- Returns true if valid, false if a violation is found. localfunctionvalidateClassDependency(input,requiredClass,dependentClass) ifnotinArraythen inArray=require('Module:TableTools').inArray end for_,classListinipairs(extractHTMLClassLists(input))do ifinArray(classList,requiredClass)andnotinArray(classList,dependentClass)then returnfalse end end returntrue end -- Format a single tooltip entry for a reaction. -- If timestamp is provided, include it. localfunctionformatTooltipEntry(user,timestamp) iftimestampandtimestamp~=""then returnstring.format(TEXT.tooltipStamp,user,timestamp) end returnuser end -- Parse legacy reaction format from positional parameter. -- Returns user and timestamp (may be nil). localfunctionparseLegacyReaction(entry) localtrimmed=mw.text.trim(entryor"") iftrimmed==""then returnnil,nil end localuser,timestamp=mw.ustring.match(trimmed,"^(.-)[於于]%s*(.+)$") ifuserthen user=mw.text.trim(user) timestamp=mw.text.trim(timestamp) ifuser==""then user=trimmed end iftimestamp==""then timestamp=nil end returnuser,timestamp end returntrimmed,nil end localfunctiontrimOrNil(value) ifvalue==nilthen returnnil end localtrimmed=mw.text.trim(value) iftrimmed==""then returnnil end returntrimmed end -- Collect reactions from parameters. -- Supports both named parameters (user1=..., ts1=...) and positional parameters. localfunctioncollectReactions(args,iconConsumesPositionalSlot) localreactions={} localindex=1 localpositionalOffset=iconConsumesPositionalSlotand1or0 whiletruedo localuserParam=trimOrNil(args["user"..index]) localtimestampParam=trimOrNil(args["ts"..index]orargs["timestamp"..index]) ifnottimestampParamandindex==1then timestampParam=trimOrNil(args.tsorargs.timestamp) end localpositionalValue=trimOrNil(args[index+positionalOffset]) ifnotpositionalValueandpositionalOffset~=1then positionalValue=trimOrNil(args[index+1]) end ifnotuserParamandnottimestampParamandnotpositionalValuethen break end localuser=userParam localtimestamp=timestampParam if(notuseroruser=="")andpositionalValuethen locallegacyUser,legacyTimestamp=parseLegacyReaction(positionalValue) iflegacyUserandlegacyUser~=""then user=legacyUser ifnottimestampandlegacyTimestampandlegacyTimestamp~=""then timestamp=legacyTimestamp end else user=positionalValue end end ifuseranduser~=""then reactions[#reactions+1]={ user=user, timestamp=timestamp } end index=index+1 end returnreactions end functionp._main(args) localiconConsumesPositionalSlot=false localiconInput=trimOrNil(args.icon) ificonInputthen iconInput=mw.text.trim(iconInput) else localpositionalIcon=trimOrNil(args[1]) ifpositionalIconthen iconInput=positionalIcon iconConsumesPositionalSlot=true else iconInput="👍" end end localiconInvalid=false iconInput=mw.text.trim(iconInput) if-- Known cases that reliably break the layout (and exceed intended usage) mw.ustring.find(iconInput,"<div[ >]")ormw.ustring.find(iconInput,"<table[ >]")or mw.ustring.find(iconInput,"<p[ >]")ormw.ustring.find(iconInput,"<li[ >]")or mw.ustring.find(iconInput,"\n")ormw.ustring.find(iconInput,"template%-reaction")or -- Only allow zhwp-talkicon inputs that also carry the reactionable class (mw.ustring.find(iconInput,"zhwp%-talkicon")and notvalidateClassDependency(iconInput,'zhwp-talkicon','zhwp-talkicon-reactionable'))then iconInvalid=true end localiconData=unstripHTML(mw.text.unstrip(iconInput)) localiconDisplay ifnoticonInvalidthen -- Custom unstrip to keep whitelisted marks iconDisplay=mayMakeFile(iconInput)ormw.text.trim(unstripMarkersCustom(iconInput)) ificonDisplay==""then -- Only discarded extension tags survived, treat as invalid iconDisplay=string.format('<span class="error">%s</span>',TEXT.iconInvalidMessage) iconInvalid=true end else iconDisplay=string.format('<span class="error">%s</span>',TEXT.iconInvalidMessage) end localreactions=collectReactions(args,iconConsumesPositionalSlot) localrealReactionCount=#reactions-- actual count localreactionNames={} localstructuredReactions={} for_,reactioninipairs(reactions)do reactionNames[#reactionNames+1]=formatTooltipEntry(reaction.user,reaction.timestamp) structuredReactions[#structuredReactions+1]={ user=reaction.user, timestamp=reaction.timestamp } end localreactionTitle ifrealReactionCount>=1then locallist=mw.text.listToText(reactionNames,TEXT.tooltipSeparator,TEXT.tooltipSeparator) reactionTitle=list..TEXT.tooltipSuffix else reactionTitle=TEXT.tooltipNoReactions end localreactionCount=stripInputCount(args.num,realReactionCount)-- displayed count localout=mw.html.create('span'):addClass('reactionable'):addClass('template-reaction'):attr('title', reactionTitle):attr('data-reaction-commentors',table.concat(reactionNames,'/')):attr( 'data-reaction-commentors-json',jsonEncode(structuredReactions)):attr('data-reaction-icon',iconData):attr( 'data-reaction-icon-invalid',iconInvalidand""ornil):attr('data-reaction-count',reactionCount):attr( 'data-reaction-real-count',realReactionCount) localcontent=out:tag('span'):addClass('reaction-content') -- icon content:tag('span'):addClass('reaction-icon-container'):tag('span'):addClass('reaction-icon'):wikitext(iconDisplay) -- counter content:tag('span'):addClass('reaction-counter-container'):tag('span'):addClass('reaction-counter'):wikitext( tostring(reactionCount)) returnmw.getCurrentFrame():extensionTag({ name='templatestyles', args={ src='Template:Reaction/styles.css' } })..tostring(out) end functionp.main(frame) localparent=frame:getParent() ifnotparentthen -- Stop if the template wasn't invoked return'' end returnp._main(parent.args) end returnp -- </nowiki>