Jump to content
Wikimedia Meta-Wiki

Module:Reaction

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.
 -- 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>

AltStyle によって変換されたページ (->オリジナル) /