<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh">
	<id>https://arolstar52-zhtest.hf.space/index.php?action=history&amp;feed=atom&amp;title=Module%3AReaction</id>
	<title>Module:Reaction - 版本历史</title>
	<link rel="self" type="application/atom+xml" href="https://arolstar52-zhtest.hf.space/index.php?action=history&amp;feed=atom&amp;title=Module%3AReaction"/>
	<link rel="alternate" type="text/html" href="https://arolstar52-zhtest.hf.space/index.php?title=Module:Reaction&amp;action=history"/>
	<updated>2026-06-28T12:16:11Z</updated>
	<subtitle>在这个wiki上该页的修订历史</subtitle>
	<generator>MediaWiki 1.43.8</generator>
	<entry>
		<id>https://arolstar52-zhtest.hf.space/index.php?title=Module:Reaction&amp;diff=4679016&amp;oldid=prev</id>
		<title>imported&gt;SuperGrey 来自 2025年12月31日 (三) 20:27</title>
		<link rel="alternate" type="text/html" href="https://arolstar52-zhtest.hf.space/index.php?title=Module:Reaction&amp;diff=4679016&amp;oldid=prev"/>
		<updated>2025-12-31T20:27:23Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;新页面&lt;/b&gt;&lt;/p&gt;&lt;div&gt;-- This module implements {{Reaction}}.&lt;br /&gt;
