Add WenPai Trademark Symbol plugin core files

Introduce the initial implementation of the WenPai Trademark Symbol WordPress plugin, including the main plugin logic (wenpai-trademark.php), admin interface template (admin-template.php), and admin styles (admin-styles.css). Features include term management, scope and exclusion settings, import/export functionality, and AJAX-based admin operations.
This commit is contained in:
feibisi 2025-08-14 09:11:15 +08:00
parent 8b4dbb69cf
commit 734ed41ec3
3 changed files with 1185 additions and 0 deletions

329
admin-styles.css Normal file
View file

@ -0,0 +1,329 @@
/* WenPai Trademark Plugin Admin Styles */
.wenpai-admin {
max-width: 1200px;
}
.wenpai-card {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
margin: 20px 0;
padding: 20px;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.wenpai-card h2 {
margin-top: 0;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
color: #23282d;
}
.wenpai-card h3 {
margin-top: 25px;
margin-bottom: 15px;
color: #23282d;
}
#wenpai-admin-notices {
margin: 15px 0;
}
.wenpai-notice {
background: #fff;
border-left: 4px solid #00a0d2;
box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
margin: 5px 0 15px;
padding: 12px;
border-radius: 0 4px 4px 0;
}
.wenpai-notice.notice-success {
border-left-color: #46b450;
background-color: #f7fff7;
}
.wenpai-notice.notice-error {
border-left-color: #dc3232;
background-color: #fff7f7;
}
.wenpai-notice.notice-warning {
border-left-color: #ffb900;
background-color: #fffbf0;
}
.wenpai-loading {
opacity: 0.6;
pointer-events: none;
position: relative;
}
.wenpai-loading::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.7);
z-index: 1000;
}
.wenpai-terms-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.wenpai-terms-table th,
.wenpai-terms-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.wenpai-terms-table th {
background-color: #f9f9f9;
font-weight: 600;
color: #23282d;
}
.wenpai-terms-table tr:hover {
background-color: #f9f9f9;
}
.wenpai-terms-table .actions {
white-space: nowrap;
}
.wenpai-terms-table .actions .button {
margin-right: 5px;
font-size: 12px;
padding: 4px 8px;
height: auto;
line-height: 1.4;
}
.wenpai-form-row {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 15px;
}
.wenpai-form-row label {
min-width: 120px;
font-weight: 600;
color: #23282d;
}
.wenpai-form-row input,
.wenpai-form-row select {
flex: 1;
max-width: 300px;
}
.wenpai-form-row input[type="checkbox"] {
flex: none;
margin-right: 8px;
}
.wenpai-checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.wenpai-checkbox-group label {
display: flex;
align-items: center;
min-width: auto;
font-weight: normal;
}
.wenpai-file-upload {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.wenpai-description {
font-style: italic;
color: #666;
font-size: 13px;
margin-top: 5px;
}
.wenpai-button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.wenpai-button-primary {
background: #0073aa;
border-color: #0073aa;
color: #fff;
}
.wenpai-button-primary:hover {
background: #005a87;
border-color: #005a87;
}
.wenpai-button-secondary {
background: #f7f7f7;
border-color: #ccc;
color: #555;
}
.wenpai-button-secondary:hover {
background: #fafafa;
border-color: #999;
}
.wenpai-button-danger {
background: #dc3232;
border-color: #dc3232;
color: #fff;
}
.wenpai-button-danger:hover {
background: #c62d2d;
border-color: #c62d2d;
}
.wenpai-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.wenpai-stat-box {
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
text-align: center;
flex: 1;
}
.wenpai-stat-number {
font-size: 24px;
font-weight: bold;
color: #0073aa;
display: block;
}
.wenpai-stat-label {
font-size: 13px;
color: #666;
margin-top: 5px;
}
.wenpai-help-text {
background: #f0f8ff;
border: 1px solid #b3d9ff;
border-radius: 4px;
padding: 15px;
margin: 15px 0;
}
.wenpai-help-text h4 {
margin-top: 0;
color: #0073aa;
}
.wenpai-help-text ul {
margin-bottom: 0;
}
.wenpai-help-text li {
margin-bottom: 5px;
}
@media (max-width: 768px) {
.wenpai-form-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.wenpai-form-row label {
min-width: auto;
}
.wenpai-form-row input,
.wenpai-form-row select {
max-width: 100%;
width: 100%;
}
.wenpai-stats {
flex-direction: column;
gap: 10px;
}
.wenpai-button-group {
flex-direction: column;
}
.wenpai-file-upload {
flex-direction: column;
align-items: flex-start;
}
}
/* Animation for notices */
@keyframes wenpai-fade-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.wenpai-notice {
animation: wenpai-fade-in 0.3s ease-out;
}
/* Loading spinner */
.wenpai-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #0073aa;
border-radius: 50%;
animation: wenpai-spin 1s linear infinite;
margin-right: 8px;
}
@keyframes wenpai-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive table */
@media (max-width: 768px) {
.wenpai-terms-table {
font-size: 14px;
}
.wenpai-terms-table th,
.wenpai-terms-table td {
padding: 8px 4px;
}
.wenpai-terms-table .actions .button {
font-size: 11px;
padding: 3px 6px;
margin-right: 2px;
}
}

