mirror of
https://github.com/WenPai-org/wenpai-trademark.git
synced 2025-08-17 19:31:10 +08:00
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:
parent
8b4dbb69cf
commit
734ed41ec3
3 changed files with 1185 additions and 0 deletions
329
admin-styles.css
Normal file
329
admin-styles.css
Normal 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
366
admin-template.php
Normal 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
490
wenpai-trademark.php
Executable 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();
|
Loading…
Add table
Add a link
Reference in a new issue