{"id":30609,"date":"2025-10-14T14:22:57","date_gmt":"2025-10-14T06:22:57","guid":{"rendered":"https:\/\/www.hkmu.edu.hk\/oetools\/?page_id=30609"},"modified":"2025-11-07T09:14:53","modified_gmt":"2025-11-07T01:14:53","slug":"resource-2-words-in-articles","status":"publish","type":"page","link":"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/","title":{"rendered":"Resource 2 Words in Articles"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"30609\" class=\"elementor elementor-30609\" data-elementor-settings=\"[]\">\n\t\t\t\t\t\t\t<div class=\"elementor-section-wrap\">\n\t\t\t\t\t\t\t<section class=\"has_eae_slider wavo-column-gap-default elementor-section elementor-top-section elementor-element elementor-element-e1a571d elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"e1a571d\" data-element_type=\"section\" data-settings=\"{&quot;jet_parallax_layout_list&quot;:[]}\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"has_eae_slider elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-cf1e927\" data-id=\"cf1e927\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t\t\t<div class=\"elementor-element elementor-element-9aec9c3 elementor-widget elementor-widget-html\" data-id=\"9aec9c3\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Words in Articles - \u4e2d\u6587\u6587\u7ae0\u8a5e\u5f59\u641c\u7d22<\/title>\n    <style>\n        :root {\n            --primary-color: #32744C; \/* SKILL deep green *\/\n            --primary-light: #508E42; \/* SKILL medium green *\/\n            --primary-dark: #4C4F26; \/* SKILL dark olive *\/\n            --accent-green: #508E42; \/* Align accents *\/\n            --medium-gray: #e0e0e0;\n            --dark-gray: #707070;\n            --warning-color: #3A7CA5; \/* Coherent blue for term highlighting *\/\n            --dark-text: #333;\n            --light-text: #666;\n            --card-bg: rgba(255, 255, 255, 0.95);\n            --bg-light: linear-gradient(135deg, #CFE2CF, #8DBEA2); \/* SKILL soft green gradient *\/\n            --shadow: 0 15px 40px rgba(0, 0, 0, 0.2);\n        }\n        \n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n            font-family: 'Microsoft YaHei', '\u9ed1\u9ad4', 'SimHei', Arial, sans-serif;\n        }\n        \n        body {\n            color: var(--dark-text);\n            min-height: 100vh;\n            padding: 20px;\n            line-height: 1.6;\n        }\n        \n        .container {\n            max-width: 1800px;\n            margin: 0 auto;\n            background: var(--card-bg);\n            border-radius: 15px;\n            box-shadow: var(--shadow);\n            overflow: hidden;\n        }\n        \n        .header {\n            background: linear-gradient(45deg, var(--primary-color), var(--primary-light));\n            color: #ffffff;\n            padding: 25px;\n            text-align: center;\n        }\n        \n        .header h1 {\n            font-size: 2.5rem;\n            font-weight: 700;\n            margin-bottom: 10px;\n        }\n        \n        .header p {\n            font-size: 18px;\n            opacity: 0.9;\n        }\n        \n        .section {\n            padding: 30px;\n        }\n        \n        .search-section {\n            margin-bottom: 30px;\n            padding: 20px;\n            background-color: #f8f9fa;\n            border-radius: 12px;\n            border-left: 4px solid var(--primary-color);\n            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);\n        }\n        \n        .input-group {\n            display: flex;\n            gap: 10px;\n            margin-bottom: 15px;\n        }\n        \n        #searchInput {\n            flex: 1;\n            padding: 12px 15px;\n            font-size: 16px;\n            border: 2px solid var(--medium-gray);\n            border-radius: 8px;\n            outline: none;\n            transition: all 0.3s ease;\n            font-weight: 500;\n        }\n        \n        #searchInput:focus {\n            border-color: var(--primary-color);\n            box-shadow: 0 0 0 3px rgba(250, 151, 94, 0.1);\n        }\n        \n        #searchButton {\n            padding: 12px 25px;\n            background-color: var(--primary-color);\n            color: white;\n            border: none;\n            border-radius: 8px;\n            cursor: pointer;\n            font-size: 16px;\n            font-weight: 600;\n            transition: all 0.3s ease;\n        }\n        \n        #searchButton:hover {\n            background-color: var(--primary-dark);\n            transform: translateY(-2px);\n            box-shadow: 0 4px 15px rgba(250, 151, 94, 0.3);\n        }\n        \n        #clearButton {\n            padding: 12px 25px;\n            background-color: var(--dark-gray);\n            color: white;\n            border: none;\n            border-radius: 8px;\n            cursor: pointer;\n            font-size: 16px;\n            font-weight: 600;\n            transition: all 0.3s ease;\n        }\n        \n        #clearButton:hover {\n            background-color: #5a5a5a;\n            transform: translateY(-2px);\n            box-shadow: 0 4px 15px rgba(112, 112, 112, 0.3);\n        }\n        \n        .search-info {\n            font-size: 14px;\n            color: var(--light-text);\n            margin-top: 10px;\n            font-weight: 500;\n        }\n        \n        .loading {\n            text-align: center;\n            padding: 20px;\n            font-size: 18px;\n            color: #666;\n        }\n        \n        .results-section {\n            margin-top: 30px;\n        }\n        \n        .results-header {\n            padding: 15px;\n            background: linear-gradient(135deg, rgba(50, 116, 76, 0.1), rgba(80, 142, 66, 0.1));\n            border-radius: 12px;\n            margin-bottom: 20px;\n            font-weight: bold;\n            color: var(--primary-dark);\n            border-left: 4px solid var(--primary-color);\n        }\n        \n        .results-table {\n            width: 100%;\n            border-collapse: collapse;\n            margin-top: 10px;\n            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);\n            border-radius: 12px;\n            overflow: hidden;\n        }\n        \n        .results-table th {\n            background: linear-gradient(45deg, var(--primary-color), var(--primary-light));\n            color: white;\n            padding: 15px;\n            text-align: left;\n            font-weight: 600;\n        }\n        \n        .results-table td {\n            padding: 15px;\n            border-bottom: 1px solid #ddd;\n            vertical-align: top;\n        }\n        \n        .results-table tr:nth-child(even) {\n            background-color: #f8f9fa;\n        }\n        \n        .results-table tr:hover {\n            background-color: rgba(141, 190, 162, 0.15); \/* soft green hover *\/\n        }\n        \n        .article-number {\n            font-weight: bold;\n            color: var(--primary-color);\n            text-align: center;\n            width: 80px;\n        }\n        \n        .article-title {\n            font-weight: 600;\n            color: var(--dark-text);\n            max-width: 300px;\n        }\n        \n        .sentence-content {\n            line-height: 1.6;\n            color: #333;\n            max-width: 900px; \/* Limit the width of sentence content *\/\n        }\n        \n        .highlight {\n            background-color: var(--warning-color);\n            color: white;\n            padding: 2px 6px;\n            border-radius: 4px;\n            font-weight: 600;\n        }\n        \n        .audio-control {\n            margin-left: 10px;\n            cursor: pointer;\n            font-size: 18px;\n            transition: all 0.3s ease;\n            display: inline-flex;\n            align-items: center;\n            justify-content: center;\n            width: 32px;\n            height: 32px;\n            border-radius: 50%;\n            background-color: #f0f0f0;\n        }\n        \n        .audio-control.active {\n            background-color: var(--accent-green);\n            color: white;\n            cursor: pointer;\n        }\n        \n        .audio-control.active:hover {\n            background-color: #5a9c32;\n            transform: scale(1.1);\n        }\n        \n        .audio-control.inactive {\n            background-color: #e0e0e0;\n            color: #999;\n            cursor: not-allowed;\n        }\n        \n        .audio-control.playing {\n            background-color: var(--primary-color);\n            color: white;\n            animation: pulse 1.5s infinite;\n        }\n        \n        \/* Audio progress bar styles *\/\n        .audio-progress-container {\n            width: 100%;\n            max-width: 900px; \/* Limit progress bar width *\/\n            height: 20px; \/* Increase height to accommodate handle *\/\n            background-color: #e0e0e0;\n            border-radius: 3px;\n            margin-top: 8px;\n            overflow: visible; \/* Allow handle to overflow *\/\n            display: none;\n            position: relative;\n            cursor: pointer;\n        }\n        \n        .audio-progress-bar {\n            height: 6px; \/* Keep the actual bar at 6px *\/\n            background-color: var(--primary-color);\n            width: 0%;\n            transition: width 0.1s linear;\n            position: relative;\n            top: 50%;\n            transform: translateY(-50%);\n            border-radius: 3px;\n        }\n        \n        \/* Draggable circle on progress bar *\/\n        .progress-handle {\n            position: absolute;\n            width: 16px;\n            height: 16px;\n            background-color: var(--accent-green);\n            border-radius: 50%;\n            top: 50%;\n            transform: translate(-50%, -50%);\n            cursor: pointer;\n            display: none;\n            box-shadow: 0 2px 4px rgba(0,0,0,0.2);\n            z-index: 10;\n            transition: transform 0.1s ease;\n            border: 2px solid white;\n        }\n        \n        .progress-handle:hover, .progress-handle.dragging {\n            transform: translate(-50%, -50%) scale(1.2);\n            background-color: #5a9c32;\n        }\n        \n        \/* Speed control styles *\/\n        .speed-control {\n            display: flex;\n            align-items: center;\n            margin-top: 8px;\n            display: none;\n        }\n        \n        .speed-label {\n            font-size: 12px;\n            color: var(--light-text);\n            margin-right: 8px;\n        }\n        \n        .speed-options {\n            display: flex;\n            gap: 5px;\n        }\n        \n        .speed-btn {\n            padding: 3px 8px;\n            font-size: 12px;\n            background-color: #f0f0f0;\n            border: 1px solid #ddd;\n            border-radius: 4px;\n            cursor: pointer;\n        }\n        \n        .speed-btn.active {\n            background-color: var(--primary-color);\n            color: white;\n            border-color: var(--primary-color);\n        }\n        \n        .speed-btn:hover:not(.active) {\n            background-color: #e0e0e0;\n        }\n        \n        @keyframes pulse {\n            0% { transform: scale(1); }\n            50% { transform: scale(1.05); }\n            100% { transform: scale(1); }\n        }\n        \n        .sentence-with-audio {\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n        }\n        \n        .no-results {\n            text-align: center;\n            padding: 40px;\n            color: var(--light-text);\n            font-size: 18px;\n            background-color: #f8f9fa;\n            border-radius: 12px;\n            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);\n        }\n        \n        .error-message {\n            color: #dc3545;\n            text-align: center;\n            padding: 20px;\n            background-color: #f8d7da;\n            border-radius: 12px;\n            margin: 20px 0;\n            box-shadow: 0 4px 15px rgba(220, 53, 69, 0.1);\n        }\n        \n        .stats {\n            margin-top: 20px;\n            padding: 15px;\n            background: linear-gradient(135deg, rgba(80, 142, 66, 0.1), rgba(80, 142, 66, 0.05));\n            border-radius: 12px;\n            color: var(--accent-green);\n            font-weight: 600;\n            border-left: 4px solid var(--accent-green);\n            box-shadow: 0 4px 15px rgba(80, 142, 66, 0.1);\n        }\n    <\/style>\n<\/head>\n<body>\n    <div class=\"container\">\n        <div class=\"header\">\n            <h1>\u4e2d\u6587\u6587\u7ae0\u8a5e\u5f59\u641c\u7d22\u5de5\u5177 Words in Articles<\/h1>\n            <p>\u672c\u7db2\u9801\u5f15\u7528\u300a\u666e\u901a\u8a71\u6c34\u5e73\u6e2c\u8a66\u5be6\u65bd\u7db1\u8981-2021\u7248\u300b\u4e2d\u768450\u7bc7\u6717\u8b80\u4f5c\u54c1<\/p>\n        <\/div>\n        \n        <div class=\"section\">\n            <div class=\"search-section\">\n            <div class=\"input-group\">\n                <input type=\"text\" id=\"searchInput\" placeholder=\"\u8acb\u8f38\u5165\u8981\u641c\u7d22\u7684\u4e2d\u6587\u5b57\u7b26\u6216\u8a5e\u8a9e...\" maxlength=\"50\">\n                <button id=\"searchButton\">\u641c\u7d22<\/button>\n                <button id=\"clearButton\">\u6e05\u9664<\/button>\n            <\/div>\n            <div class=\"search-info\">\n                \u63d0\u793a\uff1a\u8f38\u5165\u4e2d\u6587\u5b57\u7b26\u3001\u8a5e\u8a9e\u6216\u77ed\u8a9e\uff0c\u7cfb\u7d71\u5c07\u5728\u6240\u6709\u6587\u7ae0\u4e2d\u641c\u7d22\u5305\u542b\u8a72\u5167\u5bb9\u7684\u53e5\u5b50\u3002\n            <\/div>\n        <\/div>\n        \n        <div id=\"loadingMessage\" class=\"loading\">\n            \u6b63\u5728\u52a0\u8f09\u6587\u7ae0\u6578\u64da...\n        <\/div>\n        \n        <div id=\"errorMessage\" class=\"error-message\" style=\"display: none;\"><\/div>\n        \n        <div id=\"resultsSection\" class=\"results-section\" style=\"display: none;\">\n            <div id=\"resultsHeader\" class=\"results-header\"><\/div>\n            <table id=\"resultsTable\" class=\"results-table\">\n                <thead>\n                    <tr>\n                        <th>\u5e8f\u865f<\/th>\n                        <th>\u6587\u7ae0\u6a19\u984c<\/th>\n                        <th>\u5305\u542b\u8a72\u8a5e\u8a9e\u7684\u53e5\u5b50<\/th>\n                        <th style=\"width: 60px; text-align: center;\">\u97f3\u983b<\/th>\n                    <\/tr>\n                <\/thead>\n                <tbody id=\"resultsBody\">\n                <\/tbody>\n            <\/table>\n        <\/div>\n        \n        <div id=\"noResults\" class=\"no-results\" style=\"display: none;\">\n            \u672a\u627e\u5230\u5305\u542b\u8a72\u8a5e\u8a9e\u7684\u53e5\u5b50\uff0c\u8acb\u5617\u8a66\u5176\u4ed6\u95dc\u9375\u8a5e\u3002\n        <\/div>\n        \n        <div id=\"stats\" class=\"stats\" style=\"display: none;\"><\/div>\n        <\/div>\n    <\/div>\n\n    <script>\n        let articlesData = null;\n        \n        \/\/ Load articles data when page loads\n        window.addEventListener('DOMContentLoaded', function() {\n            loadArticlesData();\n            setupEventListeners();\n        });\n        \n        function setupEventListeners() {\n            const searchInput = document.getElementById('searchInput');\n            const searchButton = document.getElementById('searchButton');\n            const clearButton = document.getElementById('clearButton');\n            \n            searchButton.addEventListener('click', performSearch);\n            clearButton.addEventListener('click', clearResults);\n            \n            \/\/ Allow Enter key to trigger search\n            searchInput.addEventListener('keypress', function(e) {\n                if (e.key === 'Enter') {\n                    performSearch();\n                }\n            });\n            \n            \/\/ Clear results when input is empty\n            searchInput.addEventListener('input', function() {\n                if (this.value.trim() === '') {\n                    clearResults();\n                }\n            });\n        }\n        \n        async function loadArticlesData() {\n            try {\n                \/\/ URLs for the two JSON files\n                const urls = [\n                    'https:\/\/raw.githubusercontent.com\/oetools\/Chines_Articles\/refs\/heads\/main\/2021_50_articles-with_urls.json'\n                ];\n                \n                \/\/ Fetch both JSON files\n                const responses = await Promise.all(urls.map(url => fetch(url)));\n                \n                \/\/ Check if all responses are ok\n                for (let i = 0; i < responses.length; i++) {\n                    if (!responses[i].ok) {\n                        throw new Error(`HTTP error! status: ${responses[i].status} for file ${i + 1}`);\n                    }\n                }\n                \n                \/\/ Parse JSON data\n                const jsonData = await Promise.all(responses.map(response => response.json()));\n                \n                \/\/ Combine articles from both sources\n                const combinedArticles = [];\n                let totalSentences = 0;\n                \n                jsonData.forEach(data => {\n                    if (data.articles) {\n                        combinedArticles.push(...data.articles);\n                        totalSentences += data.total_sentences || 0;\n                    }\n                });\n                \n                \/\/ Create combined data structure\n                articlesData = {\n                    articles: combinedArticles,\n                    total_articles: combinedArticles.length,\n                    total_sentences: totalSentences\n                };\n                \n                document.getElementById('loadingMessage').style.display = 'none';\n                document.getElementById('stats').style.display = 'block';\n                document.getElementById('stats').innerHTML = \n                    `\u6578\u64da\u52a0\u8f09\u5b8c\u6210\uff1a\u5171 ${articlesData.total_articles} \u7bc7\u6587\u7ae0\uff0c${articlesData.total_sentences} \u500b\u53e5\u5b50`;\n                \n                console.log('Articles data loaded successfully:', articlesData);\n            } catch (error) {\n                console.error('Error loading articles data:', error);\n                document.getElementById('loadingMessage').style.display = 'none';\n                showError('\u7121\u6cd5\u52a0\u8f09\u6587\u7ae0\u6578\u64da\uff0c\u8acb\u6aa2\u67e5\u7db2\u7d61\u9023\u63a5\u6216\u7a0d\u5f8c\u91cd\u8a66\u3002');\n            }\n        }\n        \n        function performSearch() {\n            const searchTerm = document.getElementById('searchInput').value.trim();\n            \n            if (!searchTerm) {\n                showError('\u8acb\u8f38\u5165\u8981\u641c\u7d22\u7684\u5167\u5bb9');\n                return;\n            }\n            \n            if (!articlesData) {\n                showError('\u6587\u7ae0\u6578\u64da\u5c1a\u672a\u52a0\u8f09\u5b8c\u6210\uff0c\u8acb\u7a0d\u7b49');\n                return;\n            }\n            \n            hideError();\n            const results = searchInArticles(searchTerm);\n            displayResults(results, searchTerm);\n        }\n        \n        function searchInArticles(searchTerm) {\n            const results = [];\n            \n            articlesData.articles.forEach(article => {\n                article.sentences.forEach((sentence, sentenceIndex) => {\n                    if (sentence.includes(searchTerm)) {\n                        \/\/ Get audio URL if available\n                        const audioUrl = article.audios && article.audios[sentenceIndex] ? article.audios[sentenceIndex] : null;\n                        const hasAudio = audioUrl && audioUrl.trim() !== '';\n                        \n                        results.push({\n                            number: article.number,\n                            title: article.title,\n                            sentence: sentence,\n                            audioUrl: audioUrl,\n                            hasAudio: hasAudio,\n                            articleIndex: articlesData.articles.indexOf(article),\n                            sentenceIndex: sentenceIndex\n                        });\n                    }\n                });\n            });\n            \n            \/\/ Sort results: first by audio availability (audio first), then by article order\n            results.sort((a, b) => {\n                \/\/ First priority: sentences with audio come first\n                if (a.hasAudio && !b.hasAudio) return -1;\n                if (!a.hasAudio && b.hasAudio) return 1;\n                \n                \/\/ Second priority: maintain article order within each group\n                if (a.articleIndex !== b.articleIndex) {\n                    return a.articleIndex - b.articleIndex;\n                }\n                \n                \/\/ Third priority: maintain sentence order within same article\n                return a.sentenceIndex - b.sentenceIndex;\n            });\n            \n            return results;\n        }\n        \n        function displayResults(results, searchTerm) {\n            const resultsSection = document.getElementById('resultsSection');\n            const noResultsDiv = document.getElementById('noResults');\n            const resultsHeader = document.getElementById('resultsHeader');\n            const resultsBody = document.getElementById('resultsBody');\n            \n            if (results.length === 0) {\n                resultsSection.style.display = 'none';\n                noResultsDiv.style.display = 'block';\n                return;\n            }\n            \n            noResultsDiv.style.display = 'none';\n            resultsSection.style.display = 'block';\n            \n            resultsHeader.innerHTML = `\u641c\u7d22\u7d50\u679c\uff1a\u627e\u5230 ${results.length} \u500b\u5305\u542b\"${searchTerm}\"\u7684\u53e5\u5b50`;\n            \n            resultsBody.innerHTML = '';\n            \n            results.forEach((result, index) => {\n                const row = document.createElement('tr');\n                \n                \/\/ Highlight the search term in the sentence\n                const highlightedSentence = result.sentence.replace(\n                    new RegExp(escapeRegExp(searchTerm), 'g'),\n                    `<span class=\"highlight\">${searchTerm}<\/span>`\n                );\n                \n                \/\/ Create audio control based on availability\n                const hasAudio = result.audioUrl && result.audioUrl.trim() !== '';\n                const audioControlId = `audio-control-${index}`;\n                const audioControlClass = hasAudio ? 'audio-control active' : 'audio-control inactive';\n                const audioIcon = hasAudio ? '\u25b6' : '\u25b6';\n                const audioControlHtml = `<span id=\"${audioControlId}\" class=\"${audioControlClass}\" ${hasAudio ? `onclick=\"toggleAudio('${result.audioUrl}', '${audioControlId}', ${index})\"` : ''}>${audioIcon}<\/span>`;\n                \n                \/\/ Add progress bar container\n                const progressBarHtml = hasAudio ? \n                    `<div id=\"progress-container-${index}\" class=\"audio-progress-container\">\n                        <div id=\"progress-bar-${index}\" class=\"audio-progress-bar\"><\/div>\n                        <div id=\"progress-handle-${index}\" class=\"progress-handle\" draggable=\"false\"><\/div>\n                    <\/div>\n                    <div id=\"speed-control-${index}\" class=\"speed-control\">\n                        <span class=\"speed-label\">\u901f\u5ea6:<\/span>\n                        <div class=\"speed-options\">\n                            <button class=\"speed-btn\" onclick=\"setPlaybackSpeed(${index}, 0.5)\">0.5x<\/button>\n                            <button class=\"speed-btn\" onclick=\"setPlaybackSpeed(${index}, 0.8)\">0.8x<\/button>\n                            <button class=\"speed-btn active\" onclick=\"setPlaybackSpeed(${index}, 1.0)\">1.0x<\/button>\n                            <button class=\"speed-btn\" onclick=\"setPlaybackSpeed(${index}, 1.2)\">1.2x<\/button>\n                            <button class=\"speed-btn\" onclick=\"setPlaybackSpeed(${index}, 1.5)\">1.5x<\/button>\n                        <\/div>\n                    <\/div>` : '';\n                \n                \/\/ Use sequential numbering starting from 1\n                row.innerHTML = `\n                    <td class=\"article-number\">${index + 1}.<\/td>\n                    <td class=\"article-title\">${result.title}<\/td>\n                    <td class=\"sentence-content\">${highlightedSentence}${progressBarHtml}<\/td>\n                    <td style=\"text-align: center;\">${audioControlHtml}<\/td>\n                `;\n                \n                resultsBody.appendChild(row);\n            });\n        }\n        \n        function clearResults() {\n            document.getElementById('searchInput').value = '';\n            document.getElementById('resultsSection').style.display = 'none';\n            document.getElementById('noResults').style.display = 'none';\n            hideError();\n        }\n        \n        function showError(message) {\n            const errorDiv = document.getElementById('errorMessage');\n            errorDiv.textContent = message;\n            errorDiv.style.display = 'block';\n        }\n        \n        function hideError() {\n            document.getElementById('errorMessage').style.display = 'none';\n        }\n        \n        function escapeRegExp(string) {\n            return string.replace(\/[.*+?^${}()|[\\]\\\\]\/g, '\\\\$&');\n        }\n        \n        \/\/ Audio management\n        let currentAudio = null;\n        let currentControlId = null;\n        let currentAudioIndex = null;\n        let progressInterval = null;\n        let isSeeking = false;\n        let isDragging = false;\n        \n        function toggleAudio(audioUrl, controlId, index) {\n            const control = document.getElementById(controlId);\n            const progressContainer = document.getElementById(`progress-container-${index}`);\n            const progressBar = document.getElementById(`progress-bar-${index}`);\n            const progressHandle = document.getElementById(`progress-handle-${index}`);\n            const speedControl = document.getElementById(`speed-control-${index}`);\n            \n            \/\/ If the same audio is playing, stop it\n            if (currentAudio && currentControlId === controlId && !currentAudio.paused) {\n                stopCurrentAudio();\n                return;\n            }\n            \n            \/\/ Stop any currently playing audio\n            if (currentAudio && !currentAudio.paused) {\n                stopCurrentAudio();\n            }\n            \n            \/\/ Show progress bar and speed control for this audio\n            if (progressContainer) {\n                progressContainer.style.display = 'block';\n            }\n            if (speedControl) {\n                speedControl.style.display = 'flex';\n            }\n            \n            \/\/ Play new audio\n            try {\n                currentAudio = new Audio(audioUrl);\n                currentControlId = controlId;\n                currentAudioIndex = index;\n                \n                \/\/ Set default playback speed\n                if (currentAudio) {\n                    currentAudio.playbackRate = 1.0;\n                    \/\/ Update speed buttons to show 1.0x as active\n                    updateSpeedButtons(index, 1.0);\n                }\n                \n                \/\/ Update control appearance\n                control.innerHTML = '\u23f9';\n                control.classList.add('playing');\n                \n                \/\/ Set up event listeners\n                currentAudio.addEventListener('ended', function() {\n                    resetAudioControl(control);\n                    if (progressContainer) {\n                        progressContainer.style.display = 'none';\n                        progressContainer.style.cursor = 'pointer';\n                    }\n                    if (progressHandle) {\n                        progressHandle.style.display = 'none';\n                    }\n                    if (speedControl) {\n                        speedControl.style.display = 'none';\n                    }\n                    if (progressInterval) {\n                        clearInterval(progressInterval);\n                        progressInterval = null;\n                    }\n                    currentAudio = null;\n                    currentControlId = null;\n                    currentAudioIndex = null;\n                    isSeeking = false;\n                });\n                \n                currentAudio.addEventListener('error', function() {\n                    console.error('Audio playback error:', audioUrl);\n                    resetAudioControl(control);\n                    if (progressContainer) {\n                        progressContainer.style.display = 'none';\n                        progressContainer.style.cursor = 'pointer';\n                    }\n                    if (progressHandle) {\n                        progressHandle.style.display = 'none';\n                    }\n                    if (speedControl) {\n                        speedControl.style.display = 'none';\n                    }\n                    if (progressInterval) {\n                        clearInterval(progressInterval);\n                        progressInterval = null;\n                    }\n                    currentAudio = null;\n                    currentControlId = null;\n                    currentAudioIndex = null;\n                    isSeeking = false;\n                    alert('\u97f3\u983b\u64ad\u653e\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u97f3\u983b\u6587\u4ef6\u662f\u5426\u5b58\u5728\u3002');\n                });\n                \n                \/\/ Start progress bar update\n                if (progressBar && progressHandle) {\n                    \/\/ Add event listeners for dragging\n                    setupProgressDragging(progressContainer, progressHandle, index);\n                    \n                    if (progressInterval) {\n                        clearInterval(progressInterval);\n                    }\n                    progressInterval = setInterval(() => {\n                        if (currentAudio && currentAudio.duration && !isSeeking && !isDragging) {\n                            const progress = (currentAudio.currentTime \/ currentAudio.duration) * 100;\n                            progressBar.style.width = `${progress}%`;\n                            progressHandle.style.left = `${progress}%`;\n                            progressHandle.style.display = 'block';\n                        }\n                    }, 100);\n                }\n                \n                \/\/ Start playing\n                currentAudio.play();\n                \n            } catch (error) {\n                console.error('Error creating audio:', error);\n                resetAudioControl(control);\n                if (progressContainer) {\n                    progressContainer.style.display = 'none';\n                }\n                if (progressHandle) {\n                    progressHandle.style.display = 'none';\n                }\n                if (speedControl) {\n                    speedControl.style.display = 'none';\n                }\n                alert('\u97f3\u983b\u64ad\u653e\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u97f3\u983b\u6587\u4ef6\u662f\u5426\u5b58\u5728\u3002');\n            }\n        }\n        \n        function stopCurrentAudio() {\n            if (currentAudio) {\n                currentAudio.pause();\n                currentAudio.currentTime = 0;\n                \n                if (currentControlId) {\n                    const control = document.getElementById(currentControlId);\n                    if (control) {\n                        resetAudioControl(control);\n                    }\n                }\n                \n                \/\/ Hide progress bar and handle\n                if (currentAudioIndex !== null) {\n                    const progressContainer = document.getElementById(`progress-container-${currentAudioIndex}`);\n                    const progressHandle = document.getElementById(`progress-handle-${currentAudioIndex}`);\n                    const speedControl = document.getElementById(`speed-control-${currentAudioIndex}`);\n                    if (progressContainer) {\n                        progressContainer.style.display = 'none';\n                        progressContainer.style.cursor = 'pointer';\n                    }\n                    if (progressHandle) {\n                        progressHandle.style.display = 'none';\n                    }\n                    if (speedControl) {\n                        speedControl.style.display = 'none';\n                    }\n                }\n                \n                if (progressInterval) {\n                    clearInterval(progressInterval);\n                    progressInterval = null;\n                }\n                \n                currentAudio = null;\n                currentControlId = null;\n                currentAudioIndex = null;\n                isSeeking = false;\n            }\n        }\n        \n        function resetAudioControl(control) {\n            control.innerHTML = '\u25b6';\n            control.classList.remove('playing');\n        }\n        \n        \/\/ Setup dragging functionality for progress bar\n        function setupProgressDragging(progressContainer, progressHandle, index) {\n            let isDraggingLocal = false;\n            \n            \/\/ Mouse events\n            progressHandle.addEventListener('mousedown', (e) => {\n                e.preventDefault();\n                e.stopPropagation();\n                isDraggingLocal = true;\n                isDragging = true;\n                progressHandle.classList.add('dragging');\n                document.addEventListener('mousemove', handleMouseMove);\n                document.addEventListener('mouseup', handleMouseUp);\n            });\n            \n            progressContainer.addEventListener('mousedown', (e) => {\n                if (e.target === progressContainer) {\n                    seekAudio(e, index);\n                }\n            });\n            \n            function handleMouseMove(e) {\n                if (!isDraggingLocal || !currentAudio || currentAudioIndex !== index) return;\n                seekAudio(e, index);\n            }\n            \n            function handleMouseUp() {\n                isDraggingLocal = false;\n                isDragging = false;\n                progressHandle.classList.remove('dragging');\n                document.removeEventListener('mousemove', handleMouseMove);\n                document.removeEventListener('mouseup', handleMouseUp);\n            }\n            \n            \/\/ Touch events for mobile\n            progressHandle.addEventListener('touchstart', (e) => {\n                e.preventDefault();\n                isDraggingLocal = true;\n                isDragging = true;\n                progressHandle.classList.add('dragging');\n                document.addEventListener('touchmove', handleTouchMove, { passive: false });\n                document.addEventListener('touchend', handleTouchEnd);\n            });\n            \n            function handleTouchMove(e) {\n                if (!isDraggingLocal || !currentAudio || currentAudioIndex !== index) return;\n                e.preventDefault();\n                const touch = e.touches[0];\n                const mouseEvent = new MouseEvent('mousemove', {\n                    clientX: touch.clientX,\n                    clientY: touch.clientY\n                });\n                seekAudio(mouseEvent, index);\n            }\n            \n            function handleTouchEnd() {\n                isDraggingLocal = false;\n                isDragging = false;\n                progressHandle.classList.remove('dragging');\n                document.removeEventListener('touchmove', handleTouchMove);\n                document.removeEventListener('touchend', handleTouchEnd);\n            }\n        }\n        \n        \/\/ Seek audio when clicking\/dragging on progress bar\n        function seekAudio(event, index) {\n            if (!currentAudio || currentAudioIndex !== index) return;\n            \n            const progressContainer = document.getElementById(`progress-container-${index}`);\n            const progressBar = document.getElementById(`progress-bar-${index}`);\n            const progressHandle = document.getElementById(`progress-handle-${index}`);\n            \n            const rect = progressContainer.getBoundingClientRect();\n            const pos = (event.clientX - rect.left) \/ rect.width;\n            const clampedPos = Math.max(0, Math.min(1, pos)); \/\/ Clamp between 0 and 1\n            const percentage = clampedPos * 100;\n            \n            \/\/ Update UI immediately\n            progressBar.style.width = `${percentage}%`;\n            progressHandle.style.left = `${percentage}%`;\n            progressHandle.style.display = 'block';\n            \n            \/\/ Set audio time\n            if (currentAudio.duration) {\n                isSeeking = true;\n                currentAudio.currentTime = clampedPos * currentAudio.duration;\n                \n                \/\/ Reset seeking flag after a short delay\n                setTimeout(() => {\n                    isSeeking = false;\n                }, 100);\n            }\n        }\n        \n        \/\/ Set playback speed\n        function setPlaybackSpeed(index, speed) {\n            if (currentAudio && currentAudioIndex === index) {\n                currentAudio.playbackRate = speed;\n                updateSpeedButtons(index, speed);\n            }\n        }\n        \n        \/\/ Update speed button active states\n        function updateSpeedButtons(index, activeSpeed) {\n            const speedButtons = document.querySelectorAll(`#speed-control-${index} .speed-btn`);\n            speedButtons.forEach(button => {\n                const speedValue = parseFloat(button.textContent);\n                if (speedValue === activeSpeed) {\n                    button.classList.add('active');\n                } else {\n                    button.classList.remove('active');\n                }\n            });\n        }\n    <\/script>\n<\/body>\n<\/html>\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Words in Articles - \u4e2d\u6587\u6587\u7ae0\u8a5e\u5f59\u641c\u7d22 \u4e2d\u6587\u6587\u7ae0\u8a5e\u5f59\u641c\u7d22\u5de5\u5177 Words in Articles \u672c\u7db2\u9801\u5f15\u7528\u300a\u666e\u901a\u8a71\u6c34\u5e73\u6e2c\u8a66\u5be6\u65bd\u7db1\u8981-2021\u7248\u300b\u4e2d\u768450\u7bc7\u6717\u8b80\u4f5c\u54c1 \u641c\u7d22 \u6e05\u9664 \u63d0\u793a\uff1a\u8f38\u5165\u4e2d\u6587\u5b57\u7b26\u3001\u8a5e\u8a9e\u6216\u77ed\u8a9e\uff0c\u7cfb\u7d71\u5c07\u5728\u6240\u6709\u6587\u7ae0\u4e2d\u641c\u7d22\u5305\u542b\u8a72\u5167\u5bb9\u7684\u53e5\u5b50\u3002 \u6b63\u5728\u52a0\u8f09\u6587\u7ae0\u6578\u64da... \u5e8f\u865f \u6587\u7ae0\u6a19\u984c \u5305\u542b\u8a72\u8a5e\u8a9e\u7684\u53e5\u5b50 \u97f3\u983b \u672a\u627e\u5230\u5305\u542b\u8a72\u8a5e\u8a9e\u7684\u53e5\u5b50\uff0c\u8acb\u5617\u8a66\u5176\u4ed6\u95dc\u9375\u8a5e\u3002<\/p>\n","protected":false},"author":748,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"redux-templates_canvas","meta":{"_expiration-date-status":"","_expiration-date":0,"_expiration-date-type":"","_expiration-date-categories":[],"_expiration-date-options":[]},"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v17.3 (Yoast SEO v21.2) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Resource 2 Words in Articles - Hong Kong Metropolitan University<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Resource 2 Words in Articles\" \/>\n<meta property=\"og:description\" content=\"Words in Articles - \u4e2d\u6587\u6587\u7ae0\u8a5e\u5f59\u641c\u7d22 \u4e2d\u6587\u6587\u7ae0\u8a5e\u5f59\u641c\u7d22\u5de5\u5177 Words in Articles \u672c\u7db2\u9801\u5f15\u7528\u300a\u666e\u901a\u8a71\u6c34\u5e73\u6e2c\u8a66\u5be6\u65bd\u7db1\u8981-2021\u7248\u300b\u4e2d\u768450\u7bc7\u6717\u8b80\u4f5c\u54c1 \u641c\u7d22 \u6e05\u9664 \u63d0\u793a\uff1a\u8f38\u5165\u4e2d\u6587\u5b57\u7b26\u3001\u8a5e\u8a9e\u6216\u77ed\u8a9e\uff0c\u7cfb\u7d71\u5c07\u5728\u6240\u6709\u6587\u7ae0\u4e2d\u641c\u7d22\u5305\u542b\u8a72\u5167\u5bb9\u7684\u53e5\u5b50\u3002 \u6b63\u5728\u52a0\u8f09\u6587\u7ae0\u6578\u64da... \u5e8f\u865f \u6587\u7ae0\u6a19\u984c\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/\" \/>\n<meta property=\"og:site_name\" content=\"Hong Kong Metropolitan University\" \/>\n<meta property=\"article:modified_time\" content=\"2025-11-07T01:14:53+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Resource 2 Words in Articles - Hong Kong Metropolitan University","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/","og_locale":"en_US","og_type":"article","og_title":"Resource 2 Words in Articles","og_description":"Words in Articles - \u4e2d\u6587\u6587\u7ae0\u8a5e\u5f59\u641c\u7d22 \u4e2d\u6587\u6587\u7ae0\u8a5e\u5f59\u641c\u7d22\u5de5\u5177 Words in Articles \u672c\u7db2\u9801\u5f15\u7528\u300a\u666e\u901a\u8a71\u6c34\u5e73\u6e2c\u8a66\u5be6\u65bd\u7db1\u8981-2021\u7248\u300b\u4e2d\u768450\u7bc7\u6717\u8b80\u4f5c\u54c1 \u641c\u7d22 \u6e05\u9664 \u63d0\u793a\uff1a\u8f38\u5165\u4e2d\u6587\u5b57\u7b26\u3001\u8a5e\u8a9e\u6216\u77ed\u8a9e\uff0c\u7cfb\u7d71\u5c07\u5728\u6240\u6709\u6587\u7ae0\u4e2d\u641c\u7d22\u5305\u542b\u8a72\u5167\u5bb9\u7684\u53e5\u5b50\u3002 \u6b63\u5728\u52a0\u8f09\u6587\u7ae0\u6578\u64da... \u5e8f\u865f \u6587\u7ae0\u6a19\u984c","og_url":"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/","og_site_name":"Hong Kong Metropolitan University","article_modified_time":"2025-11-07T01:14:53+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/","url":"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/","name":"Resource 2 Words in Articles - Hong Kong Metropolitan University","isPartOf":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website"},"datePublished":"2025-10-14T06:22:57+00:00","dateModified":"2025-11-07T01:14:53+00:00","breadcrumb":{"@id":"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/resource-2-words-in-articles\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Open Educational Tools","item":"\/oetools\/"},{"@type":"ListItem","position":2,"name":"Resource 2 Words in Articles"}]},{"@type":"WebSite","@id":"https:\/\/www.hkmu.edu.hk\/oetools\/#website","url":"https:\/\/www.hkmu.edu.hk\/oetools\/","name":"Hong Kong Metropolitan University","description":"Open Educational Tools - Hong Kong Metropolitan University","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.hkmu.edu.hk\/oetools\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"}]}},"_links":{"self":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/30609"}],"collection":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/users\/748"}],"replies":[{"embeddable":true,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/comments?post=30609"}],"version-history":[{"count":15,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/30609\/revisions"}],"predecessor-version":[{"id":31081,"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/pages\/30609\/revisions\/31081"}],"wp:attachment":[{"href":"https:\/\/www.hkmu.edu.hk\/oetools\/wp-json\/wp\/v2\/media?parent=30609"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}