366
admin-template.php Normal file
View file

@ -0,0 +1,366 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$plugin = WenpaiTrademarkPlugin::get_instance();
$terms = $plugin->get_terms();
$scopes = $plugin->get_scopes();
$exclude_post_ids = \get_option(WenpaiTrademarkPlugin::OPTION_EXCLUDED_POST_IDS, '');
$exclude_html_tags = \get_option(WenpaiTrademarkPlugin::OPTION_EXCLUDED_TAGS, 'a,code,pre');
?>
<div class="wrap">
<h1>WenPai Trademark Symbol Settings</h1>
<div id="wenpai-admin-notices"></div>
<form id="wenpai-settings-form" method="post" action="options.php">
<?php \settings_fields('wenpai_trademark_settings'); ?>
<!-- Terms Management -->
<div class="card">
<h2>Trademark Terms</h2>
<table class="wp-list-table widefat fixed striped" id="terms-table">
<thead>
<tr>
<th>Term</th>
<th>Symbol</th>
<th>Position</th>
<th>Density</th>
<th>Case Sensitive</th>
<th>Whole Word</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($terms as $term => $config): ?>
<tr>
<td><?php echo \esc_html($term); ?></td>
<td><?php echo \esc_html($config['symbol']); ?></td>
<td><?php echo \esc_html($config['position']); ?></td>
<td><?php echo \esc_html($config['density']); ?></td>
<td><?php echo $config['case_sensitive'] ? 'Yes' : 'No'; ?></td>
<td><?php echo $config['whole_word'] ? 'Yes' : 'No'; ?></td>
<td>
<button type="button" class="button edit-term" data-term="<?php echo \esc_attr($term); ?>">Edit</button>
<button type="button" class="button delete-term" data-term="<?php echo \esc_attr($term); ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h3>Add New Term</h3>
<table class="form-table">
<tr>
<th><label for="new-term">Term</label></th>
<td><input type="text" id="new-term" class="regular-text" /></td>
</tr>
<tr>
<th><label for="new-symbol">Symbol</label></th>
<td>
<select id="new-symbol">
<option value=""> (Trademark)</option>
<option value="®">® (Registered)</option>
<option value="©">© (Copyright)</option>
<option value=""> (Service Mark)</option>
<option value=""> (Sound Recording)</option>
</select>
</td>
</tr>
<tr>
<th><label for="new-position">Position</label></th>
<td>
<select id="new-position">
<option value="after">After term</option>
<option value="before">Before term</option>
</select>
</td>
</tr>
<tr>
<th><label for="new-density">Density</label></th>
<td><input type="number" id="new-density" min="1" max="10" value="1" /></td>
</tr>
<tr>
<th><label for="new-case-sensitive">Case Sensitive</label></th>
<td><input type="checkbox" id="new-case-sensitive" /></td>
</tr>
<tr>
<th><label for="new-whole-word">Whole Word Only</label></th>
<td><input type="checkbox" id="new-whole-word" checked /></td>
</tr>
</table>
<button type="button" id="add-term" class="button button-primary">Add Term</button>
</div>
<!-- Scope Settings -->
<div class="card">
<h2>Application Scope</h2>
<table class="form-table">
<tr>
<th>Apply To</th>
<td>
<label><input type="checkbox" name="scopes[content]" value="1" <?php \checked(!empty($scopes['content'])); ?> /> Post Content</label><br>
<label><input type="checkbox" name="scopes[title]" value="1" <?php \checked(!empty($scopes['title'])); ?> /> Post Titles</label><br>
<label><input type="checkbox" name="scopes[widgets]" value="1" <?php \checked(!empty($scopes['widgets'])); ?> /> Widgets</label><br>
<label><input type="checkbox" name="scopes[comments]" value="1" <?php \checked(!empty($scopes['comments'])); ?> /> Comments</label>
</td>
</tr>
</table>
</div>
<!-- Exclusion Settings -->
<div class="card">
<h2>Exclusion Settings</h2>
<table class="form-table">
<tr>
<th><label for="exclude-post-ids">Exclude Post IDs</label></th>
<td>
<input type="text" id="exclude-post-ids" name="exclude_post_ids" value="<?php echo \esc_attr($exclude_post_ids); ?>" class="regular-text" />
<p class="description">Comma-separated list of post IDs to exclude from replacement.</p>
</td>
</tr>
<tr>
<th><label for="exclude-html-tags">Exclude HTML Tags</label></th>
<td>
<input type="text" id="exclude-html-tags" name="exclude_html_tags" value="<?php echo \esc_attr($exclude_html_tags); ?>" class="regular-text" />
<p class="description">Comma-separated list of HTML tags to exclude from replacement (e.g., a,code,pre).</p>
</td>
</tr>
</table>
</div>
<!-- Import/Export -->
<div class="card">
<h2>Import/Export</h2>
<table class="form-table">
<tr>
<th><label for="csv-file">Import CSV</label></th>
<td>
<input type="file" id="csv-file" accept=".csv" />
<button type="button" id="import-terms" class="button">Import Terms</button>
<p class="description">CSV format: Term,Symbol,Position,Density,Case Sensitive,Whole Word</p>
</td>
</tr>
<tr>
<th>Export</th>
<td>
<button type="button" id="export-terms" class="button">Export Terms as CSV</button>
</td>
</tr>
</table>
</div>
<?php \submit_button('Save Settings'); ?>
</form>
</div>
<style>
.card {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 4px;
margin: 20px 0;
padding: 20px;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.card h2 {
margin-top: 0;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
#wenpai-admin-notices {
margin: 15px 0;
}
.notice {
background: #fff;
border-left: 4px solid #00a0d2;
box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
margin: 5px 0 15px;
padding: 1px 12px;
}
.notice.notice-success {
border-left-color: #46b450;
}
.notice.notice-error {
border-left-color: #dc3232;
}
.loading {
opacity: 0.6;
pointer-events: none;
}
</style>
<script>
jQuery(document).ready(function($) {
var wenpaiAdmin = window.wenpaiAdmin || {};
function showNotice(message, type) {
var noticeClass = type === 'success' ? 'notice-success' : 'notice-error';
var notice = $('<div class="notice ' + noticeClass + '"><p>' + message + '</p></div>');
$('#wenpai-admin-notices').html(notice);
setTimeout(function() {
notice.fadeOut();
}, 5000);
}
function addTermToTable(term, config) {
var row = '<tr>' +
'<td>' + $('<div>').text(term).html() + '</td>' +
'<td>' + $('<div>').text(config.symbol).html() + '</td>' +
'<td>' + $('<div>').text(config.position).html() + '</td>' +
'<td>' + $('<div>').text(config.density).html() + '</td>' +
'<td>' + (config.case_sensitive ? 'Yes' : 'No') + '</td>' +
'<td>' + (config.whole_word ? 'Yes' : 'No') + '</td>' +
'<td>' +
'<button type="button" class="button edit-term" data-term="' + $('<div>').text(term).html() + '">Edit</button> ' +
'<button type="button" class="button delete-term" data-term="' + $('<div>').text(term).html() + '">Delete</button>' +
'</td>' +
'</tr>';
$('#terms-table tbody').append(row);
}
// Add new term
$('#add-term').click(function() {
var term = $('#new-term').val().trim();
var symbol = $('#new-symbol').val();
var position = $('#new-position').val();
var density = parseInt($('#new-density').val());
var caseSensitive = $('#new-case-sensitive').is(':checked');
var wholeWord = $('#new-whole-word').is(':checked');
if (!term) {
showNotice('Please enter a term.', 'error');
return;
}
var termData = {
action: 'wenpai_save_settings',
nonce: wenpaiAdmin.nonce,
terms: {}
};
termData.terms[term] = {
symbol: symbol,
position: position,
density: density,
case_sensitive: caseSensitive,
whole_word: wholeWord
};
$.post(wenpaiAdmin.ajaxUrl, termData, function(response) {
if (response.success) {
addTermToTable(term, termData.terms[term]);
$('#new-term').val('');
$('#new-density').val(1);
$('#new-case-sensitive').prop('checked', false);
$('#new-whole-word').prop('checked', true);
showNotice('Term added successfully!', 'success');
} else {
showNotice(response.data.message || 'Failed to add term.', 'error');
}
});
});
// Delete term
$(document).on('click', '.delete-term', function() {
var term = $(this).data('term');
var row = $(this).closest('tr');
if (!confirm(wenpaiAdmin.strings.confirmDelete)) {
return;
}
$.post(wenpaiAdmin.ajaxUrl, {
action: 'wenpai_delete_term',
nonce: wenpaiAdmin.nonce,
term: term
}, function(response) {
if (response.success) {
row.fadeOut(function() {
row.remove();
});
showNotice('Term deleted successfully!', 'success');
} else {
showNotice(response.data.message || 'Failed to delete term.', 'error');
}
});
});
// Import terms
$('#import-terms').click(function() {
var fileInput = $('#csv-file')[0];
if (!fileInput.files.length) {
showNotice('Please select a CSV file.', 'error');
return;
}
var formData = new FormData();
formData.append('action', 'wenpai_import_terms');
formData.append('nonce', wenpaiAdmin.nonce);
formData.append('csv_file', fileInput.files[0]);
$.ajax({
url: wenpaiAdmin.ajaxUrl,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
location.reload();
} else {
showNotice(response.data.message || 'Import failed.', 'error');
}
}
});
});
// Export terms
$('#export-terms').click(function() {
$.post(wenpaiAdmin.ajaxUrl, {
action: 'wenpai_export_terms',
nonce: wenpaiAdmin.nonce
}, function(response) {
if (response.success) {
var blob = new Blob([response.data.csv], { type: 'text/csv' });
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'wenpai-trademark-terms.csv';
a.click();
window.URL.revokeObjectURL(url);
} else {
showNotice(response.data.message || 'Export failed.', 'error');
}
});
});
// Save settings
$('#submit').click(function(e) {
e.preventDefault();
var formData = {
action: 'wenpai_save_settings',
nonce: wenpaiAdmin.nonce,
scopes: {},
exclude_post_ids: $('#exclude-post-ids').val(),
exclude_html_tags: $('#exclude-html-tags').val()
};
$('input[name^="scopes["]').each(function() {
var name = $(this).attr('name').match(/scopes\[(.+)\]/)[1];
formData.scopes[name] = $(this).is(':checked');
});
$.post(wenpaiAdmin.ajaxUrl, formData, function(response) {
if (response.success) {
showNotice('Settings saved successfully!', 'success');
} else {
showNotice(response.data.message || 'Failed to save settings.', 'error');
}
});
});
});
</script>