-- Maintainers: SunAfterRain, SuperGrey&lt;br /&gt;
-- Repository: https://github.com/QZGao/Reaction&lt;br /&gt;
-- Release: 2.0.1&lt;br /&gt;
-- Timestamp: 2025-12-31T20:03:22.399Z&lt;br /&gt;
-- &amp;lt;nowiki&amp;gt;&lt;br /&gt;
local p = {}&lt;br /&gt;
local mIfexist&lt;br /&gt;
&lt;br /&gt;
-- Centralized text constants for the on-wiki fallback UI.&lt;br /&gt;
local TEXT = {&lt;br /&gt;
    iconInvalidMessage = &amp;quot;不支援輸入的圖標&amp;quot;,&lt;br /&gt;
    tooltipSeparator = &amp;quot;、&amp;quot;,&lt;br /&gt;
    tooltipSuffix = &amp;quot;回應了這條留言&amp;quot;,&lt;br /&gt;
    tooltipStamp = &amp;quot;%s於%s&amp;quot;,&lt;br /&gt;
    tooltipPrefixNoReactions = &amp;quot;沒有人&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
TEXT.tooltipNoReactions = TEXT.tooltipPrefixNoReactions .. TEXT.tooltipSuffix&lt;br /&gt;
&lt;br /&gt;
-- Attempt to interpret iconInput as a File: title and return a file link if it exists.&lt;br /&gt;
-- Otherwise, return false.&lt;br /&gt;
local function mayMakeFile(iconInput)&lt;br /&gt;
    local success, title = pcall(mw.title.new, iconInput)&lt;br /&gt;
    if success and title and title.namespace == 6 then&lt;br /&gt;
        if not mIfexist then&lt;br /&gt;
            mIfexist = require(&amp;#039;Module:Ifexist&amp;#039;)&lt;br /&gt;
        end&lt;br /&gt;
        -- if title.file.exists then&lt;br /&gt;
        if mIfexist._pfFileExists(title) then&lt;br /&gt;
            return string.format(&amp;#039;[[File:%s|x20px|link=]]&amp;#039;, title.text)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local jsonEncode = mw.text.jsonEncode&lt;br /&gt;
&lt;br /&gt;
-- Determine the displayed reaction count based on user input and actual count.&lt;br /&gt;
local function stripInputCount(inputCount, realCount)&lt;br /&gt;
    if inputCount ~= nil then&lt;br /&gt;
        inputCount = mw.text.trim(inputCount)&lt;br /&gt;
        if inputCount == &amp;quot;&amp;quot; then&lt;br /&gt;
            return &amp;quot;0&amp;quot;&lt;br /&gt;
        else&lt;br /&gt;
            -- Example input allows 99+, so keep the trailing plus and drop leading zeros.&lt;br /&gt;
            local num = mw.ustring.match(inputCount, &amp;quot;^0*(%d+%+?)$&amp;quot;)&lt;br /&gt;
            if num then&lt;br /&gt;
                return num&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return tostring(realCount)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Remove all HTML tags from content.&lt;br /&gt;
local function unstripHTML(content)&lt;br /&gt;
    content = mw.ustring.gsub(content, &amp;quot;%s*&amp;lt;[^&amp;gt;]+&amp;gt;%s*&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
    return content&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Custom unstrip function to keep whitelisted extension tags.&lt;br /&gt;
local function unstripMarkersCustom(content)&lt;br /&gt;
    -- from [[Module:Check_for_unknown_parameters]] # local function clean&lt;br /&gt;
    content = mw.ustring.gsub(content, &amp;quot;(\127[^\127]*%-(%l+)%-[^\127]*\127)&amp;quot;, function(fullTag, tag)&lt;br /&gt;
        if tag == &amp;#039;nowiki&amp;#039; then&lt;br /&gt;
            -- unstrip nowiki content&lt;br /&gt;
            return mw.text.unstripNoWiki(fullTag)&lt;br /&gt;
        elseif tag == &amp;#039;templatestyles&amp;#039; or tag == &amp;#039;math&amp;#039; or tag == &amp;#039;chem&amp;#039; then&lt;br /&gt;
            -- keep templatestyles and low-risk extension tags that interact with templates&lt;br /&gt;
            return fullTag&lt;br /&gt;
        end&lt;br /&gt;
        -- discard all other tags entirely&lt;br /&gt;
        return &amp;quot;&amp;quot;&lt;br /&gt;
    end)&lt;br /&gt;
    return content&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Extract all class attributes and return them as arrays of tokens.&lt;br /&gt;
local function extractHTMLClassLists(input)&lt;br /&gt;
    local result = {}&lt;br /&gt;
&lt;br /&gt;
    -- 1) Quoted attributes: class=&amp;quot;...&amp;quot; or class=&amp;#039;...&amp;#039;&lt;br /&gt;
    for _, val in input:gmatch([[%f[%w]class%f[^%w]%s*=%s*([&amp;quot;&amp;#039;])(.-)%1]]) do&lt;br /&gt;
        local arr = {}&lt;br /&gt;
        for cls in val:gmatch(&amp;quot;%S+&amp;quot;) do&lt;br /&gt;
            arr[#arr + 1] = cls&lt;br /&gt;
        end&lt;br /&gt;
        result[#result + 1] = arr&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- 2) Unquoted attributes: class=xxx (stop at first delimiter)&lt;br /&gt;
    -- HTML forbids whitespace or &amp;quot; &amp;#039; = &amp;lt; &amp;gt; ` in unquoted values&lt;br /&gt;
    for val in input:gmatch([[%f[%w]class%f[^%w]%s*=%s*([^%s&amp;quot;&amp;#039;=&amp;lt;&amp;gt;`]+)]]) do&lt;br /&gt;
        result[#result + 1] = {val}&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return result&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local inArray&lt;br /&gt;
&lt;br /&gt;
-- Validate that for every occurrence of requiredClass, dependentClass is also present.&lt;br /&gt;
-- Returns true if valid, false if a violation is found.&lt;br /&gt;
local function validateClassDependency(input, requiredClass, dependentClass)&lt;br /&gt;
    if not inArray then&lt;br /&gt;
        inArray = require(&amp;#039;Module:TableTools&amp;#039;).inArray&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    for _, classList in ipairs(extractHTMLClassLists(input)) do&lt;br /&gt;
        if inArray(classList, requiredClass) and not inArray(classList, dependentClass) then&lt;br /&gt;
            return false&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return true&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Format a single tooltip entry for a reaction.&lt;br /&gt;
-- If timestamp is provided, include it.&lt;br /&gt;
local function formatTooltipEntry(user, timestamp)&lt;br /&gt;
    if timestamp and timestamp ~= &amp;quot;&amp;quot; then&lt;br /&gt;
        return string.format(TEXT.tooltipStamp, user, timestamp)&lt;br /&gt;
    end&lt;br /&gt;
    return user&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Parse legacy reaction format from positional parameter.&lt;br /&gt;
-- Returns user and timestamp (may be nil).&lt;br /&gt;
local function parseLegacyReaction(entry)&lt;br /&gt;
    local trimmed = mw.text.trim(entry or &amp;quot;&amp;quot;)&lt;br /&gt;
    if trimmed == &amp;quot;&amp;quot; then&lt;br /&gt;
        return nil, nil&lt;br /&gt;
    end&lt;br /&gt;
    local user, timestamp = mw.ustring.match(trimmed, &amp;quot;^(.-)[於于]%s*(.+)$&amp;quot;)&lt;br /&gt;
    if user then&lt;br /&gt;
        user = mw.text.trim(user)&lt;br /&gt;
        timestamp = mw.text.trim(timestamp)&lt;br /&gt;
        if user == &amp;quot;&amp;quot; then&lt;br /&gt;
            user = trimmed&lt;br /&gt;
        end&lt;br /&gt;
        if timestamp == &amp;quot;&amp;quot; then&lt;br /&gt;
            timestamp = nil&lt;br /&gt;
        end&lt;br /&gt;
        return user, timestamp&lt;br /&gt;
    end&lt;br /&gt;
    return trimmed, nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function trimOrNil(value)&lt;br /&gt;
    if value == nil then&lt;br /&gt;
        return nil&lt;br /&gt;
    end&lt;br /&gt;
    local trimmed = mw.text.trim(value)&lt;br /&gt;
    if trimmed == &amp;quot;&amp;quot; then&lt;br /&gt;
        return nil&lt;br /&gt;
    end&lt;br /&gt;
    return trimmed&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Collect reactions from parameters.&lt;br /&gt;
-- Supports both named parameters (user1=..., ts1=...) and positional parameters.&lt;br /&gt;
local function collectReactions(args, iconConsumesPositionalSlot)&lt;br /&gt;
    local reactions = {}&lt;br /&gt;
    local index = 1&lt;br /&gt;
    local positionalOffset = iconConsumesPositionalSlot and 1 or 0&lt;br /&gt;
    while true do&lt;br /&gt;
        local userParam = trimOrNil(args[&amp;quot;user&amp;quot; .. index])&lt;br /&gt;
        local timestampParam = trimOrNil(args[&amp;quot;ts&amp;quot; .. index] or args[&amp;quot;timestamp&amp;quot; .. index])&lt;br /&gt;
        if not timestampParam and index == 1 then&lt;br /&gt;
            timestampParam = trimOrNil(args.ts or args.timestamp)&lt;br /&gt;
        end&lt;br /&gt;
        local positionalValue = trimOrNil(args[index + positionalOffset])&lt;br /&gt;
        if not positionalValue and positionalOffset ~= 1 then&lt;br /&gt;
            positionalValue = trimOrNil(args[index + 1])&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        if not userParam and not timestampParam and not positionalValue then&lt;br /&gt;
            break&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        local user = userParam&lt;br /&gt;
        local timestamp = timestampParam&lt;br /&gt;
&lt;br /&gt;
        if (not user or user == &amp;quot;&amp;quot;) and positionalValue then&lt;br /&gt;
            local legacyUser, legacyTimestamp = parseLegacyReaction(positionalValue)&lt;br /&gt;
            if legacyUser and legacyUser ~= &amp;quot;&amp;quot; then&lt;br /&gt;
                user = legacyUser&lt;br /&gt;
                if not timestamp and legacyTimestamp and legacyTimestamp ~= &amp;quot;&amp;quot; then&lt;br /&gt;
                    timestamp = legacyTimestamp&lt;br /&gt;
                end&lt;br /&gt;
            else&lt;br /&gt;
                user = positionalValue&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        if user and user ~= &amp;quot;&amp;quot; then&lt;br /&gt;
            reactions[#reactions + 1] = {&lt;br /&gt;
                user = user,&lt;br /&gt;
                timestamp = timestamp&lt;br /&gt;
            }&lt;br /&gt;
        end&lt;br /&gt;
        index = index + 1&lt;br /&gt;
    end&lt;br /&gt;
    return reactions&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function p._main(args)&lt;br /&gt;
    local iconConsumesPositionalSlot = false&lt;br /&gt;
    local iconInput = trimOrNil(args.icon)&lt;br /&gt;
    if iconInput then&lt;br /&gt;
        iconInput = mw.text.trim(iconInput)&lt;br /&gt;
    else&lt;br /&gt;
        local positionalIcon = trimOrNil(args[1])&lt;br /&gt;
        if positionalIcon then&lt;br /&gt;
            iconInput = positionalIcon&lt;br /&gt;
            iconConsumesPositionalSlot = true&lt;br /&gt;
        else&lt;br /&gt;
            iconInput = &amp;quot;👍&amp;quot;&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    local iconInvalid = false&lt;br /&gt;
    iconInput = mw.text.trim(iconInput)&lt;br /&gt;
    if -- Known cases that reliably break the layout (and exceed intended usage)&lt;br /&gt;
    mw.ustring.find(iconInput, &amp;quot;&amp;lt;div[ &amp;gt;]&amp;quot;) or mw.ustring.find(iconInput, &amp;quot;&amp;lt;table[ &amp;gt;]&amp;quot;) or&lt;br /&gt;
        mw.ustring.find(iconInput, &amp;quot;&amp;lt;p[ &amp;gt;]&amp;quot;) or mw.ustring.find(iconInput, &amp;quot;&amp;lt;li[ &amp;gt;]&amp;quot;) or&lt;br /&gt;
        mw.ustring.find(iconInput, &amp;quot;\n&amp;quot;) or mw.ustring.find(iconInput, &amp;quot;template%-reaction&amp;quot;) or&lt;br /&gt;
        -- Only allow zhwp-talkicon inputs that also carry the reactionable class&lt;br /&gt;
        (mw.ustring.find(iconInput, &amp;quot;zhwp%-talkicon&amp;quot;) and&lt;br /&gt;
            not validateClassDependency(iconInput, &amp;#039;zhwp-talkicon&amp;#039;, &amp;#039;zhwp-talkicon-reactionable&amp;#039;)) then&lt;br /&gt;
        iconInvalid = true&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local iconData = unstripHTML(mw.text.unstrip(iconInput))&lt;br /&gt;
    local iconDisplay&lt;br /&gt;
    if not iconInvalid then&lt;br /&gt;
        -- Custom unstrip to keep whitelisted marks&lt;br /&gt;
        iconDisplay = mayMakeFile(iconInput) or mw.text.trim(unstripMarkersCustom(iconInput))&lt;br /&gt;
        if iconDisplay == &amp;quot;&amp;quot; then&lt;br /&gt;
            -- Only discarded extension tags survived, treat as invalid&lt;br /&gt;
            iconDisplay = string.format(&amp;#039;&amp;lt;span class=&amp;quot;error&amp;quot;&amp;gt;%s&amp;lt;/span&amp;gt;&amp;#039;, TEXT.iconInvalidMessage)&lt;br /&gt;
            iconInvalid = true&lt;br /&gt;
        end&lt;br /&gt;
    else&lt;br /&gt;
        iconDisplay = string.format(&amp;#039;&amp;lt;span class=&amp;quot;error&amp;quot;&amp;gt;%s&amp;lt;/span&amp;gt;&amp;#039;, TEXT.iconInvalidMessage)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local reactions = collectReactions(args, iconConsumesPositionalSlot)&lt;br /&gt;
    local realReactionCount = #reactions -- actual count&lt;br /&gt;
    local reactionNames = {}&lt;br /&gt;
    local structuredReactions = {}&lt;br /&gt;
    for _, reaction in ipairs(reactions) do&lt;br /&gt;
        reactionNames[#reactionNames + 1] = formatTooltipEntry(reaction.user, reaction.timestamp)&lt;br /&gt;
        structuredReactions[#structuredReactions + 1] = {&lt;br /&gt;
            user = reaction.user,&lt;br /&gt;
            timestamp = reaction.timestamp&lt;br /&gt;
        }&lt;br /&gt;
    end&lt;br /&gt;
    local reactionTitle&lt;br /&gt;
    if realReactionCount &amp;gt;= 1 then&lt;br /&gt;
        local list = mw.text.listToText(reactionNames, TEXT.tooltipSeparator, TEXT.tooltipSeparator)&lt;br /&gt;
        reactionTitle = list .. TEXT.tooltipSuffix&lt;br /&gt;
    else&lt;br /&gt;
        reactionTitle = TEXT.tooltipNoReactions&lt;br /&gt;
    end&lt;br /&gt;
    local reactionCount = stripInputCount(args.num, realReactionCount) -- displayed count&lt;br /&gt;
&lt;br /&gt;
    local out = mw.html.create(&amp;#039;span&amp;#039;):addClass(&amp;#039;reactionable&amp;#039;):addClass(&amp;#039;template-reaction&amp;#039;):attr(&amp;#039;title&amp;#039;,&lt;br /&gt;
        reactionTitle):attr(&amp;#039;data-reaction-commentors&amp;#039;, table.concat(reactionNames, &amp;#039;/&amp;#039;)):attr(&lt;br /&gt;
        &amp;#039;data-reaction-commentors-json&amp;#039;, jsonEncode(structuredReactions)):attr(&amp;#039;data-reaction-icon&amp;#039;, iconData):attr(&lt;br /&gt;
        &amp;#039;data-reaction-icon-invalid&amp;#039;, iconInvalid and &amp;quot;&amp;quot; or nil):attr(&amp;#039;data-reaction-count&amp;#039;, reactionCount):attr(&lt;br /&gt;
        &amp;#039;data-reaction-real-count&amp;#039;, realReactionCount)&lt;br /&gt;
&lt;br /&gt;
    local content = out:tag(&amp;#039;span&amp;#039;):addClass(&amp;#039;reaction-content&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    -- icon&lt;br /&gt;
    content:tag(&amp;#039;span&amp;#039;):addClass(&amp;#039;reaction-icon-container&amp;#039;):tag(&amp;#039;span&amp;#039;):addClass(&amp;#039;reaction-icon&amp;#039;):wikitext(iconDisplay)&lt;br /&gt;
&lt;br /&gt;
    -- counter&lt;br /&gt;
    content:tag(&amp;#039;span&amp;#039;):addClass(&amp;#039;reaction-counter-container&amp;#039;):tag(&amp;#039;span&amp;#039;):addClass(&amp;#039;reaction-counter&amp;#039;):wikitext(&lt;br /&gt;
        tostring(reactionCount))&lt;br /&gt;
&lt;br /&gt;
    return mw.getCurrentFrame():extensionTag({&lt;br /&gt;
        name = &amp;#039;templatestyles&amp;#039;,&lt;br /&gt;
        args = {&lt;br /&gt;
            src = &amp;#039;Template:Reaction/styles.css&amp;#039;&lt;br /&gt;
        }&lt;br /&gt;
    }) .. tostring(out)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function p.main(frame)&lt;br /&gt;
    local parent = frame:getParent()&lt;br /&gt;
    if not parent then&lt;br /&gt;
        -- Stop if the template wasn&amp;#039;t invoked&lt;br /&gt;
        return &amp;#039;&amp;#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return p._main(parent.args)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;br /&gt;
-- &amp;lt;/nowiki&amp;gt;&lt;/div&gt;</summary>
		<author><name>imported&gt;SuperGrey</name></author>
	</entry>
</feed>