490
wenpai-trademark.php Executable file
View file

@ -0,0 +1,490 @@
<?php
/**
* Plugin Name: WenPai Trademark Symbol
* Description: Automatically adds trademark, registered, or copyright symbols to specified terms with enhanced controls.
* Version: 2.0.0
* Author: WenPai.org
* License: GPLv3 or later
* Text Domain: wenpai-trademark
* Domain Path: /languages
* Requires at least: 5.0
* Tested up to: 6.4
* Requires PHP: 7.4
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Define plugin constants
define('WENPAI_TRADEMARK_VERSION', '2.0.0');
define('WENPAI_TRADEMARK_PATH', \plugin_dir_path(__FILE__));
define('WENPAI_TRADEMARK_URL', \plugin_dir_url(__FILE__));
define('WENPAI_TRADEMARK_FILE', __FILE__);
/**
* Main plugin class with all core functionality
*/
class WenpaiTrademarkPlugin {
private static $instance = null;
private $replacing = false;
private $cached_terms = null;
// Option constants
const OPTION_TERMS = 'wenpai_trademark_terms';
const OPTION_SCOPES = 'wenpai_trademark_scopes';
const OPTION_EXCLUDED_POST_IDS = 'wenpai_trademark_excluded_post_ids';
const OPTION_EXCLUDED_TAGS = 'wenpai_trademark_excluded_tags';
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
\register_activation_hook(WENPAI_TRADEMARK_FILE, [$this, 'activate']);
\register_deactivation_hook(WENPAI_TRADEMARK_FILE, [$this, 'deactivate']);
\add_action('plugins_loaded', [$this, 'init']);
\add_action('admin_init', [$this, 'register_settings']);
\add_action('wp_loaded', [$this, 'apply_filters']);
\add_action('admin_menu', [$this, 'register_admin_page']);
\add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
// AJAX hooks
\add_action('wp_ajax_wenpai_save_settings', [$this, 'ajax_save_settings']);
\add_action('wp_ajax_wenpai_import_terms', [$this, 'ajax_import_terms']);
\add_action('wp_ajax_wenpai_export_terms', [$this, 'ajax_export_terms']);
\add_action('wp_ajax_wenpai_delete_term', [$this, 'ajax_delete_term']);
}
public function init() {
\load_plugin_textdomain('wenpai-trademark', false, dirname(\plugin_basename(WENPAI_TRADEMARK_FILE)) . '/languages/');
}
// ========== ACTIVATION/DEACTIVATION ==========
public function activate() {
$default_terms = [
'WordPress' => [
'symbol' => '®',
'position' => 'after',
'density' => 1,
'case_sensitive' => true,
'whole_word' => true
]
];
\add_option(self::OPTION_TERMS, $default_terms);
\add_option(self::OPTION_SCOPES, [
'content' => true,
'title' => true,
'widgets' => false,
'comments' => false
]);
\add_option(self::OPTION_EXCLUDED_POST_IDS, '');
\add_option(self::OPTION_EXCLUDED_TAGS, 'a,code,pre');
\flush_rewrite_rules();
}
public function deactivate() {
\wp_cache_flush();
}
// ========== SETTINGS MANAGEMENT ==========
public function register_settings() {
\register_setting('wenpai_trademark_settings', self::OPTION_TERMS, [
'sanitize_callback' => [$this, 'sanitize_terms'],
'default' => []
]);
\register_setting('wenpai_trademark_settings', self::OPTION_SCOPES, [
'sanitize_callback' => [$this, 'sanitize_scopes'],
'default' => [
'content' => true,
'title' => true,
'widgets' => true,
'comments' => true,
]
]);
\register_setting('wenpai_trademark_settings', self::OPTION_EXCLUDED_POST_IDS, [
'sanitize_callback' => '\sanitize_text_field',
'default' => ''
]);
\register_setting('wenpai_trademark_settings', self::OPTION_EXCLUDED_TAGS, [
'sanitize_callback' => '\sanitize_text_field',
'default' => 'a,code,pre'
]);
}
public function sanitize_terms($terms) {
if (!is_array($terms)) {
return [];
}
$sanitized = [];
foreach ($terms as $term => $data) {
$clean_term = \sanitize_text_field($term);
if (empty($clean_term)) continue;
$sanitized[$clean_term] = [
'symbol' => \sanitize_text_field($data['symbol'] ?? '™'),
'position' => in_array($data['position'] ?? 'after', ['before', 'after']) ? $data['position'] : 'after',
'density' => max(1, min(10, intval($data['density'] ?? 1))),
'case_sensitive' => !empty($data['case_sensitive']),
'whole_word' => !empty($data['whole_word'])
];
}
return $sanitized;
}
public function sanitize_scopes($scopes) {
if (!is_array($scopes)) {
return ['content' => true];
}
$valid_scopes = ['content', 'title', 'widgets', 'comments'];
$sanitized = [];
foreach ($valid_scopes as $scope) {
$sanitized[$scope] = !empty($scopes[$scope]);
}
return $sanitized;
}
public function get_terms() {
if (null === $this->cached_terms) {
$this->cached_terms = \get_option(self::OPTION_TERMS, []);
}
return $this->cached_terms;
}
public function get_scopes() {
return \get_option(self::OPTION_SCOPES, ['content' => true]);
}
// ========== TEXT REPLACEMENT ENGINE ==========
public function apply_filters() {
$scopes = $this->get_scopes();
if (!empty($scopes['content'])) {
\add_filter('the_content', [$this, 'replace_terms'], 20);
}
if (!empty($scopes['title'])) {
\add_filter('the_title', [$this, 'replace_terms'], 20);
}
if (!empty($scopes['widgets'])) {
\add_filter('widget_text', [$this, 'replace_terms'], 20);
}
if (!empty($scopes['comments'])) {
\add_filter('comment_text', [$this, 'replace_terms'], 20);
}
}
public function replace_terms($text) {
if ($this->replacing || empty($text) || !is_string($text)) {
return $text;
}
if ($this->should_skip_replacement()) {
return $text;
}
$this->replacing = true;
$terms = $this->get_terms();
if (empty($terms)) {
$this->replacing = false;
return $text;
}
$excluded_tags = \get_option(self::OPTION_EXCLUDED_TAGS, 'a,code,pre');
$tags_array = array_map('trim', explode(',', $excluded_tags));
$processed_text = $this->process_html_content($text, $terms, $tags_array);
$this->replacing = false;
return $processed_text;
}
private function should_skip_replacement() {
if (!\is_singular()) {
return false;
}
$excluded_ids = \get_option(self::OPTION_EXCLUDED_POST_IDS, '');
if (empty($excluded_ids)) {
return false;
}
$excluded_array = array_map('trim', explode(',', $excluded_ids));
return in_array((string)\get_the_ID(), $excluded_array);
}
private function process_html_content($content, $terms, $excluded_tags) {
if (empty($excluded_tags)) {
return $this->replace_text_content($content, $terms);
}
$tag_pattern = '<(' . implode('|', array_map('preg_quote', $excluded_tags)) . ')\b[^>]*>.*?</\\1>';
return preg_replace_callback(
'/(' . $tag_pattern . ')|([^<]+)/is',
function($matches) use ($terms) {
if (!empty($matches[1])) {
return $matches[1];
}
return $this->replace_text_content($matches[2], $terms);
},
$content
);
}
private function replace_text_content($text, $terms) {
foreach ($terms as $term => $config) {
$pattern = $this->build_pattern($term, $config);
$replacement = $this->build_replacement($term, $config);
$limit = max(1, intval($config['density']));
$text = preg_replace($pattern, $replacement, $text, $limit);
}
return $text;
}
private function build_pattern($term, $config) {
$escaped_term = preg_quote($term, '/');
if (!empty($config['whole_word'])) {
$pattern = '/\\b' . $escaped_term . '\\b/';
} else {
$pattern = '/' . $escaped_term . '/';
}
if (empty($config['case_sensitive'])) {
$pattern .= 'i';
}
return $pattern;
}
private function build_replacement($term, $config) {
$symbol = $config['symbol'] ?? '™';
$position = $config['position'] ?? 'after';
if ($position === 'before') {
return $symbol . '$0';
}
return '$0' . $symbol;
}
// ========== ADMIN INTERFACE ==========
public function register_admin_page() {
\add_options_page(
'WenPai Trademark Symbol Settings',
'Trademark Symbol',
'manage_options',
'wenpai-trademark-settings',
[$this, 'render_admin_page']
);
}
public function enqueue_admin_scripts($hook) {
if ('settings_page_wenpai-trademark-settings' !== $hook) {
return;
}
// Enqueue admin styles
\wp_enqueue_style(
'wenpai-admin-styles',
WENPAI_TRADEMARK_URL . 'admin-styles.css',
[],
WENPAI_TRADEMARK_VERSION
);
// Enqueue jQuery and localize script
\wp_enqueue_script('jquery');
\wp_localize_script('jquery', 'wenpaiAdmin', [
'ajaxUrl' => \admin_url('admin-ajax.php'),
'nonce' => \wp_create_nonce('wenpai_trademark_nonce'),
'strings' => [
'confirmDelete' => 'Are you sure you want to delete this term?',
'importSuccess' => 'Terms imported successfully!',
'exportError' => 'Export failed. Please try again.',
'saveSuccess' => 'Settings saved successfully!',
'saveError' => 'Failed to save settings. Please try again.',
]
]);
}
public function render_admin_page() {
if (!\current_user_can('manage_options')) {
return;
}
include WENPAI_TRADEMARK_PATH . 'admin-template.php';
}
// ========== AJAX HANDLERS ==========
private function verify_nonce() {
if (!\wp_verify_nonce($_POST['nonce'] ?? '', 'wenpai_trademark_nonce')) {
\wp_send_json_error(['message' => 'Security check failed']);
}
}
public function ajax_save_settings() {
$this->verify_nonce();
if (!\current_user_can('manage_options')) {
\wp_send_json_error(['message' => 'Permission denied']);
}
try {
if (isset($_POST['terms'])) {
$terms = $this->sanitize_terms($_POST['terms']);
\update_option(self::OPTION_TERMS, $terms);
$this->cached_terms = null;
}
if (isset($_POST['scopes'])) {
$scopes = $this->sanitize_scopes($_POST['scopes']);
\update_option(self::OPTION_SCOPES, $scopes);
}
if (isset($_POST['exclude_post_ids'])) {
\update_option(self::OPTION_EXCLUDED_POST_IDS, \sanitize_text_field($_POST['exclude_post_ids']));
}
if (isset($_POST['exclude_html_tags'])) {
\update_option(self::OPTION_EXCLUDED_TAGS, \sanitize_text_field($_POST['exclude_html_tags']));
}
\wp_send_json_success(['message' => 'Settings saved successfully']);
} catch (Exception $e) {
\wp_send_json_error(['message' => 'Failed to save settings: ' . $e->getMessage()]);
}
}
public function ajax_import_terms() {
$this->verify_nonce();
if (!\current_user_can('manage_options')) {
\wp_send_json_error(['message' => 'Permission denied']);
}
if (!isset($_FILES['csv_file'])) {
\wp_send_json_error(['message' => 'No file uploaded']);
}
$file = $_FILES['csv_file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
\wp_send_json_error(['message' => 'File upload error']);
}
$csv_data = file_get_contents($file['tmp_name']);
$lines = str_getcsv($csv_data, "\n");
$imported_terms = [];
foreach ($lines as $line) {
$data = str_getcsv($line);
if (count($data) >= 2) {
$imported_terms[trim($data[0])] = [
'symbol' => trim($data[1]),
'position' => isset($data[2]) ? trim($data[2]) : 'after',
'density' => isset($data[3]) ? max(1, intval($data[3])) : 1,
'case_sensitive' => isset($data[4]) ? !empty($data[4]) : false,
'whole_word' => isset($data[5]) ? !empty($data[5]) : true
];
}
}
if (empty($imported_terms)) {
\wp_send_json_error(['message' => 'No valid terms found in CSV']);
}
$current_terms = $this->get_terms();
$merged_terms = array_merge($current_terms, $imported_terms);
\update_option(self::OPTION_TERMS, $merged_terms);
$this->cached_terms = null;
\wp_send_json_success([
'message' => sprintf('Successfully imported %d terms', count($imported_terms)),
'terms' => $merged_terms
]);
}
public function ajax_export_terms() {
$this->verify_nonce();
if (!\current_user_can('manage_options')) {
\wp_send_json_error(['message' => 'Permission denied']);
}
$terms = $this->get_terms();
if (empty($terms)) {
\wp_send_json_error(['message' => 'No terms to export']);
}
$csv_content = "Term,Symbol,Position,Density,Case Sensitive,Whole Word\n";
foreach ($terms as $term => $config) {
$csv_content .= sprintf(
"\"%s\",\"%s\",\"%s\",%d,%s,%s\n",
$term,
$config['symbol'],
$config['position'],
$config['density'],
$config['case_sensitive'] ? 'true' : 'false',
$config['whole_word'] ? 'true' : 'false'
);
}
\wp_send_json_success(['csv' => $csv_content]);
}
public function ajax_delete_term() {
$this->verify_nonce();
if (!\current_user_can('manage_options')) {
\wp_send_json_error(['message' => 'Permission denied']);
}
$term = \sanitize_text_field($_POST['term'] ?? '');
if (empty($term)) {
\wp_send_json_error(['message' => 'Term not specified']);
}
$terms = $this->get_terms();
if (!isset($terms[$term])) {
\wp_send_json_error(['message' => 'Term not found']);
}
unset($terms[$term]);
\update_option(self::OPTION_TERMS, $terms);
$this->cached_terms = null;
\wp_send_json_success(['message' => 'Term deleted successfully']);
}
}
// Initialize the plugin
WenpaiTrademarkPlugin::get_instance();