Compare commits

..

1 commit

Author SHA1 Message Date
c4905e952e plugin upload 2025-12-12 23:29:08 -05:00
14383 changed files with 1953 additions and 1911992 deletions

689
LICENSE
View file

@ -1,277 +1,642 @@
# Maple Open Tech License (MOTL) Version 1.0
MAPLE OPEN TECH LICENSE (MOTL) VERSION 1.0
**Source-Available · Time-Released · Ethical Use License**
Source-Available. Time-Released. Fair.
Copyright © 2025 Maple Open Tech
Copyright 2025 Maple Open Tech
---
================================================================================
## License Parameters
LICENSE PARAMETERS
- **Licensed Work:** [SOFTWARE NAME]
- **Version:** [VERSION NUMBER]
- **Release Date:** [YYYY-MM-DD]
- **Change Date:** Four (4) years from the Release Date, or [YYYY-MM-DD]
- **Change Effect:** Removal of commercial and revenue-centered restrictions
- **Licensor:** Maple Open Tech (https://mapleopentech.ca)
Licensed Work: Maple Open Tech Monorepo
Version: 1.0
Release Date: 2025-12-05
Change Date: Four (4) years from the Release Date
Change License: MIT License (https://opensource.org/licenses/MIT)
Licensor: Maple Open Tech (https://mapleopentech.ca)
---
================================================================================
## Plain-English Summary (Non-Binding)
EU Users: This license is supplemented by the EU Compliance Addendum which
provides additional consumer protections required under EU law.
### Before the Change Date
AI Features: Use of AI-powered features is governed by the AI Terms Addendum.
- ✅ View, audit, and study the source code
- ✅ Personal, academic, evaluation, and non-production use
- ✅ Internal testing and proof-of-concept
- ✅ Submit contributions
- ✅ Ethical restrictions in Section 4A apply
Data Processing: Where GDPR applies, a Data Processing Agreement is available.
- ❌ Production or business use without a Commercial License
- ❌ Hosting or SaaS use without a Commercial License
- ❌ Competing commercial services
================================================================================
### After the Change Date
PLAIN ENGLISH SUMMARY
- ✅ Production, commercial, and unrestricted use is permitted
- ⚠️ Ethical restrictions in Section 4A continue to apply
Before the Change Date:
---
YES - View and Audit: Read the source code, verify security, study how
it works
## 1. Definitions
YES - Non-Production Use: Development, testing, evaluation, academic
research
**“Licensed Work”** means the software, source code, object code, documentation, and related materials identified above.
YES - Personal Use: Run it for yourself, your family, your personal
projects
**“Licensor”** means Maple Open Tech.
YES - Contribute: Submit bug reports, patches, improvements
**“You”** or **“Licensee”** means the individual or entity exercising rights under this License.
PAID - Production Use: Requires a Commercial License from Maple Open Tech
**“Production Use”** means use of the Licensed Work in a live or operational environment that provides value to You or to third parties, including business operations, commercial services, or processing real (non-test) data.
PAID - Hosting/SaaS: Requires a Commercial License from Maple Open Tech
**“Service Offering”** means providing the functionality of the Licensed Work to third parties as a hosted, managed, cloud, or software-as-a-service offering.
NO - Compete: Cannot use to build a competing commercial service
**“Non-Production Use”** means personal, academic, research, evaluation, development, testing, or proof-of-concept use.
After the Change Date:
**“Companion Animals”** means animals commonly kept for companionship, assistance, service, or protection, including dogs, cats, and similar domesticated animals, and excludes animals raised primarily for food, fiber, or agricultural production.
YES - Everything: This version becomes MIT-licensed - do whatever you want
---
================================================================================
## 2. Grant of Rights
LICENSE TERMS
Subject to the terms of this License, the Licensor grants You a limited, worldwide, non-exclusive, non-transferable license to:
--------------------------------------------------------------------------------
SECTION 1. DEFINITIONS
--------------------------------------------------------------------------------
1. Copy, view, and study the Licensed Work;
2. Modify the Licensed Work for Non-Production Use;
3. Create derivative works for Non-Production Use;
4. Submit contributions to the Licensed Work.
"Licensed Work" means the source code, object code, documentation, and
associated files identified above and distributed under this License.
Production Use or Service Offerings prior to the Change Date require a separate Commercial License.
"Maple Open Tech" or "Licensor" means the copyright holder(s) of the
Licensed Work.
---
"You" or "Licensee" means the individual or entity exercising rights under
this License.
## 3. Permitted Uses Without a Commercial License
"Release Date" means the date the Licensed Work was first distributed under
this License, as specified above.
"Change Date" means the date specified above, after which the Licensed Work
becomes available under the Change License.
"Change License" means the MIT License, or such other open source license as
specified above.
"Production Use" means use of the Licensed Work in a live environment where
the Licensed Work or output derived from it provides value to You or third
parties, including but not limited to:
- Deploying as part of a business application
- Processing real (non-test) data
- Serving end users or customers
- Internal business operations beyond evaluation
"Service Offering" means providing the functionality of the Licensed Work to
third parties as a hosted, managed, cloud, or software-as-a-service offering,
whether paid or unpaid.
"Competing Service" means a Service Offering whose primary purpose
substantially overlaps with the primary purpose of the Licensed Work.
"Non-Production Use" means use solely for:
- Development and testing
- Personal, non-commercial purposes
- Academic research and education
- Evaluation and proof-of-concept
- Contributing improvements back to the Licensed Work
--------------------------------------------------------------------------------
SECTION 2. GRANT OF RIGHTS
--------------------------------------------------------------------------------
Subject to the terms of this License, Maple Open Tech grants You a limited,
worldwide, non-exclusive, non-transferable license to:
(a) Source Access - Copy, view, study, and audit the source code of the
Licensed Work.
(b) Non-Production Use - Use, modify, and create derivative works of the
Licensed Work for Non-Production Use, at no cost.
(c) Contributions - Submit bug reports, patches, documentation improvements,
and other contributions to the Licensed Work.
(d) Production Use (with Commercial License) - Use the Licensed Work in
Production Use if You have obtained a Commercial License from Maple
Open Tech.
--------------------------------------------------------------------------------
SECTION 3. ADDITIONAL USE GRANT
--------------------------------------------------------------------------------
The following uses are permitted without a Commercial License:
- Personal, non-commercial use;
- Internal evaluation for a reasonable period not exceeding ninety (90) days;
- Academic teaching or non-commercial research;
- Use by registered non-profit or charitable organizations for non-commercial purposes;
- Government use for **civilian, non-coercive public-service purposes**, such as education, health, or infrastructure administration, **excluding military, intelligence, immigration enforcement, or mass-surveillance uses**.
(a) Personal Use - Individuals using the Licensed Work for personal,
non-commercial purposes.
---
(b) Internal Evaluation - Businesses evaluating the Licensed Work for
potential adoption, for a reasonable evaluation period not exceeding
ninety (90) days.
## 4. Restrictions
(c) Academic and Research - Accredited educational institutions and
non-commercial research organizations using the Licensed Work for
teaching or research.
Before the Change Date, You may not:
(d) Registered Non-Profits - Organizations registered as non-profit or
charitable under applicable law, using the Licensed Work for their
charitable purposes.
1. Use the Licensed Work in Production Use without a Commercial License;
2. Offer the Licensed Work as a Service Offering without a Commercial License;
3. Use the Licensed Work to create a competing commercial product or service;
4. Remove or obscure copyright or proprietary notices;
5. Use Licensor trademarks without written permission;
6. Sublicense, sell, rent, lease, or transfer the Licensed Work.
(e) Government Use - Government entities using the Licensed Work for public
services (not for resale or Service Offerings).
These restrictions apply to modified and derivative works.
--------------------------------------------------------------------------------
SECTION 4. RESTRICTIONS
--------------------------------------------------------------------------------
---
4.1 Before the Change Date, You may NOT:
## 4A. Prohibited Uses (Ethical Restrictions)
(a) Use the Licensed Work in Production Use without a Commercial License.
Notwithstanding any other provision of this License or any Commercial License, You may not use the Licensed Work, directly or indirectly, for any of the following purposes:
(b) Offer the Licensed Work as a Service Offering without a Commercial License.
### 4A.1 Weapons or Lethal Systems
The development, operation, support, or optimization of weapons, weapon systems, or systems designed primarily to cause physical injury or death to humans.
(c) Offer a Competing Service using the Licensed Work, with or without
modification.
### 4A.2 Lethal, Extrajudicial, or Discriminatory Harm
Any activity intended to facilitate or materially contribute to:
- extrajudicial killing;
- collective punishment;
- targeting of individuals or groups based on protected characteristics; or
- treatment of people as threats or “undesirables” outside lawful due process.
(d) Remove, alter, or obscure any copyright, trademark, or other proprietary
notices.
### 4A.3 Mass Surveillance or Population Control
Mass or bulk surveillance of civilian populations, or systems designed primarily for population control, suppression, or intimidation rather than individualized, lawful investigation.
(e) Use Maple Open Tech's name, logo, or trademarks without prior written
consent.
### 4A.4 Immigration Enforcement and Removal
(f) Sublicense, sell, rent, lease, or otherwise transfer the Licensed Work.
Use of the Licensed Work for immigration enforcement, deportation, detention, or population removal is prohibited where such use is primarily coercive, punitive, discriminatory, or designed for mass identification, profiling, targeting, detention, or expulsion of individuals based on immigration status.
4.2 These restrictions apply to:
This prohibition includes use by agencies, programs, or contractors whose primary mandate is large-scale immigration control, population screening, or forced removal, and applies regardless of whether such use is described as civil, administrative, or national-security related.
- The Licensed Work in its original form
- Any modified versions or derivative works
- Any work incorporating any portion of the Licensed Work
#### Lawful Individualized Removal Exception
--------------------------------------------------------------------------------
SECTION 5. TIME-RELEASE PROVISION
--------------------------------------------------------------------------------
Notwithstanding the above, use of the Licensed Work is permitted **solely** to support **individualized, lawful removal actions** where **all** of the following conditions are met:
5.1 Automatic License Change
a. The removal is based on a final, individualized decision issued by a competent court or tribunal with jurisdiction, following due process and an opportunity to be heard;
b. The Licensed Work is not used to identify, profile, select, prioritize, or generate targets for removal, but only to support administrative or logistical functions after the decision has been made;
c. The use involves meaningful human oversight and decision-making at all stages, and does not rely on automated or semi-automated systems to recommend, optimize, or scale removal actions;
d. The Licensed Work is not used to conduct mass operations, sweeps, raids, predictive enforcement, or population-level screening; and
e. The use complies with applicable law, oversight, and accountability mechanisms in the jurisdiction in which it occurs.
Effective on the Change Date, this License automatically and irrevocably
converts to the Change License (MIT License) for the specific version of the
Licensed Work identified above.
Any use that exceeds these conditions, or that materially contributes to mass, automated, or coercive immigration enforcement, constitutes a prohibited use under this License.
5.2 Effect of Change
#### Humanitarian Border Assistance Exception
On and after the Change Date:
Use of the Licensed Work is permitted for humanitarian border assistance or life-safety operations, provided that:
- All restrictions in Section 4 cease to apply to this version
- You receive all rights granted by the Change License
- No action by Maple Open Tech is required for this change to take effect
- This change is irrevocable
a. The primary purpose is to prevent loss of life or provide emergency assistance, medical aid, shelter, search and rescue, or safe transport;
b. The Licensed Work is not used to identify, track, profile, detain, deport, or otherwise enforce immigration status;
c. Any data collected is limited to what is necessary for immediate humanitarian assistance and is not retained, repurposed, or shared for immigration enforcement; and
d. The use complies with applicable law and oversight in the jurisdiction in which it occurs.
5.3 Version-Specific
Humanitarian assistance shall not be deemed valid if it is incidental to, or a pretext for, immigration enforcement activities.
This time-release applies only to the specific version identified in the
License Parameters. Newer versions of the Licensed Work may have their own
Change Dates.
5.4 Earlier Release
### 4A.5 Military, Intelligence, or Paramilitary Operations
Use by or on behalf of armed forces, intelligence agencies, private military contractors, or similar organizations.
Maple Open Tech may, at its sole discretion, make the Change License effective
earlier than the Change Date by publishing notice at
https://mapleopentech.ca/license.
#### Humanitarian and Disaster Relief Exception
--------------------------------------------------------------------------------
SECTION 6. COMMERCIAL LICENSING
--------------------------------------------------------------------------------
Use by military or governmental entities is permitted **solely** for humanitarian assistance or disaster relief, provided that the Licensed Work is not used for combat, surveillance, targeting, law enforcement, population control, or security enforcement.
6.1 When Required
### 4A.6 Human Rights Violations
Activities that materially contribute to persecution, discrimination, segregation, or deprivation of civil or human rights as recognized under international law.
You must obtain a Commercial License if You:
### 4A.7 Biological or Environmental Harm
Systems intended to cause large-scale harm to living organisms or ecosystems.
- Use the Licensed Work in Production Use (and do not qualify under
Section 3)
- Offer the Licensed Work as a Service Offering
- Wish to redistribute the Licensed Work before the Change Date
#### Ethical Resource Extraction and Environmental Management Exception
6.2 Obtaining a Commercial License
Use is permitted for **lawful, regulated, and necessary resource extraction or environmental management**, provided that:
Commercial Licenses are available at: https://mapleopentech.ca/license
- The activity relates to energy, minerals, forestry, water, agriculture, infrastructure, or remediation;
- It complies with applicable environmental laws or governance frameworks, including those exercised by **First Nations** pursuant to treaty rights, self-government agreements, or First Nations law;
- Environmental harm is not the primary purpose and is not unnecessary or clearly disproportionate;
- The Licensed Work is not used for deliberate ecological destruction or irreversible damage beyond what is inherent to the lawful activity.
Contact: licensing@mapleopentech.ca
### Agriculture and Food Systems Exception
6.3 Commercial License Benefits
Use is permitted for lawful agriculture, aquaculture, livestock, veterinary, or food-production purposes, including lawful cultivation, processing, and distribution of cannabis, provided that:
Commercial Licenses may include:
- The use is regulated and legitimate;
- The Licensed Work is not used to cause unnecessary suffering or mass extermination;
- Companion Animals and non-agricultural wildlife are not targeted.
- Legal right to Production Use and Service Offerings
- Priority support and SLA options
- Input on feature roadmap
- Redistribution rights (negotiable)
- Custom terms for enterprise deployments
- Private cloud deployment rights
### 4A.8 Lawful Civilian Law Enforcement (Permitted Use)
6.4 Pricing Transparency
Use by civilian law-enforcement agencies is permitted only where it supports lawful, individualized investigations, is subject to due process, and does not involve population control, mass surveillance, or prohibited purposes.
Current pricing is published at: https://mapleopentech.ca/pricing
### 4A.9 Automated Targeting and Autonomous Harm
Maple Open Tech commits to reasonable and fair commercial terms.
Use of the Licensed Work to develop or operate systems that autonomously or semi-autonomously identify, select, track, or target people for physical harm, detention, coercion, or lethal action is prohibited, regardless of human involvement.
--------------------------------------------------------------------------------
SECTION 7. CONTRIBUTOR TERMS
--------------------------------------------------------------------------------
---
If You submit code, documentation, or other contributions to the Licensed Work:
## 5. Time-Release Provision
(a) License Grant - You grant Maple Open Tech a perpetual, worldwide,
royalty-free, non-exclusive license to use, modify, sublicense, and
distribute Your contribution under this License, the Change License,
and any Commercial Licenses.
On the Change Date, all commercial and revenue-centered restrictions of this License are permanently removed for the specified version of the Licensed Work.
(b) Representation - You represent that You have the legal right to grant
such license and that Your contribution does not infringe third-party
rights.
After the Change Date:
- Production Use and Service Offerings are permitted without a Commercial License; and
- All other terms of this License, including attribution, disclaimers, patent provisions, and **Section 4A ethical restrictions**, remain in full force and effect.
(c) No Obligation - Maple Open Tech is under no obligation to accept, use,
or acknowledge Your contribution.
This time-release applies only to the identified version. New versions may have different Change Dates or licensing terms.
(d) Attribution - Accepted contributions will be acknowledged in accordance
with project practices.
---
--------------------------------------------------------------------------------
SECTION 8. PATENT GRANT
--------------------------------------------------------------------------------
## 6. Commercial Licensing
8.1 Grant
Prior to the Change Date, Production Use, Service Offerings, or redistribution require a Commercial License issued by Maple Open Tech.
Subject to the terms of this License, each contributor to the Licensed Work
grants You a perpetual, worldwide, non-exclusive, royalty-free patent license
to make, have made, use, sell, offer for sale, import, and otherwise transfer
the Licensed Work, to the extent such license is necessary to exercise the
rights granted under this License.
Commercial Licenses may not authorize any use prohibited under Section 4A.
8.2 Defensive Termination
---
If You (or any entity on Your behalf) initiate patent litigation against any
entity alleging that the Licensed Work constitutes patent infringement:
## 7. Contributions
- All patent licenses granted to You under this License terminate
immediately
- All other licenses granted under this License also terminate
- This termination is automatic and requires no action by Maple Open Tech
By submitting a contribution, You grant the Licensor a perpetual, worldwide, royalty-free license to use, modify, sublicense, and distribute the contribution under this License.
--------------------------------------------------------------------------------
SECTION 9. ACCEPTANCE AND ACKNOWLEDGMENT
--------------------------------------------------------------------------------
You represent that You have the legal right to grant this license.
BY DOWNLOADING, ACCESSING, COPYING, OR OTHERWISE USING THE LICENSED WORK, YOU:
---
- Acknowledge that You have read and understood this License
- Agree to be bound by all terms and conditions
- Represent that You have the legal capacity to enter this agreement
- Affirm that if accepting on behalf of an organization, You have
authority to bind that organization
- Confirm that You will use the Licensed Work only as permitted by
this License
## 8. Patent Grant
IF YOU DO NOT AGREE TO THESE TERMS, DO NOT DOWNLOAD, ACCESS, OR USE THE
LICENSED WORK.
Each contributor grants You a non-exclusive, worldwide, royalty-free patent license to use the Licensed Work as permitted under this License.
--------------------------------------------------------------------------------
SECTION 10. NO WARRANTY
--------------------------------------------------------------------------------
If You initiate patent litigation alleging infringement by the Licensed Work, all rights granted under this License terminate automatically.
10.1 AS-IS PROVISION
---
THE LICENSED WORK IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY
OF ANY KIND, EXPRESS, IMPLIED, OR STATUTORY.
## 9. Acceptance
10.2 DISCLAIMER OF WARRANTIES
By using the Licensed Work, You agree to be bound by this License.
If You do not agree, do not use the Licensed Work.
MAPLE OPEN TECH EXPRESSLY DISCLAIMS ALL WARRANTIES, INCLUDING BUT NOT
LIMITED TO:
---
- MERCHANTABILITY
- FITNESS FOR A PARTICULAR PURPOSE
- NON-INFRINGEMENT
- ACCURACY OR COMPLETENESS
- QUIET ENJOYMENT
- COMPATIBILITY WITH ANY SYSTEM OR SOFTWARE
- THAT THE LICENSED WORK WILL MEET YOUR REQUIREMENTS
- THAT OPERATION WILL BE UNINTERRUPTED OR ERROR-FREE
- THAT DEFECTS WILL BE CORRECTED
## 10. No Warranty
10.3 RISK ASSUMPTION
THE LICENSED WORK IS PROVIDED **“AS IS”**, WITHOUT WARRANTY OF ANY KIND.
YOU ACKNOWLEDGE THAT:
---
- You use the Licensed Work entirely at Your own risk
- You are solely responsible for determining suitability for Your
intended use
- You are responsible for all data backup and recovery
- No advice or information obtained from Maple Open Tech creates any
warranty
## 11. Limitation of Liability
--------------------------------------------------------------------------------
SECTION 11. LIMITATION OF LIABILITY
--------------------------------------------------------------------------------
To the maximum extent permitted by law, the Licensors total aggregate liability shall not exceed **CAD $100**.
11.1 EXCLUSION OF DAMAGES
---
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL MAPLE
OPEN TECH OR ITS DIRECTORS, OFFICERS, EMPLOYEES, AGENTS, CONTRIBUTORS, OR
LICENSORS BE LIABLE FOR ANY:
## 12. Governing Law and Jurisdiction
- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE
DAMAGES
- LOSS OF PROFITS, REVENUE, DATA, GOODWILL, OR BUSINESS OPPORTUNITY
- COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES
- BUSINESS INTERRUPTION OR LOSS OF USE
- DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE LICENSED WORK
- DAMAGES ARISING FROM ANY CONTENT OBTAINED THROUGH THE LICENSED WORK
- ANY OTHER PECUNIARY LOSS
This License is governed by the laws of the Province of Ontario and the federal laws of Canada.
WHETHER BASED ON WARRANTY, CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT
LIABILITY, OR ANY OTHER LEGAL THEORY, AND WHETHER OR NOT MAPLE OPEN TECH
HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
---
11.2 AGGREGATE LIABILITY CAP
## 13. No Consumer Offering
NOTWITHSTANDING THE FOREGOING, MAPLE OPEN TECH'S TOTAL AGGREGATE LIABILITY
FOR ALL CLAIMS ARISING OUT OF OR RELATING TO THIS LICENSE OR THE LICENSED
WORK SHALL NOT EXCEED ONE HUNDRED CANADIAN DOLLARS (CAD $100.00).
The Licensed Work is intended for professional, developer, academic, and organizational use only.
This cap applies:
---
- Regardless of the number of claims
- Regardless of the form of action
- To all versions of the Licensed Work collectively
- To all incidents and causes of action in aggregate
## 14. Termination
11.3 ESSENTIAL BASIS
This License terminates automatically upon breach.
**Termination for violation of Section 4A applies both before and after the Change Date.**
YOU ACKNOWLEDGE THAT:
---
- The limitations in this Section 11 are fundamental elements of the
bargain between You and Maple Open Tech
- Maple Open Tech would not provide the Licensed Work without these
limitations
- These limitations shall apply even if any limited remedy fails of its
essential purpose
## 15. General Provisions
11.4 JURISDICTIONAL VARIATIONS
If any provision is held unenforceable, the remainder shall remain in effect.
Some jurisdictions do not allow the exclusion of certain warranties or the
limitation of certain damages. In such jurisdictions, the exclusions and
limitations herein shall apply to the maximum extent permitted by applicable
law.
This License constitutes the entire agreement regarding the Licensed Work.
--------------------------------------------------------------------------------
SECTION 12. DISPUTE RESOLUTION AND ARBITRATION
--------------------------------------------------------------------------------
---
12.1 MANDATORY ARBITRATION
## Contact
Any dispute, controversy, or claim arising out of or relating to this License
or the Licensed Work, including the formation, interpretation, breach, or
termination thereof, shall be resolved by binding arbitration rather than
in court.
- **Licensing:** licensing@mapleopentech.ca
- **Website:** https://mapleopentech.ca/license
12.2 ARBITRATION RULES
---
Arbitration shall be conducted:
*Maple Open Tech License (MOTL) v1.0*
- Under the ADR Institute of Canada National Arbitration Rules
- By a single arbitrator mutually agreed upon, or appointed under the Rules
- In Toronto, Ontario, Canada (or virtually, at either party's request)
- In the English language
- Under the laws of Ontario, Canada
12.3 CLASS ACTION WAIVER
YOU AGREE THAT ANY ARBITRATION OR PROCEEDING SHALL BE CONDUCTED ONLY ON AN
INDIVIDUAL BASIS AND NOT AS A CLASS, CONSOLIDATED, OR REPRESENTATIVE ACTION.
You waive any right to:
- Participate in a class action lawsuit or class-wide arbitration
- Participate in any consolidated or representative proceeding
- Have any dispute decided by a judge or jury
12.4 EXCEPTIONS TO ARBITRATION
Either party may seek:
- Injunctive or other equitable relief in any court of competent
jurisdiction to protect intellectual property rights
- Relief in small claims court for disputes within that court's
jurisdiction
12.5 EU CONSUMER EXEMPTION
For EU consumers, see the EU Compliance Addendum which exempts consumers
from mandatory arbitration.
--------------------------------------------------------------------------------
SECTION 13. GOVERNING LAW AND JURISDICTION
--------------------------------------------------------------------------------
13.1 GOVERNING LAW
This License is governed by the laws of the Province of Ontario, Canada and
the federal laws of Canada, without regard to conflict of law principles.
13.2 EXCLUSIVE JURISDICTION
Subject to arbitration (Section 12), legal actions shall be brought
exclusively in courts located in Toronto, Ontario, Canada. You consent to
personal jurisdiction and venue in such courts.
13.3 INTERNATIONAL USERS
If You are outside Canada:
- This License is governed by Ontario/Canadian law
- You submit to Ontario jurisdiction (subject to arbitration)
- If provisions are unenforceable in Your jurisdiction, they shall be
modified to the minimum extent necessary
--------------------------------------------------------------------------------
SECTION 14. TERMINATION
--------------------------------------------------------------------------------
14.1 Automatic Termination
Your rights under this License terminate automatically and immediately if
You breach any term, without notice from Maple Open Tech.
14.2 Effect of Termination
Upon termination, You must:
- Immediately cease all use of the Licensed Work
- Delete all copies in Your possession or control
- Certify deletion in writing if requested
14.3 Survival
Sections 8, 10, 11, 12, 13, 15, and 17 survive termination.
14.4 No Effect on Change License
Termination of this License does not affect any rights You have acquired
under the Change License after the Change Date.
--------------------------------------------------------------------------------
SECTION 15. DATA PROTECTION
--------------------------------------------------------------------------------
15.1 Your Responsibility
If the Licensed Work processes personal data, You are responsible for
compliance with applicable data protection laws.
15.2 GDPR
For users subject to GDPR:
- A Data Processing Agreement is available at: dpa@mapleopentech.ca
- Privacy Policy: https://mapleopentech.ca/privacy
- Sub-processor list: https://mapleopentech.ca/subprocessors
15.3 Data Sovereignty
Where the Licensed Work is offered as a hosted service by Maple Open Tech,
data residency options (including Canadian-only storage) are available.
Contact: licensing@mapleopentech.ca
--------------------------------------------------------------------------------
SECTION 16. EXPORT COMPLIANCE
--------------------------------------------------------------------------------
You are responsible for compliance with applicable export control laws. The
Licensed Work may be subject to Canadian and other export restrictions.
--------------------------------------------------------------------------------
SECTION 17. GENERAL PROVISIONS
--------------------------------------------------------------------------------
17.1 SEVERABILITY
If any provision is held invalid or unenforceable:
- It shall be modified to the minimum extent necessary
- If modification is not possible, it shall be severed
- Remaining provisions continue in full force
17.2 ENTIRE AGREEMENT
This License constitutes the entire agreement regarding the Licensed Work
and supersedes all prior agreements.
17.3 WAIVER
No waiver is effective unless in writing. No failure to exercise any right
operates as a waiver.
17.4 ASSIGNMENT
You may not assign this License without Maple Open Tech's written consent.
Maple Open Tech may assign without restriction.
17.5 NOTICES
Notices shall be sent to licensing@mapleopentech.ca and are deemed received
when actually received.
17.6 INTERPRETATION
- Headings are for convenience only
- "Including" means "including without limitation"
- "Days" means calendar days
- Currency is Canadian dollars unless specified
17.7 RELATIONSHIP
This License does not create any partnership, joint venture, employment, or
agency relationship.
--------------------------------------------------------------------------------
SECTION 18. CONTACT
--------------------------------------------------------------------------------
Commercial Licensing: licensing@mapleopentech.ca
https://mapleopentech.ca/license
General Inquiries: hello@mapleopentech.ca
Security Issues: security@mapleopentech.ca
EU Compliance: eu-compliance@mapleopentech.ca
GDPR / Data Protection: dpa@mapleopentech.ca
AI Compliance: ai-compliance@mapleopentech.ca
================================================================================
HOW TO APPLY THIS LICENSE
1. Include this file as LICENSE.txt in your project root
2. Fill in the License Parameters at the top (Software name, version, dates)
3. Add the following header to source files:
Copyright [YEAR] Maple Open Tech
Licensed under the Maple Open Tech License (MOTL) v1.0
https://mapleopentech.ca/license
This software is source-available. Production use requires a commercial
license. After [CHANGE DATE], this version becomes MIT-licensed.
================================================================================
FREQUENTLY ASKED QUESTIONS
Q: Is this open source?
A: Not yet. This is "source-available" until the Change Date. After that,
each version becomes MIT-licensed, which is OSI-approved open source.
Q: Can I view and audit the source code?
A: Yes, absolutely. Transparency and security auditing are explicitly
permitted.
Q: Can I use this for development and testing?
A: Yes. Non-Production Use (development, testing, evaluation, research)
is free.
Q: When do I need a Commercial License?
A: When you use it in production, offer it as a service, or want to
redistribute before the Change Date.
Q: What happens after the Change Date?
A: That specific version becomes MIT-licensed. You can do anything MIT
permits. Newer versions may still be under MOTL with their own Change
Dates.
Q: Can AWS/cloud providers take this and compete with you?
A: Not before the Change Date. Competing services require a Commercial
License.
Q: Can I self-host for my company?
A: Production self-hosting requires a Commercial License. We offer private
cloud licensing.
Q: I'm in the EU. Does the arbitration apply to me?
A: If you're an EU consumer, no. See the EU Compliance Addendum.
Q: Can I contribute improvements?
A: Yes! Contributions are welcome. By contributing, you grant us license
to use your contribution.
Q: What if my use case isn't clear?
A: Contact licensing@mapleopentech.ca - we're happy to clarify.
================================================================================
Maple Open Tech License (MOTL) v1.0
Last Updated: November 2025
https://mapleopentech.ca/license

View file

@ -0,0 +1,117 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice

View file

@ -0,0 +1,3 @@
# maple-calc
WordPress plugin to create gutenberg friendly calculators.

View file

@ -0,0 +1,211 @@
/* assets/css/frontend.css */
.maple-calc {
margin: 0 auto 20px;
padding: 20px;
background: white;
border-radius: 8px;
font-family: Arial, sans-serif;
color: #222222;
border: 1px solid #e0e0e0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
animation: fadeIn 0.5s ease-in-out;
max-width: 100%;
box-sizing: border-box;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.maple-calc h2 {
color: #222222;
text-align: center;
font-size: 24px;
}
.maple-calc h3 {
color: #222222;
font-size: 24px;
}
.maple-calc label {
color: #222222;
}
.maple-calc p {
color: #222222;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #222222;
font-size: 24px;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 8px;
transition: all 0.3s ease;
color: #222222;
font-size: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
.form-group input:focus {
border-color: #02066f;
box-shadow: 0 0 8px rgba(2, 6, 111, 0.3);
}
.calculate-button {
background: #02066f;
color: white;
border: none;
padding: 12px 15px;
border-radius: 8px;
cursor: pointer;
font-size: 24px;
width: 100%;
transition: all 0.3s ease;
}
.calculate-button:hover {
background: #000040;
transform: translateY(-2px);
}
.results {
margin-top: 20px;
padding: 15px;
background: white;
border-radius: 8px;
border: 1px solid #ddd;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
color: #222222;
}
.results p {
margin: 10px 0;
color: #222222;
font-size: 24px;
}
/* Chart container styling */
.chart-container {
margin: 10px auto;
width: 100%;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
/* Target Chart.js canvas and legend */
.chart-container canvas {
max-width: 100% !important;
margin: 0 auto !important;
}
/* Override Chart.js legend styles */
div.chart-container + ul {
display: flex !important;
flex-wrap: wrap !important;
justify-content: center !important;
margin: 10px auto 0 !important;
padding: 0 !important;
list-style: none !important;
width: 100% !important;
}
div.chart-container + ul li {
display: flex !important;
align-items: center !important;
margin: 5px 10px !important;
font-size: 18px !important;
color: #222222 !important;
}
/* Gutenberg Alignment Classes */
.alignleft {
float: left;
max-width: 50%;
margin: 0 2em 1em 0;
}
.alignright {
float: right;
max-width: 50%;
margin: 0 0 1em 2em;
}
.aligncenter {
margin-left: auto;
margin-right: auto;
}
.alignwide {
max-width: none;
width: auto;
}
.alignfull {
max-width: none;
width: 100vw;
margin-left: calc(-50vw + 50%);
margin-right: calc(-50vw + 50%);
}
/* Side-by-side layout for desktop */
@media (min-width: 768px) {
.maple-calc {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.maple-calc > h2 {
grid-column: 1 / -1;
}
.maple-calc form {
grid-column: 1;
}
.maple-calc .results {
grid-column: 2;
}
.wp-block[class*="align"] {
max-width: 100%;
}
}
/* Clear floats for alignment classes */
.maple-calc:after {
content: "";
display: table;
clear: both;
}
/* Ensure blocks respect alignment classes */
.wp-block-maple-mortgage-calculator,
.wp-block-maple-datacenter-calculator,
.wp-block-maple-roi-calculator {
max-width: 100%;
width: auto;
}

View file

@ -0,0 +1,12 @@
<?php
/**
* Silence is golden.
*
* Prevent direct access to this file.
*
* @package WordPress
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

View file

@ -0,0 +1,12 @@
<?php
/**
* Silence is golden.
*
* Prevent direct access to this file.
*
* @package WordPress
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

View file

@ -0,0 +1,108 @@
(function (blocks, element, blockEditor, components) {
var el = element.createElement;
var useBlockProps = blockEditor.useBlockProps;
var BlockControls = blockEditor.BlockControls;
var AlignmentToolbar = blockEditor.AlignmentToolbar;
var TextControl = components.TextControl;
blocks.registerBlockType("maple/datacenter-calculator", {
title: "Data Center Costs Calculator",
icon: "calculator",
category: "common",
attributes: {
align: {
type: "string",
},
},
supports: {
align: true,
},
edit: function (props) {
const blockProps = useBlockProps({
className: props.attributes.align
? "align" + props.attributes.align
: "",
});
return [
el(
BlockControls,
{ key: "controls" },
el(AlignmentToolbar, {
value: props.attributes.align,
onChange: (newAlign) => {
props.setAttributes({ align: newAlign });
},
}),
),
el(
"div",
blockProps,
el("h2", {}, "Data Center Costs Calculator"),
el(
"form",
{ className: "maple-calc-form" },
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Power Usage (kW)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Cost per kWh ($)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Rack Space (units)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"button",
{ type: "button", className: "calculate-button" },
"Calculate",
),
),
el(
"div",
{ className: "results" },
el("h3", {}, "Results"),
el("p", {}, el("strong", {}, "Power Cost (Monthly): "), "$---"),
el("p", {}, el("strong", {}, "Cooling Cost (Monthly): "), "$---"),
el("p", {}, el("strong", {}, "Rack Cost (Monthly): "), "$---"),
el("p", {}, el("strong", {}, "Total Monthly Cost: "), "$---"),
el("p", {}, el("strong", {}, "Total Annual Cost: "), "$---"),
el(
"div",
{ className: "chart-container" },
el("canvas", { className: "datacenter-chart" }),
),
),
),
];
},
save: function () {
return null;
},
});
})(
window.wp.blocks,
window.wp.element,
window.wp.blockEditor,
window.wp.components,
);

View file

@ -0,0 +1,353 @@
document.addEventListener("DOMContentLoaded", function () {
console.log("Frontend.js loaded");
// Initialize charts on page load
initializeCharts();
// Handle form submissions
handleFormSubmissions();
// Add animations for results
addResultAnimations();
});
function initializeCharts() {
console.log("Initializing charts...");
// Mortgage Charts
const mortgageCharts = document.querySelectorAll(".mortgage-chart");
mortgageCharts.forEach((canvas) => {
console.log("Rendering mortgage chart for:", canvas);
const principal = parseFloat(canvas.dataset.principal) || 200000;
const totalInterest = parseFloat(canvas.dataset.interest) || 123312.18;
renderMortgageChart(canvas, principal, totalInterest);
});
// Data Center Charts
const datacenterCharts = document.querySelectorAll(".datacenter-chart");
datacenterCharts.forEach((canvas) => {
console.log("Rendering datacenter chart for:", canvas);
const powerCost = parseFloat(canvas.dataset.power) || 730;
const coolingCost = parseFloat(canvas.dataset.cooling) || 876;
const rackCost = parseFloat(canvas.dataset.rack) || 500;
renderDatacenterChart(canvas, powerCost, coolingCost, rackCost);
});
// ROI Charts
const roiCharts = document.querySelectorAll(".roi-chart");
roiCharts.forEach((canvas) => {
console.log("Rendering ROI chart for:", canvas);
const initialInvestment = parseFloat(canvas.dataset.investment) || 10000;
const netProfit = parseFloat(canvas.dataset.profit) || 4025.52;
renderRoiChart(canvas, initialInvestment, netProfit);
});
}
function handleFormSubmissions() {
const calculatorForms = document.querySelectorAll(".maple-calc form");
calculatorForms.forEach((form) => {
form.addEventListener("submit", function (e) {
e.preventDefault();
console.log("Form submitted");
// Validate form inputs
const inputs = form.querySelectorAll("input[required]");
let isValid = true;
inputs.forEach((input) => {
if (!input.value) {
isValid = false;
input.style.borderColor = "#ff0000";
} else {
input.style.borderColor = "#ccc";
}
});
if (!isValid) {
alert("Please fill in all required fields.");
return;
}
// Process form data
const formData = new FormData(form);
const formEntries = Object.fromEntries(formData.entries());
// Determine calculator type
const calculatorType = form
.closest(".maple-calc")
.classList.contains("mortgage-calculator")
? "mortgage"
: form
.closest(".maple-calc")
.classList.contains("datacenter-calculator")
? "datacenter"
: "roi";
const resultsDiv = form.nextElementSibling;
if (calculatorType === "mortgage") {
const principal = parseFloat(formEntries.principal) || 200000;
const interestRate = parseFloat(formEntries.interest_rate) || 3.5;
const termYears = parseInt(formEntries.term_years) || 30;
const monthlyPayment = calculateMonthlyPayment(
principal,
interestRate,
termYears,
);
const totalPayment = monthlyPayment * termYears * 12;
const totalInterest = totalPayment - principal;
updateMortgageResults(
resultsDiv,
monthlyPayment,
totalPayment,
totalInterest,
);
renderMortgageChart(
resultsDiv.querySelector(".mortgage-chart"),
principal,
totalInterest,
);
} else if (calculatorType === "datacenter") {
const powerKw = parseFloat(formEntries.power_kw) || 10;
const costPerKwh = parseFloat(formEntries.cost_per_kwh) || 0.1;
const rackSpace = parseInt(formEntries.rack_space) || 5;
const coolingFactor = 1.2;
const powerCostPerMonth = powerKw * costPerKwh * 24 * 30;
const coolingCostPerMonth = powerCostPerMonth * coolingFactor;
const rackCostPerMonth = rackSpace * 100;
const totalMonthlyCost =
powerCostPerMonth + coolingCostPerMonth + rackCostPerMonth;
const totalAnnualCost = totalMonthlyCost * 12;
updateDatacenterResults(
resultsDiv,
powerCostPerMonth,
coolingCostPerMonth,
rackCostPerMonth,
totalMonthlyCost,
totalAnnualCost,
);
renderDatacenterChart(
resultsDiv.querySelector(".datacenter-chart"),
powerCostPerMonth,
coolingCostPerMonth,
rackCostPerMonth,
);
} else if (calculatorType === "roi") {
const initialInvestment =
parseFloat(formEntries.initial_investment) || 10000;
const annualReturnRate =
parseFloat(formEntries.annual_return_rate) || 7;
const timeYears = parseInt(formEntries.time_years) || 5;
const finalValue =
initialInvestment * Math.pow(1 + annualReturnRate / 100, timeYears);
const netProfit = finalValue - initialInvestment;
const roiPercentage = (netProfit / initialInvestment) * 100;
updateRoiResults(resultsDiv, finalValue, netProfit, roiPercentage);
renderRoiChart(
resultsDiv.querySelector(".roi-chart"),
initialInvestment,
netProfit,
);
}
// Show results with animation
resultsDiv.style.opacity = "0";
setTimeout(() => {
resultsDiv.style.opacity = "1";
}, 100);
});
});
}
function calculateMonthlyPayment(principal, interestRate, termYears) {
const monthlyInterestRate = interestRate / 100 / 12;
const termMonths = termYears * 12;
return (
(principal *
(monthlyInterestRate * Math.pow(1 + monthlyInterestRate, termMonths))) /
(Math.pow(1 + monthlyInterestRate, termMonths) - 1)
);
}
function updateMortgageResults(
resultsDiv,
monthlyPayment,
totalPayment,
totalInterest,
) {
resultsDiv.querySelector("p:nth-child(2)").innerHTML =
`<strong>Monthly Payment:</strong> $${monthlyPayment.toFixed(2)}`;
resultsDiv.querySelector("p:nth-child(3)").innerHTML =
`<strong>Total Payment:</strong> $${totalPayment.toFixed(2)}`;
resultsDiv.querySelector("p:nth-child(4)").innerHTML =
`<strong>Total Interest:</strong> $${totalInterest.toFixed(2)}`;
}
function updateDatacenterResults(
resultsDiv,
powerCost,
coolingCost,
rackCost,
totalMonthlyCost,
totalAnnualCost,
) {
resultsDiv.querySelector("p:nth-child(2)").innerHTML =
`<strong>Power Cost (Monthly):</strong> $${powerCost.toFixed(2)}`;
resultsDiv.querySelector("p:nth-child(3)").innerHTML =
`<strong>Cooling Cost (Monthly):</strong> $${coolingCost.toFixed(2)}`;
resultsDiv.querySelector("p:nth-child(4)").innerHTML =
`<strong>Rack Cost (Monthly):</strong> $${rackCost.toFixed(2)}`;
resultsDiv.querySelector("p:nth-child(5)").innerHTML =
`<strong>Total Monthly Cost:</strong> $${totalMonthlyCost.toFixed(2)}`;
resultsDiv.querySelector("p:nth-child(6)").innerHTML =
`<strong>Total Annual Cost:</strong> $${totalAnnualCost.toFixed(2)}`;
}
function updateRoiResults(resultsDiv, finalValue, netProfit, roiPercentage) {
resultsDiv.querySelector("p:nth-child(2)").innerHTML =
`<strong>Final Value:</strong> $${finalValue.toFixed(2)}`;
resultsDiv.querySelector("p:nth-child(3)").innerHTML =
`<strong>Net Profit:</strong> $${netProfit.toFixed(2)}`;
resultsDiv.querySelector("p:nth-child(4)").innerHTML =
`<strong>ROI (%):</strong> ${roiPercentage.toFixed(2)}%`;
}
function renderMortgageChart(canvas, principal, totalInterest) {
if (!canvas) {
console.error("Mortgage chart canvas not found");
return;
}
const ctx = canvas.getContext("2d");
new Chart(ctx, {
type: "pie",
data: {
labels: ["Principal", "Total Interest"],
datasets: [
{
data: [principal, totalInterest],
backgroundColor: ["#02066F", "#ff6384"],
borderWidth: 1,
},
],
},
options: {
responsive: true,
plugins: {
legend: {
position: "bottom",
labels: {
font: {
size: 14,
family: "Arial, sans-serif",
},
color: "#222222",
boxWidth: 20,
padding: 20,
usePointStyle: true,
},
},
title: {
display: false,
},
},
},
});
}
function renderDatacenterChart(canvas, powerCost, coolingCost, rackCost) {
if (!canvas) {
console.error("Datacenter chart canvas not found");
return;
}
const ctx = canvas.getContext("2d");
new Chart(ctx, {
type: "pie",
data: {
labels: ["Power Cost", "Cooling Cost", "Rack Cost"],
datasets: [
{
data: [powerCost, coolingCost, rackCost],
backgroundColor: ["#02066F", "#ff6384", "#36a2eb"],
borderWidth: 1,
},
],
},
options: {
responsive: true,
plugins: {
legend: {
position: "bottom",
labels: {
font: {
size: 14,
family: "Arial, sans-serif",
},
color: "#222222",
boxWidth: 20,
padding: 20,
usePointStyle: true,
},
},
title: {
display: false,
},
},
},
});
}
function renderRoiChart(canvas, initialInvestment, netProfit) {
if (!canvas) {
console.error("ROI chart canvas not found");
return;
}
const ctx = canvas.getContext("2d");
new Chart(ctx, {
type: "pie",
data: {
labels: ["Initial Investment", "Net Profit"],
datasets: [
{
data: [initialInvestment, netProfit],
backgroundColor: ["#02066F", "#36a2eb"],
borderWidth: 1,
},
],
},
options: {
responsive: true,
plugins: {
legend: {
position: "bottom",
labels: {
font: {
size: 14,
family: "Arial, sans-serif",
},
color: "#222222",
boxWidth: 20,
padding: 20,
usePointStyle: true,
},
},
title: {
display: false,
},
},
},
});
}
function addResultAnimations() {
const resultsDivs = document.querySelectorAll(".results");
resultsDivs.forEach((div) => {
div.style.transition = "opacity 0.5s ease-in-out";
});
}

View file

@ -0,0 +1,12 @@
<?php
/**
* Silence is golden.
*
* Prevent direct access to this file.
*
* @package WordPress
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

View file

@ -0,0 +1,106 @@
(function (blocks, element, blockEditor, components) {
var el = element.createElement;
var useBlockProps = blockEditor.useBlockProps;
var BlockControls = blockEditor.BlockControls;
var AlignmentToolbar = blockEditor.AlignmentToolbar;
var TextControl = components.TextControl;
blocks.registerBlockType("maple/mortgage-calculator", {
title: "Mortgage Calculator",
icon: "calculator",
category: "common",
attributes: {
align: {
type: "string",
},
},
supports: {
align: true,
},
edit: function (props) {
const blockProps = useBlockProps({
className: props.attributes.align
? "align" + props.attributes.align
: "",
});
return [
el(
BlockControls,
{ key: "controls" },
el(AlignmentToolbar, {
value: props.attributes.align,
onChange: (newAlign) => {
props.setAttributes({ align: newAlign });
},
}),
),
el(
"div",
blockProps,
el("h2", {}, "Mortgage Calculator"),
el(
"form",
{ className: "maple-calc-form" },
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Loan Amount ($)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Interest Rate (%)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Loan Term (Years)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"button",
{ type: "button", className: "calculate-button" },
"Calculate",
),
),
el(
"div",
{ className: "results" },
el("h3", {}, "Results"),
el("p", {}, el("strong", {}, "Monthly Payment: "), "$---"),
el("p", {}, el("strong", {}, "Total Payment: "), "$---"),
el("p", {}, el("strong", {}, "Total Interest: "), "$---"),
el(
"div",
{ className: "chart-container" },
el("canvas", { className: "mortgage-chart" }),
),
),
),
];
},
save: function () {
return null;
},
});
})(
window.wp.blocks,
window.wp.element,
window.wp.blockEditor,
window.wp.components,
);

View file

@ -0,0 +1,106 @@
(function (blocks, element, blockEditor, components) {
var el = element.createElement;
var useBlockProps = blockEditor.useBlockProps;
var BlockControls = blockEditor.BlockControls;
var AlignmentToolbar = blockEditor.AlignmentToolbar;
var TextControl = components.TextControl;
blocks.registerBlockType("maple/roi-calculator", {
title: "ROI Calculator",
icon: "calculator",
category: "common",
attributes: {
align: {
type: "string",
},
},
supports: {
align: true,
},
edit: function (props) {
const blockProps = useBlockProps({
className: props.attributes.align
? "align" + props.attributes.align
: "",
});
return [
el(
BlockControls,
{ key: "controls" },
el(AlignmentToolbar, {
value: props.attributes.align,
onChange: (newAlign) => {
props.setAttributes({ align: newAlign });
},
}),
),
el(
"div",
blockProps,
el("h2", {}, "ROI Calculator"),
el(
"form",
{ className: "maple-calc-form" },
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Initial Investment ($)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Annual Return Rate (%)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"div",
{ className: "form-group" },
el(TextControl, {
label: "Time (Years)",
type: "number",
value: "",
onChange: () => {},
}),
),
el(
"button",
{ type: "button", className: "calculate-button" },
"Calculate",
),
),
el(
"div",
{ className: "results" },
el("h3", {}, "Results"),
el("p", {}, el("strong", {}, "Final Value: "), "$---"),
el("p", {}, el("strong", {}, "Net Profit: "), "$---"),
el("p", {}, el("strong", {}, "ROI (%): "), "---%"),
el(
"div",
{ className: "chart-container" },
el("canvas", { className: "roi-chart" }),
),
),
),
];
},
save: function () {
return null;
},
});
})(
window.wp.blocks,
window.wp.element,
window.wp.blockEditor,
window.wp.components,
);

View file

@ -0,0 +1,12 @@
<?php
/**
* Silence is golden.
*
* Prevent direct access to this file.
*
* @package WordPress
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

View file

@ -0,0 +1,230 @@
<?php
/**
* Plugin Name: Maple Calc
* Description: A WordPress plugin for inserting various calculators as Gutenberg blocks and shortcodes.
* Version: 1.0.1
* Author: SSP Media
* Author URI: https://sspmedia.ca/wordpress/
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Define plugin constants
define('MAPLE_CALC_VERSION', '1.0.1');
define('MAPLE_CALC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MAPLE_CALC_PLUGIN_URL', plugin_dir_url(__FILE__));
// Enqueue frontend scripts and styles
function maple_calc_frontend_enqueue_scripts() {
wp_enqueue_style('maple-calc-frontend', MAPLE_CALC_PLUGIN_URL . 'assets/css/frontend.css', [], MAPLE_CALC_VERSION);
wp_enqueue_script('maple-calc-frontend', MAPLE_CALC_PLUGIN_URL . 'assets/js/frontend.js', ['jquery', 'wp-element'], MAPLE_CALC_VERSION, true);
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], '3.7.0', true);
}
add_action('wp_enqueue_scripts', 'maple_calc_frontend_enqueue_scripts');
// Enqueue block editor assets
function maple_calc_enqueue_block_editor_assets() {
// Mortgage Calculator Block
wp_enqueue_script(
'maple-calc-mortgage-block',
MAPLE_CALC_PLUGIN_URL . 'assets/js/mortgage-block.js',
['wp-blocks', 'wp-element', 'wp-components', 'wp-editor', 'wp-block-editor'],
MAPLE_CALC_VERSION,
true
);
// Data Center Calculator Block
wp_enqueue_script(
'maple-calc-datacenter-block',
MAPLE_CALC_PLUGIN_URL . 'assets/js/datacenter-block.js',
['wp-blocks', 'wp-element', 'wp-components', 'wp-editor', 'wp-block-editor'],
MAPLE_CALC_VERSION,
true
);
// ROI Calculator Block
wp_enqueue_script(
'maple-calc-roi-block',
MAPLE_CALC_PLUGIN_URL . 'assets/js/roi-block.js',
['wp-blocks', 'wp-element', 'wp-components', 'wp-editor', 'wp-block-editor'],
MAPLE_CALC_VERSION,
true
);
}
add_action('enqueue_block_editor_assets', 'maple_calc_enqueue_block_editor_assets');
// Register Gutenberg blocks
function maple_calc_register_blocks() {
// Register Mortgage Calculator Block
register_block_type('maple/mortgage-calculator', [
'editor_script' => 'maple-calc-mortgage-block',
'render_callback' => 'maple_calc_render_mortgage_block',
'attributes' => [
'align' => [
'type' => 'string',
],
],
'supports' => [
'align' => true,
],
]);
// Register Data Center Calculator Block
register_block_type('maple/datacenter-calculator', [
'editor_script' => 'maple-calc-datacenter-block',
'render_callback' => 'maple_calc_render_datacenter_block',
'attributes' => [
'align' => [
'type' => 'string',
],
],
'supports' => [
'align' => true,
],
]);
// Register ROI Calculator Block
register_block_type('maple/roi-calculator', [
'editor_script' => 'maple-calc-roi-block',
'render_callback' => 'maple_calc_render_roi_block',
'attributes' => [
'align' => [
'type' => 'string',
],
],
'supports' => [
'align' => true,
],
]);
}
add_action('init', 'maple_calc_register_blocks');
// Register shortcodes
function maple_calc_register_shortcodes() {
add_shortcode('maple_mortgage_calculator', 'maple_calc_mortgage_shortcode');
add_shortcode('maple_datacenter_calculator', 'maple_calc_datacenter_shortcode');
add_shortcode('maple_roi_calculator', 'maple_calc_roi_shortcode');
}
add_action('init', 'maple_calc_register_shortcodes');
// Add admin menu
function maple_calc_admin_menu() {
add_menu_page(
'Maple Calc',
'Maple Calc',
'manage_options',
'maple-calc',
'maple_calc_admin_page',
'dashicons-calculator',
6
);
}
add_action('admin_menu', 'maple_calc_admin_menu');
// Admin page content
function maple_calc_admin_page() {
?>
<div class="wrap">
<h1>Maple Calc</h1>
<p>Welcome to the Maple Calc admin page. Use the shortcodes below to insert calculators into your posts or pages:</p>
<ul>
<li><code>[maple_mortgage_calculator]</code></li>
<li><code>[maple_datacenter_calculator]</code></li>
<li><code>[maple_roi_calculator]</code></li>
</ul>
</div>
<?php
}
// Render callback for Mortgage Calculator Block
function maple_calc_render_mortgage_block($attributes) {
ob_start();
include MAPLE_CALC_PLUGIN_DIR . 'templates/mortgage-calculator.php';
return ob_get_clean();
}
// Render callback for Data Center Calculator Block
function maple_calc_render_datacenter_block($attributes) {
ob_start();
include MAPLE_CALC_PLUGIN_DIR . 'templates/datacenter-calculator.php';
return ob_get_clean();
}
// Render callback for ROI Calculator Block
function maple_calc_render_roi_block($attributes) {
ob_start();
include MAPLE_CALC_PLUGIN_DIR . 'templates/roi-calculator.php';
return ob_get_clean();
}
// Shortcode callback for Mortgage Calculator
function maple_calc_mortgage_shortcode($atts) {
ob_start();
include MAPLE_CALC_PLUGIN_DIR . 'templates/mortgage-calculator.php';
return ob_get_clean();
}
// Shortcode callback for Data Center Calculator
function maple_calc_datacenter_shortcode($atts) {
ob_start();
include MAPLE_CALC_PLUGIN_DIR . 'templates/datacenter-calculator.php';
return ob_get_clean();
}
// Shortcode callback for ROI Calculator
function maple_calc_roi_shortcode($atts) {
ob_start();
include MAPLE_CALC_PLUGIN_DIR . 'templates/roi-calculator.php';
return ob_get_clean();
}
// Mortgage Calculator Logic
function maple_calculate_mortgage($principal, $interest_rate, $term_years) {
$monthly_interest_rate = $interest_rate / 100 / 12;
$term_months = $term_years * 12;
$monthly_payment = $principal * ($monthly_interest_rate * pow(1 + $monthly_interest_rate, $term_months)) / (pow(1 + $monthly_interest_rate, $term_months) - 1);
$total_payment = $monthly_payment * $term_months;
$total_interest = $total_payment - $principal;
return [
'monthly_payment' => round($monthly_payment, 2),
'total_payment' => round($total_payment, 2),
'total_interest' => round($total_interest, 2),
];
}
// Data Center Costs Calculator Logic
function maple_calculate_datacenter($power_kw, $cost_per_kwh, $rack_space, $cooling_factor = 1.2) {
$power_cost_per_hour = $power_kw * $cost_per_kwh;
$power_cost_per_month = $power_cost_per_hour * 24 * 30;
$cooling_cost_per_month = $power_cost_per_month * $cooling_factor;
$rack_cost_per_month = $rack_space * 100;
$total_monthly_cost = $power_cost_per_month + $cooling_cost_per_month + $rack_cost_per_month;
$total_annual_cost = $total_monthly_cost * 12;
return [
'power_cost_per_month' => round($power_cost_per_month, 2),
'cooling_cost_per_month' => round($cooling_cost_per_month, 2),
'rack_cost_per_month' => round($rack_cost_per_month, 2),
'total_monthly_cost' => round($total_monthly_cost, 2),
'total_annual_cost' => round($total_annual_cost, 2),
];
}
// ROI Calculator Logic
function maple_calculate_roi($initial_investment, $annual_return_rate, $time_years) {
$final_value = $initial_investment * pow(1 + $annual_return_rate / 100, $time_years);
$net_profit = $final_value - $initial_investment;
$roi_percentage = ($net_profit / $initial_investment) * 100;
return [
'final_value' => round($final_value, 2),
'net_profit' => round($net_profit, 2),
'roi_percentage' => round($roi_percentage, 2),
];
}

View file

@ -0,0 +1,12 @@
<?php
// templates/admin-page.php
?>
<div class="wrap">
<h1>Maple Calc</h1>
<p>Welcome to the Maple Calc admin page. Use the shortcodes below to insert calculators into your posts or pages:</p>
<ul>
<li><code>[maple_mortgage_calculator]</code></li>
<li><code>[maple_datacenter_calculator]</code></li>
<li><code>[maple_roi_calculator]</code></li>
</ul>
</div>

View file

@ -0,0 +1,44 @@
<?php
// templates/datacenter-calculator.php
$power_kw = isset($_POST['power_kw']) ? floatval($_POST['power_kw']) : 10;
$cost_per_kwh = isset($_POST['cost_per_kwh']) ? floatval($_POST['cost_per_kwh']) : 0.1;
$rack_space = isset($_POST['rack_space']) ? intval($_POST['rack_space']) : 5;
$datacenter_results = maple_calculate_datacenter($power_kw, $cost_per_kwh, $rack_space);
?>
<div class="maple-calc datacenter-calculator <?php echo isset($attributes['align']) ? 'align' . esc_attr($attributes['align']) : 'alignnone'; ?>">
<h2>Data Center Costs Calculator</h2>
<form method="post">
<div class="form-group">
<label for="power_kw">Power Usage (kW):</label>
<input type="number" step="0.01" id="power_kw" name="power_kw" value="<?php echo isset($_POST['power_kw']) ? esc_attr($_POST['power_kw']) : '10'; ?>" required>
</div>
<div class="form-group">
<label for="cost_per_kwh">Cost per kWh ($):</label>
<input type="number" step="0.01" id="cost_per_kwh" name="cost_per_kwh" value="<?php echo isset($_POST['cost_per_kwh']) ? esc_attr($_POST['cost_per_kwh']) : '0.1'; ?>" required>
</div>
<div class="form-group">
<label for="rack_space">Rack Space (units):</label>
<input type="number" id="rack_space" name="rack_space" value="<?php echo isset($_POST['rack_space']) ? esc_attr($_POST['rack_space']) : '5'; ?>" required>
</div>
<button type="submit" class="calculate-button">Calculate</button>
</form>
<?php if (isset($datacenter_results)): ?>
<div class="results">
<h3>Results</h3>
<p><strong>Power Cost (Monthly):</strong> $<?php echo esc_html($datacenter_results['power_cost_per_month']); ?></p>
<p><strong>Cooling Cost (Monthly):</strong> $<?php echo esc_html($datacenter_results['cooling_cost_per_month']); ?></p>
<p><strong>Rack Cost (Monthly):</strong> $<?php echo esc_html($datacenter_results['rack_cost_per_month']); ?></p>
<p><strong>Total Monthly Cost:</strong> $<?php echo esc_html($datacenter_results['total_monthly_cost']); ?></p>
<p><strong>Total Annual Cost:</strong> $<?php echo esc_html($datacenter_results['total_annual_cost']); ?></p>
<div class="chart-container">
<canvas class="datacenter-chart"
data-power="<?php echo esc_attr($datacenter_results['power_cost_per_month']); ?>"
data-cooling="<?php echo esc_attr($datacenter_results['cooling_cost_per_month']); ?>"
data-rack="<?php echo esc_attr($datacenter_results['rack_cost_per_month']); ?>">
</canvas>
</div>
</div>
<?php endif; ?>
</div>

View file

@ -0,0 +1,12 @@
<?php
/**
* Silence is golden.
*
* Prevent direct access to this file.
*
* @package WordPress
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

View file

@ -0,0 +1,38 @@
<?php
// templates/mortgage-calculator.php
$principal = isset($_POST['principal']) ? floatval($_POST['principal']) : 200000;
$interest_rate = isset($_POST['interest_rate']) ? floatval($_POST['interest_rate']) : 3.5;
$term_years = isset($_POST['term_years']) ? intval($_POST['term_years']) : 30;
$mortgage_results = maple_calculate_mortgage($principal, $interest_rate, $term_years);
?>
<div class="maple-calc mortgage-calculator <?php echo isset($attributes['align']) ? 'align' . esc_attr($attributes['align']) : 'alignnone'; ?>">
<h2>Mortgage Calculator</h2>
<form method="post">
<div class="form-group">
<label for="principal">Loan Amount ($):</label>
<input type="number" id="principal" name="principal" value="<?php echo isset($_POST['principal']) ? esc_attr($_POST['principal']) : '200000'; ?>" required>
</div>
<div class="form-group">
<label for="interest_rate">Interest Rate (%):</label>
<input type="number" step="0.01" id="interest_rate" name="interest_rate" value="<?php echo isset($_POST['interest_rate']) ? esc_attr($_POST['interest_rate']) : '3.5'; ?>" required>
</div>
<div class="form-group">
<label for="term_years">Loan Term (Years):</label>
<input type="number" id="term_years" name="term_years" value="<?php echo isset($_POST['term_years']) ? esc_attr($_POST['term_years']) : '30'; ?>" required>
</div>
<button type="submit" class="calculate-button">Calculate</button>
</form>
<?php if (isset($mortgage_results)): ?>
<div class="results">
<h3>Results</h3>
<p><strong>Monthly Payment:</strong> $<?php echo esc_html($mortgage_results['monthly_payment']); ?></p>
<p><strong>Total Payment:</strong> $<?php echo esc_html($mortgage_results['total_payment']); ?></p>
<p><strong>Total Interest:</strong> $<?php echo esc_html($mortgage_results['total_interest']); ?></p>
<div class="chart-container">
<canvas class="mortgage-chart" data-principal="<?php echo esc_attr($principal); ?>" data-interest="<?php echo esc_attr($mortgage_results['total_interest']); ?>"></canvas>
</div>
</div>
<?php endif; ?>
</div>

View file

@ -0,0 +1,38 @@
<?php
// templates/roi-calculator.php
$initial_investment = isset($_POST['initial_investment']) ? floatval($_POST['initial_investment']) : 10000;
$annual_return_rate = isset($_POST['annual_return_rate']) ? floatval($_POST['annual_return_rate']) : 7;
$time_years = isset($_POST['time_years']) ? intval($_POST['time_years']) : 5;
$roi_results = maple_calculate_roi($initial_investment, $annual_return_rate, $time_years);
?>
<div class="maple-calc roi-calculator <?php echo isset($attributes['align']) ? 'align' . esc_attr($attributes['align']) : 'alignnone'; ?>">
<h2>ROI Calculator</h2>
<form method="post">
<div class="form-group">
<label for="initial_investment">Initial Investment ($):</label>
<input type="number" id="initial_investment" name="initial_investment" value="<?php echo isset($_POST['initial_investment']) ? esc_attr($_POST['initial_investment']) : '10000'; ?>" required>
</div>
<div class="form-group">
<label for="annual_return_rate">Annual Return Rate (%):</label>
<input type="number" step="0.01" id="annual_return_rate" name="annual_return_rate" value="<?php echo isset($_POST['annual_return_rate']) ? esc_attr($_POST['annual_return_rate']) : '7'; ?>" required>
</div>
<div class="form-group">
<label for="time_years">Time (Years):</label>
<input type="number" id="time_years" name="time_years" value="<?php echo isset($_POST['time_years']) ? esc_attr($_POST['time_years']) : '5'; ?>" required>
</div>
<button type="submit" class="calculate-button">Calculate</button>
</form>
<?php if (isset($roi_results)): ?>
<div class="results">
<h3>Results</h3>
<p><strong>Final Value:</strong> $<?php echo esc_html($roi_results['final_value']); ?></p>
<p><strong>Net Profit:</strong> $<?php echo esc_html($roi_results['net_profit']); ?></p>
<p><strong>ROI (%):</strong> <?php echo esc_html($roi_results['roi_percentage']); ?>%</p>
<div class="chart-container">
<canvas class="roi-chart" data-investment="<?php echo esc_attr($initial_investment); ?>" data-profit="<?php echo esc_attr($roi_results['net_profit']); ?>"></canvas>
</div>
</div>
<?php endif; ?>
</div>

Binary file not shown.

View file

@ -1,19 +0,0 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(for po in *.po)",
"Bash(do mo=\"$po%.po.mo\")",
"Bash(msgfmt:*)",
"Bash(echo:*)",
"Bash(done)",
"Bash(ls:*)",
"Bash(xargs:*)",
"Bash(chmod:*)",
"Bash(composer install:*)",
"Bash(php:*)",
"Bash(docker --version:*)",
"Bash(brew list:*)"
]
}
}

View file

@ -1,2 +0,0 @@
<?php
// Silence is golden.

View file

@ -1,191 +0,0 @@
name: CI
on:
push:
branches: [main, develop]
paths:
- 'native/wordpress/maple-fonts-wp/**'
pull_request:
branches: [main, develop]
paths:
- 'native/wordpress/maple-fonts-wp/**'
defaults:
run:
working-directory: native/wordpress/maple-fonts-wp
jobs:
phpcs:
name: PHP Coding Standards
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer, cs2pr
coverage: none
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHP_CodeSniffer
run: ./vendor/bin/phpcs --report=checkstyle | cs2pr
continue-on-error: true
phpcompat:
name: PHP Compatibility
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer
coverage: none
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPCompatibility
run: ./vendor/bin/phpcs -p --standard=PHPCompatibilityWP --runtime-set testVersion 7.4- --extensions=php --ignore=vendor,tests .
test:
name: PHP ${{ matrix.php }} / WP ${{ matrix.wordpress }}
runs-on: ubuntu-latest
needs: [phpcs, phpcompat]
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0', '8.1', '8.2', '8.3']
wordpress: ['6.5', '6.6', 'latest']
exclude:
# PHP 8.3 not fully compatible with older WP versions
- php: '8.3'
wordpress: '6.5'
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, mysqli, gd, exif, intl
tools: composer
coverage: none
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-php${{ matrix.php }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Install WordPress test suite
run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wordpress }} true
- name: Run PHPUnit
run: ./vendor/bin/phpunit --configuration phpunit.xml.dist
build:
name: Build Plugin
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer
coverage: none
- name: Install dependencies (production)
run: composer install --prefer-dist --no-dev --optimize-autoloader --no-progress
- name: Create build directory
run: mkdir -p build
- name: Build plugin zip
run: |
zip -r build/maple-local-fonts.zip . \
-x ".git/*" \
-x ".github/*" \
-x "tests/*" \
-x "bin/*" \
-x "build/*" \
-x "vendor/bin/*" \
-x "*.xml" \
-x "*.xml.dist" \
-x "composer.*" \
-x "phpcs.xml*" \
-x ".editorconfig" \
-x ".gitignore" \
-x "CLAUDE.md" \
-x "SECURITY.md" \
-x "GOOGLE_FONTS_API.md" \
-x "WORDPRESS_COMPATIBILITY.md"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: maple-local-fonts
path: native/wordpress/maple-fonts-wp/build/maple-local-fonts.zip
retention-days: 30

View file

@ -1,2 +0,0 @@
<?php
// Silence is golden.

View file

@ -1,53 +0,0 @@
when:
- event: [push, pull_request]
path: 'native/wordpress/maple-fonts-wp/**'
steps:
install:
image: composer:2
commands:
- cd native/wordpress/maple-fonts-wp
- composer install --prefer-dist --no-interaction
phpcs:
image: php:8.2-cli
commands:
- cd native/wordpress/maple-fonts-wp
- ./vendor/bin/phpcs --standard=phpcs.xml.dist --report=summary || true
depends_on:
- install
phpcompat:
image: php:8.2-cli
commands:
- cd native/wordpress/maple-fonts-wp
- ./vendor/bin/phpcs -p --standard=PHPCompatibilityWP --runtime-set testVersion 7.4- --extensions=php --ignore=vendor,tests . || true
depends_on:
- install
test-php74:
image: wordpressdevelop/phpunit:9-php-7.4-fpm
commands:
- cd native/wordpress/maple-fonts-wp
- composer install --prefer-dist --no-interaction
- bash bin/install-wp-tests.sh wordpress_test root root database latest true
- ./vendor/bin/phpunit
depends_on:
- install
test-php82:
image: wordpressdevelop/phpunit:9-php-8.2-fpm
commands:
- cd native/wordpress/maple-fonts-wp
- composer install --prefer-dist --no-interaction
- bash bin/install-wp-tests.sh wordpress_test root root database latest true
- ./vendor/bin/phpunit
depends_on:
- install
services:
database:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_test

View file

@ -1,264 +0,0 @@
# CLAUDE.md — Maple Local Fonts WordPress Plugin
## Quick Reference
| Document | When to Reference |
|----------|-------------------|
| **CLAUDE.md** (this file) | Architecture, file structure, build order |
| **SECURITY.md** | Writing ANY PHP code |
| **GOOGLE_FONTS_API.md** | Building the font downloader class |
| **WORDPRESS_COMPATIBILITY.md** | Building the font registry, FSE integration |
---
## Project Overview
Build a WordPress plugin called **Maple Local Fonts** that:
1. Imports Google Fonts to local storage (one-time download)
2. Registers them with WordPress's native Font Library API
3. Fonts appear in FSE typography dropdowns alongside theme fonts
**Key principle:** Work WITH WordPress, not around it. We're a font importer — WordPress handles everything else.
---
## Requirements
- **Minimum PHP:** 7.4
- **Minimum WordPress:** 6.5 (required for Font Library API)
- **License:** GPL-2.0-or-later
---
## User Flow
```
Admin enters "Open Sans" → selects weights (400, 700) → selects styles (normal, italic)
Plugin hits Google Fonts CSS2 API (ONE TIME)
Downloads WOFF2 files to wp-content/fonts/
Registers font via WP Font Library API
Font appears in Appearance → Editor → Styles → Typography dropdown
User applies font using standard WordPress FSE controls
GOOGLE NEVER CONTACTED AGAIN — fonts served locally
```
---
## File Structure
```
maple-local-fonts/
├── maple-local-fonts.php # Main plugin file
├── index.php # Silence is golden
├── uninstall.php # Clean removal
├── readme.txt # WordPress.org readme
├── includes/
│ ├── index.php # Silence is golden
│ ├── class-mlf-font-downloader.php
│ ├── class-mlf-font-registry.php
│ ├── class-mlf-admin-page.php
│ └── class-mlf-ajax-handler.php
├── assets/
│ ├── index.php # Silence is golden
│ ├── admin.css
│ └── admin.js
└── languages/
├── index.php # Silence is golden
└── maple-local-fonts.pot
```
---
## Class Responsibilities
### MLF_Font_Downloader
- Build Google Fonts CSS2 URL
- Fetch CSS (with correct user-agent for WOFF2)
- Parse CSS to extract font face data
- Download WOFF2 files to wp-content/fonts/
- **Reference:** GOOGLE_FONTS_API.md
### MLF_Font_Registry
- Register fonts with WP Font Library (wp_font_family, wp_font_face post types)
- Delete fonts (remove posts and files)
- List imported fonts
- **Reference:** WORDPRESS_COMPATIBILITY.md
### MLF_Admin_Page
- Render settings page under Appearance menu
- Font name input, weight checkboxes, style checkboxes
- Display installed fonts with delete buttons
- **Reference:** SECURITY.md for output escaping
### MLF_Ajax_Handler
- Handle download requests
- Handle delete requests
- **Reference:** SECURITY.md for nonce/capability checks
---
## Admin Page UI
```
┌─────────────────────────────────────────────────────────┐
│ Maple Local Fonts │
├─────────────────────────────────────────────────────────┤
│ IMPORT FROM GOOGLE FONTS │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Font Name: [Open Sans_________________________] │ │
│ │ │ │
│ │ Weights: │ │
│ │ ☑ 400 (Regular) ☐ 300 (Light) ☐ 500 (Medium) │ │
│ │ ☑ 700 (Bold) ☐ 600 (Semi) ☐ 800 (Extra) │ │
│ │ │ │
│ │ Styles: │ │
│ │ ☑ Normal ☑ Italic │ │
│ │ │ │
│ │ Files to download: 4 │ │
│ │ [Download & Install] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ INSTALLED FONTS │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Open Sans │ │
│ │ 400 normal, 400 italic, 700 normal, 700 italic │ │
│ │ [Delete] │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ Roboto │ │
│ │ 400 normal, 500 normal, 700 normal │ │
│ │ [Delete] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
Use Appearance → Editor → Styles → Typography to │
│ apply fonts to your site. │
└─────────────────────────────────────────────────────────┘
```
---
## Build Order
### Phase 1: Foundation
1. `maple-local-fonts.php` — Plugin header, constants, autoloader, activation hook
2. `index.php` files in all directories (silence is golden)
3. `includes/class-mlf-ajax-handler.php` — With full security checks (nonce, capability, validation)
### Phase 2: Core Functionality
4. `includes/class-mlf-font-downloader.php` — Google Fonts fetching/parsing/downloading
5. `includes/class-mlf-font-registry.php` — WP Font Library integration
### Phase 3: Admin Interface
6. `includes/class-mlf-admin-page.php` — Settings page render
7. `assets/admin.css` — Admin page styles
8. `assets/admin.js` — AJAX handling for download/delete
### Phase 4: Cleanup & Polish
9. `uninstall.php` — Clean removal of fonts and data
10. `readme.txt` — WordPress.org readme with privacy section
11. Testing against all compatibility targets
---
## Critical Reminders
### Security (see SECURITY.md)
- ABSPATH check on EVERY PHP file
- index.php in EVERY directory
- Nonce verification FIRST in every AJAX handler
- Capability check SECOND
- Input validation THIRD
- Escape ALL output
### Performance
- Zero JavaScript on frontend
- Zero CSS on frontend
- All plugin code is admin-side only (except lightweight font registration)
- Timeout handling on all external requests
- Maximum limits to prevent infinite loops
### GDPR
- No user data collected
- External requests only during admin import
- Fonts served locally after import
- Document in readme.txt
### Compatibility (see WORDPRESS_COMPATIBILITY.md)
- Declare WooCommerce HPOS compatibility
- Use wp_get_font_dir() not hardcoded paths
- Use Font Library API (post types) not theme.json filtering
- Don't touch frontend, let Global Styles handle everything
---
## Constants
```php
define('MLF_VERSION', '1.0.0');
define('MLF_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MLF_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MLF_PLUGIN_BASENAME', plugin_basename(__FILE__));
// Limits
define('MLF_MAX_FONTS_PER_REQUEST', 10);
define('MLF_MAX_WEIGHTS_PER_FONT', 9);
define('MLF_REQUEST_TIMEOUT', 30);
```
---
## WordPress Hooks Used
```php
// Activation
register_activation_hook(__FILE__, 'mlf_activate');
// Admin menu
add_action('admin_menu', 'mlf_register_menu');
// Admin assets (our page only)
add_action('admin_enqueue_scripts', 'mlf_enqueue_admin_assets');
// AJAX (admin only, no nopriv)
add_action('wp_ajax_mlf_download_font', 'mlf_ajax_download');
add_action('wp_ajax_mlf_delete_font', 'mlf_ajax_delete');
// WooCommerce HPOS
add_action('before_woocommerce_init', 'mlf_declare_hpos_compatibility');
```
---
## Testing Checklist
### Functional
- [ ] Import Open Sans 400, 700 normal + italic → 4 files created
- [ ] Font appears in FSE typography dropdown
- [ ] Apply font in Global Styles → frontend shows local font
- [ ] Delete font → files and posts removed
- [ ] Re-import same font → appropriate error message
### Security
- [ ] AJAX without nonce → 403
- [ ] AJAX as subscriber → 403
- [ ] Path traversal in font name → rejected
- [ ] XSS in font name → sanitized
- [ ] Direct PHP file access → blank/exit
### Performance
- [ ] No frontend network requests from plugin
- [ ] Import 5 fonts → completes without timeout
- [ ] Admin page load < 500ms added time
### Compatibility
- [ ] WordPress 6.5, 6.6+
- [ ] Twenty Twenty-Five theme
- [ ] WooCommerce (HPOS enabled)
- [ ] Wordfence (enabled)
- [ ] LearnDash
- [ ] WPForms

View file

@ -1,707 +0,0 @@
# GOOGLE_FONTS_API.md — Google Fonts Retrieval Guide
## Overview
This document covers how to retrieve fonts from Google Fonts CSS2 API. This is a **one-time retrieval** during admin import — after download, fonts are served locally and Google is never contacted again.
---
## The Retrieval Flow
```
1. Admin enters "Open Sans" + selects weights/styles
2. Plugin constructs Google Fonts CSS2 URL
3. Plugin fetches CSS (contains @font-face rules with WOFF2 URLs)
4. Plugin parses CSS to extract WOFF2 URLs
5. Plugin downloads each WOFF2 file to wp-content/fonts/
6. Plugin registers font with WordPress Font Library
7. DONE - Google never contacted again
```
---
## Google Fonts CSS2 API
### Base URL
```
https://fonts.googleapis.com/css2
```
### URL Construction
**Pattern:**
```
https://fonts.googleapis.com/css2?family={Font+Name}:ital,wght@{variations}&display=swap
```
**Variations format:**
```
ital,wght@{italic},{weight};{italic},{weight};...
```
Where:
- `ital` = 0 (normal) or 1 (italic)
- `wght` = weight (100-900)
### Examples
**Open Sans 400 normal only:**
```
https://fonts.googleapis.com/css2?family=Open+Sans:wght@400&display=swap
```
**Open Sans 400 + 700 normal:**
```
https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap
```
**Open Sans 400 + 700, both normal and italic:**
```
https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap
```
**Breakdown of `ital,wght@0,400;0,700;1,400;1,700`:**
- `0,400` = normal 400
- `0,700` = normal 700
- `1,400` = italic 400
- `1,700` = italic 700
### URL Builder Function
```php
/**
* Build Google Fonts CSS2 API URL.
*
* @param string $font_name Font family name (e.g., "Open Sans")
* @param array $weights Array of weights (e.g., [400, 700])
* @param array $styles Array of styles (e.g., ['normal', 'italic'])
* @return string Google Fonts CSS2 URL
*/
function mlf_build_google_fonts_url($font_name, $weights, $styles) {
// URL-encode font name (spaces become +)
$family = str_replace(' ', '+', $font_name);
// Build variations
$variations = [];
// Sort for consistent URLs
sort($weights);
sort($styles);
$has_italic = in_array('italic', $styles, true);
$has_normal = in_array('normal', $styles, true);
foreach ($weights as $weight) {
if ($has_normal) {
$variations[] = "0,{$weight}";
}
if ($has_italic) {
$variations[] = "1,{$weight}";
}
}
// If only normal styles, simpler format
if ($has_normal && !$has_italic) {
$wght = implode(';', $weights);
return "https://fonts.googleapis.com/css2?family={$family}:wght@{$wght}&display=swap";
}
// Full format with ital axis
$variation_string = implode(';', $variations);
return "https://fonts.googleapis.com/css2?family={$family}:ital,wght@{$variation_string}&display=swap";
}
```
---
## CRITICAL: User-Agent Requirement
Google Fonts returns **different file formats** based on user-agent:
| User-Agent | Format Returned |
|------------|-----------------|
| Old browser | TTF or WOFF |
| Modern browser | WOFF2 |
| curl (no UA) | TTF |
**We need WOFF2** (smallest, best compression, modern browser support).
### Required User-Agent
Use a modern Chrome user-agent:
```php
$user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
```
### Fetch Function
```php
/**
* Fetch CSS from Google Fonts API.
*
* @param string $font_name Font family name
* @param array $weights Weights to fetch
* @param array $styles Styles to fetch
* @return string|WP_Error CSS content or error
*/
function mlf_fetch_google_css($font_name, $weights, $styles) {
$url = mlf_build_google_fonts_url($font_name, $weights, $styles);
// Validate URL
if (!mlf_is_valid_google_fonts_url($url)) {
return new WP_Error('invalid_url', 'Invalid Google Fonts URL');
}
// CRITICAL: Must use modern browser user-agent to get WOFF2
$response = wp_remote_get($url, [
'timeout' => 15,
'sslverify' => true,
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
]);
if (is_wp_error($response)) {
return new WP_Error(
'request_failed',
'Could not connect to Google Fonts: ' . $response->get_error_message()
);
}
$status = wp_remote_retrieve_response_code($response);
if ($status === 400) {
return new WP_Error('font_not_found', 'Font not found on Google Fonts');
}
if ($status !== 200) {
return new WP_Error('http_error', 'Google Fonts returned HTTP ' . $status);
}
$css = wp_remote_retrieve_body($response);
if (empty($css)) {
return new WP_Error('empty_response', 'Empty response from Google Fonts');
}
// Verify we got WOFF2 (sanity check)
if (strpos($css, '.woff2)') === false) {
return new WP_Error('wrong_format', 'Did not receive WOFF2 format - check user-agent');
}
return $css;
}
```
---
## Parsing the CSS Response
### Sample Response
Google returns CSS like this:
```css
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1x4gaVI.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, ...;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, ...;
}
```
### Key Observations
1. **Multiple @font-face blocks per weight** — Google splits fonts by unicode subset (latin, latin-ext, cyrillic, etc.)
2. **We want the "latin" subset** — It's the base and covers most use cases
3. **Each block has:** font-family, font-style, font-weight, src URL, unicode-range
### Unicode Subset Strategy
**Option A: Download only latin (simpler, smaller)**
- Parse CSS, identify latin blocks, download only those
- Good for most sites
**Option B: Download all subsets (more complete)**
- Download all WOFF2 files
- Larger but supports more languages
**Recommended: Option A** — Start with latin subset. Can add option for more subsets later.
### Parsing Function
```php
/**
* Parse Google Fonts CSS and extract font face data.
*
* @param string $css CSS content from Google Fonts
* @param string $font_name Expected font family name
* @return array|WP_Error Array of font face data or error
*/
function mlf_parse_google_css($css, $font_name) {
$font_faces = [];
// Match all @font-face blocks
$pattern = '/@font-face\s*\{([^}]+)\}/s';
if (!preg_match_all($pattern, $css, $matches)) {
return new WP_Error('parse_failed', 'Could not parse CSS - no @font-face rules found');
}
foreach ($matches[1] as $block) {
$face_data = mlf_parse_font_face_block($block);
if (is_wp_error($face_data)) {
continue; // Skip malformed blocks
}
// Verify font family matches (security)
if (strcasecmp($face_data['family'], $font_name) !== 0) {
continue;
}
// Create unique key for weight+style combo
$key = $face_data['weight'] . '-' . $face_data['style'];
// Prefer latin subset (usually comes after latin-ext)
// Check if this is a latin block by unicode-range
$is_latin = mlf_is_latin_subset($face_data['unicode_range']);
// Only store if:
// 1. We don't have this weight/style yet, OR
// 2. This is latin and replaces non-latin
if (!isset($font_faces[$key]) || $is_latin) {
$font_faces[$key] = $face_data;
}
}
if (empty($font_faces)) {
return new WP_Error('no_fonts', 'No valid font faces found in CSS');
}
return array_values($font_faces);
}
/**
* Parse a single @font-face block.
*
* @param string $block Content inside @font-face { }
* @return array|WP_Error Parsed data or error
*/
function mlf_parse_font_face_block($block) {
$data = [];
// Extract font-family
if (preg_match('/font-family:\s*[\'"]?([^;\'"]+)[\'"]?;/i', $block, $m)) {
$data['family'] = trim($m[1]);
} else {
return new WP_Error('missing_family', 'Missing font-family');
}
// Extract font-weight
if (preg_match('/font-weight:\s*(\d+);/i', $block, $m)) {
$data['weight'] = $m[1];
} else {
return new WP_Error('missing_weight', 'Missing font-weight');
}
// Extract font-style
if (preg_match('/font-style:\s*(\w+);/i', $block, $m)) {
$data['style'] = $m[1];
} else {
$data['style'] = 'normal'; // Default
}
// Extract src URL - MUST be fonts.gstatic.com
if (preg_match('/src:\s*url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/i', $block, $m)) {
$data['url'] = $m[1];
} else {
return new WP_Error('missing_src', 'Missing or invalid src URL');
}
// Extract unicode-range (optional, for subset detection)
if (preg_match('/unicode-range:\s*([^;]+);/i', $block, $m)) {
$data['unicode_range'] = trim($m[1]);
} else {
$data['unicode_range'] = '';
}
return $data;
}
/**
* Check if unicode-range indicates latin subset.
* Latin typically starts with U+0000-00FF.
*
* @param string $range Unicode range string
* @return bool True if appears to be latin subset
*/
function mlf_is_latin_subset($range) {
// Latin subset typically includes basic ASCII range
// and does NOT include extended Latin (U+0100+) as primary
if (empty($range)) {
return true; // Assume latin if no range specified
}
// Latin subset usually starts with U+0000 and includes U+00FF
// Latin-ext starts with U+0100
if (preg_match('/U\+0000/', $range) && !preg_match('/^U\+0100/', $range)) {
return true;
}
return false;
}
```
---
## Downloading WOFF2 Files
### Download Function
```php
/**
* Download a single WOFF2 file from Google Fonts.
*
* @param string $url Google Fonts static URL
* @param string $font_slug Font slug (e.g., "open-sans")
* @param string $weight Font weight (e.g., "400")
* @param string $style Font style (e.g., "normal")
* @return string|WP_Error Local file path or error
*/
function mlf_download_font_file($url, $font_slug, $weight, $style) {
// Validate URL is from Google
if (!mlf_is_valid_google_fonts_url($url)) {
return new WP_Error('invalid_url', 'URL is not from Google Fonts');
}
// Build local filename
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight);
$filename = sanitize_file_name($filename);
// Get destination path
$font_dir = wp_get_font_dir();
$destination = trailingslashit($font_dir['path']) . $filename;
// Validate destination path
if (!mlf_validate_font_path($destination)) {
return new WP_Error('invalid_path', 'Invalid destination path');
}
// Ensure directory exists
if (!wp_mkdir_p($font_dir['path'])) {
return new WP_Error('mkdir_failed', 'Could not create fonts directory');
}
// Download file
$response = wp_remote_get($url, [
'timeout' => 30,
'sslverify' => true,
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
]);
if (is_wp_error($response)) {
return new WP_Error(
'download_failed',
'Failed to download font file: ' . $response->get_error_message()
);
}
$status = wp_remote_retrieve_response_code($response);
if ($status !== 200) {
return new WP_Error('http_error', 'Font download returned HTTP ' . $status);
}
$content = wp_remote_retrieve_body($response);
if (empty($content)) {
return new WP_Error('empty_file', 'Downloaded font file is empty');
}
// Verify it looks like a WOFF2 file (magic bytes: wOF2)
if (substr($content, 0, 4) !== 'wOF2') {
return new WP_Error('invalid_format', 'Downloaded file is not valid WOFF2');
}
// Write file using WP Filesystem
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) {
return new WP_Error('write_failed', 'Could not write font file');
}
return $destination;
}
```
### Batch Download Function
```php
/**
* Download all font files for a font family.
*
* @param string $font_name Font family name
* @param array $weights Weights to download
* @param array $styles Styles to download
* @return array|WP_Error Array of downloaded files or error
*/
function mlf_download_font_family($font_name, $weights, $styles) {
// Fetch CSS from Google
$css = mlf_fetch_google_css($font_name, $weights, $styles);
if (is_wp_error($css)) {
return $css;
}
// Parse CSS to get font face data
$font_faces = mlf_parse_google_css($css, $font_name);
if (is_wp_error($font_faces)) {
return $font_faces;
}
// Generate slug from font name
$font_slug = sanitize_title($font_name);
// Download each font file
$downloaded = [];
$errors = [];
foreach ($font_faces as $face) {
$result = mlf_download_font_file(
$face['url'],
$font_slug,
$face['weight'],
$face['style']
);
if (is_wp_error($result)) {
$errors[] = $result->get_error_message();
continue;
}
$downloaded[] = [
'path' => $result,
'weight' => $face['weight'],
'style' => $face['style'],
];
}
// If no files downloaded, return error
if (empty($downloaded)) {
return new WP_Error(
'download_failed',
'Could not download any font files: ' . implode(', ', $errors)
);
}
return [
'font_name' => $font_name,
'font_slug' => $font_slug,
'files' => $downloaded,
];
}
```
---
## Error Handling
### Common Errors
| Error | Cause | User Message |
|-------|-------|--------------|
| Font not found | Typo in font name | "Font not found on Google Fonts. Check the spelling." |
| Network timeout | Slow connection | "Could not connect to Google Fonts. Please try again." |
| Invalid format | Wrong user-agent | Internal error - should not happen |
| Write failed | Permissions | "Could not save font files. Check directory permissions." |
### Error Messages (User-Friendly)
```php
/**
* Convert internal error codes to user-friendly messages.
*
* @param WP_Error $error The error object
* @return string User-friendly message
*/
function mlf_get_user_error_message($error) {
$code = $error->get_error_code();
$messages = [
'font_not_found' => 'Font not found on Google Fonts. Please check the spelling and try again.',
'request_failed' => 'Could not connect to Google Fonts. Please check your internet connection and try again.',
'http_error' => 'Google Fonts returned an error. Please try again later.',
'parse_failed' => 'Could not process the font data. The font may not be available.',
'download_failed' => 'Could not download the font files. Please try again.',
'write_failed' => 'Could not save font files. Please check that wp-content/fonts is writable.',
'mkdir_failed' => 'Could not create fonts directory. Please check file permissions.',
'invalid_path' => 'Invalid file path. Please contact support.',
'invalid_url' => 'Invalid font URL. Please contact support.',
];
return $messages[$code] ?? 'An unexpected error occurred. Please try again.';
}
```
---
## Complete Downloader Class
```php
<?php
if (!defined('ABSPATH')) {
exit;
}
class MLF_Font_Downloader {
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
/**
* Download a font from Google Fonts.
*
* @param string $font_name Font family name
* @param array $weights Weights to download
* @param array $styles Styles to download
* @return array|WP_Error Download result or error
*/
public function download($font_name, $weights, $styles) {
// Validate inputs
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
return new WP_Error('invalid_name', 'Invalid font name');
}
$weights = array_intersect(array_map('absint', $weights), [100,200,300,400,500,600,700,800,900]);
if (empty($weights)) {
return new WP_Error('invalid_weights', 'No valid weights specified');
}
$styles = array_intersect($styles, ['normal', 'italic']);
if (empty($styles)) {
return new WP_Error('invalid_styles', 'No valid styles specified');
}
// Fetch CSS
$css = $this->fetch_css($font_name, $weights, $styles);
if (is_wp_error($css)) {
return $css;
}
// Parse CSS
$font_faces = $this->parse_css($css, $font_name);
if (is_wp_error($font_faces)) {
return $font_faces;
}
// Download files
$font_slug = sanitize_title($font_name);
$downloaded = $this->download_files($font_faces, $font_slug);
if (is_wp_error($downloaded)) {
return $downloaded;
}
return [
'font_name' => $font_name,
'font_slug' => $font_slug,
'files' => $downloaded,
];
}
private function build_url($font_name, $weights, $styles) {
$family = str_replace(' ', '+', $font_name);
sort($weights);
$has_italic = in_array('italic', $styles, true);
$has_normal = in_array('normal', $styles, true);
if ($has_normal && !$has_italic) {
$wght = implode(';', $weights);
return "https://fonts.googleapis.com/css2?family={$family}:wght@{$wght}&display=swap";
}
$variations = [];
foreach ($weights as $weight) {
if ($has_normal) {
$variations[] = "0,{$weight}";
}
if ($has_italic) {
$variations[] = "1,{$weight}";
}
}
$variation_string = implode(';', $variations);
return "https://fonts.googleapis.com/css2?family={$family}:ital,wght@{$variation_string}&display=swap";
}
private function fetch_css($font_name, $weights, $styles) {
$url = $this->build_url($font_name, $weights, $styles);
$response = wp_remote_get($url, [
'timeout' => 15,
'sslverify' => true,
'user-agent' => $this->user_agent,
]);
if (is_wp_error($response)) {
return new WP_Error('request_failed', $response->get_error_message());
}
$status = wp_remote_retrieve_response_code($response);
if ($status === 400) {
return new WP_Error('font_not_found', 'Font not found');
}
if ($status !== 200) {
return new WP_Error('http_error', 'HTTP ' . $status);
}
$css = wp_remote_retrieve_body($response);
if (empty($css) || strpos($css, '.woff2)') === false) {
return new WP_Error('invalid_response', 'Invalid CSS response');
}
return $css;
}
private function parse_css($css, $font_name) {
// Implementation as shown above
// Returns array of font face data
}
private function download_files($font_faces, $font_slug) {
// Implementation as shown above
// Returns array of downloaded file info
}
}
```
---
## Testing Checklist
- [ ] Valid font name (Open Sans) returns CSS with WOFF2 URLs
- [ ] Invalid font name returns appropriate error
- [ ] Multiple weights are all downloaded
- [ ] Italic styles are handled correctly
- [ ] Files are saved to correct location
- [ ] Files have correct WOFF2 magic bytes
- [ ] Timeout handling works (test with slow connection)
- [ ] User-agent produces WOFF2 (not TTF/WOFF)

View file

@ -1,621 +0,0 @@
# SECURITY.md — Maple Local Fonts Security Requirements
## Overview
This document covers all security requirements for the Maple Local Fonts plugin. Reference this when writing ANY PHP code.
---
## ABSPATH Check (Every PHP File)
Every PHP file MUST start with this check. No exceptions.
```php
<?php
if (!defined('ABSPATH')) {
exit;
}
```
---
## Silence is Golden Files
Create `index.php` in EVERY directory:
```php
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}
```
**Required locations:**
- `/maple-local-fonts/index.php`
- `/maple-local-fonts/includes/index.php`
- `/maple-local-fonts/assets/index.php`
- `/maple-local-fonts/languages/index.php`
---
## OWASP Compliance
### A1 - Injection Prevention
**SQL Injection:**
```php
// NEVER do this
$wpdb->query("SELECT * FROM table WHERE id = " . $_POST['id']);
// ALWAYS do this
$wpdb->get_results($wpdb->prepare(
"SELECT * FROM %i WHERE id = %d",
$table_name,
absint($_POST['id'])
));
```
**Note:** This plugin should rarely need direct SQL. Use WordPress APIs (`get_posts`, `wp_insert_post`, etc.) which handle escaping internally.
### A2 - Authentication
All admin actions require capability check:
```php
if (!current_user_can('edit_theme_options')) {
wp_die('Unauthorized', 'Error', ['response' => 403]);
}
```
### A3 - Sensitive Data
- No API keys (Google Fonts CSS2 API is public)
- No user credentials stored
- No PII collected
### A5 - Broken Access Control
**Order of checks for ALL AJAX handlers:**
```php
public function handle_ajax_action() {
// 1. Nonce verification FIRST
if (!check_ajax_referer('mlf_action_name', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
// 2. Capability check SECOND
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => 'Unauthorized'], 403);
}
// 3. Input validation THIRD
// ... validate all inputs ...
// 4. Process request
// ... actual logic ...
}
```
### A7 - Cross-Site Scripting (XSS)
**Escape ALL output:**
```php
// HTML content
echo esc_html($font_name);
// HTML attributes
echo '<input value="' . esc_attr($font_name) . '">';
// URLs
echo '<a href="' . esc_url($url) . '">';
// JavaScript data
wp_localize_script('mlf-admin', 'mlfData', [
'fontName' => esc_js($font_name), // Or let wp_localize_script handle it
]);
// Translatable strings with variables
printf(
esc_html__('Installed: %s', 'maple-local-fonts'),
esc_html($font_name)
);
```
**Never trust input for output:**
```php
// WRONG - XSS vulnerability
echo '<div>' . $_POST['font_name'] . '</div>';
// RIGHT - sanitize input, escape output
$font_name = sanitize_text_field($_POST['font_name']);
echo '<div>' . esc_html($font_name) . '</div>';
```
### A8 - Insecure Deserialization
```php
// NEVER use unserialize() on external data
$data = unserialize($_POST['data']); // DANGEROUS
// Use JSON instead
$data = json_decode(sanitize_text_field($_POST['data']), true);
if (json_last_error() !== JSON_ERROR_NONE) {
wp_send_json_error(['message' => 'Invalid data format']);
}
```
### A9 - Vulnerable Components
- No external PHP libraries
- Use only WordPress core functions
- Keep dependencies to zero
---
## Nonce Implementation
### Creating Nonces
**In admin page form:**
```php
wp_nonce_field('mlf_download_font', 'mlf_nonce');
```
**For AJAX (via wp_localize_script):**
```php
wp_localize_script('mlf-admin', 'mlfData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('mlf_download_font'),
]);
```
### Verifying Nonces
**AJAX handler:**
```php
// Returns false on failure, doesn't die (we handle response ourselves)
if (!check_ajax_referer('mlf_download_font', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
```
**Form submission:**
```php
if (!wp_verify_nonce($_POST['mlf_nonce'], 'mlf_download_font')) {
wp_die('Security check failed');
}
```
### Nonce Names
Use consistent, descriptive nonce action names:
| Action | Nonce Name |
|--------|------------|
| Download font | `mlf_download_font` |
| Delete font | `mlf_delete_font` |
| Update settings | `mlf_update_settings` |
---
## Input Validation
### Font Name Validation
```php
$font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : '';
// Strict allowlist pattern - alphanumeric, spaces, hyphens only
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
wp_send_json_error(['message' => 'Invalid font name: only letters, numbers, spaces, and hyphens allowed']);
}
// Length limit
if (strlen($font_name) > 100) {
wp_send_json_error(['message' => 'Font name too long']);
}
// Not empty
if (empty($font_name)) {
wp_send_json_error(['message' => 'Font name required']);
}
```
### Weight Validation
```php
$weights = isset($_POST['weights']) ? (array) $_POST['weights'] : [];
// Convert to integers
$weights = array_map('absint', $weights);
// Strict allowlist
$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
$weights = array_intersect($weights, $allowed_weights);
// Must have at least one
if (empty($weights)) {
wp_send_json_error(['message' => 'At least one weight required']);
}
```
### Style Validation
```php
$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : [];
// Strict allowlist - only these two values ever
$allowed_styles = ['normal', 'italic'];
$styles = array_filter($styles, function($style) use ($allowed_styles) {
return in_array($style, $allowed_styles, true);
});
// Must have at least one
if (empty($styles)) {
wp_send_json_error(['message' => 'At least one style required']);
}
```
### Font Family ID Validation (for delete)
```php
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
if ($font_id < 1) {
wp_send_json_error(['message' => 'Invalid font ID']);
}
// Verify it exists and is a font family
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => 'Font not found']);
}
// Verify it's one we imported (not a theme font)
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => 'Cannot delete theme fonts']);
}
```
---
## File Operation Security
### Path Traversal Prevention
```php
/**
* Validate that a path is within the WordPress fonts directory.
* Prevents path traversal attacks.
*
* @param string $path Full path to validate
* @return bool True if path is safe, false otherwise
*/
function mlf_validate_font_path($path) {
$font_dir = wp_get_font_dir();
$fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));
// Resolve to real path (handles ../ etc)
$real_path = realpath($path);
// If realpath fails, file doesn't exist yet - validate the directory
if ($real_path === false) {
$dir = dirname($path);
$real_dir = realpath($dir);
if ($real_dir === false) {
return false;
}
$real_path = wp_normalize_path($real_dir . '/' . basename($path));
} else {
$real_path = wp_normalize_path($real_path);
}
// Must be within fonts directory
return strpos($real_path, $fonts_path) === 0;
}
```
### Filename Sanitization
```php
/**
* Sanitize and validate a font filename.
*
* @param string $filename The filename to validate
* @return string|false Sanitized filename or false if invalid
*/
function mlf_sanitize_font_filename($filename) {
// WordPress sanitization first
$filename = sanitize_file_name($filename);
// Must have .woff2 extension
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') {
return false;
}
// No path components
if ($filename !== basename($filename)) {
return false;
}
// Reasonable length
if (strlen($filename) > 200) {
return false;
}
return $filename;
}
```
### Safe File Writing
```php
/**
* Safely write a font file to the fonts directory.
*
* @param string $filename Sanitized filename
* @param string $content File content
* @return string|WP_Error File path on success, WP_Error on failure
*/
function mlf_write_font_file($filename, $content) {
// Validate filename
$safe_filename = mlf_sanitize_font_filename($filename);
if ($safe_filename === false) {
return new WP_Error('invalid_filename', 'Invalid filename');
}
// Get fonts directory
$font_dir = wp_get_font_dir();
$destination = trailingslashit($font_dir['path']) . $safe_filename;
// Validate path
if (!mlf_validate_font_path($destination)) {
return new WP_Error('invalid_path', 'Invalid file path');
}
// Ensure directory exists
if (!wp_mkdir_p($font_dir['path'])) {
return new WP_Error('mkdir_failed', 'Could not create fonts directory');
}
// Write file
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) {
return new WP_Error('write_failed', 'Could not write font file');
}
return $destination;
}
```
### Safe File Deletion
```php
/**
* Safely delete a font file.
*
* @param string $path Full path to the file
* @return bool True on success, false on failure
*/
function mlf_delete_font_file($path) {
// Validate path is within fonts directory
if (!mlf_validate_font_path($path)) {
return false;
}
// Must be a .woff2 file
if (pathinfo($path, PATHINFO_EXTENSION) !== 'woff2') {
return false;
}
// File must exist
if (!file_exists($path)) {
return true; // Already gone, that's fine
}
return wp_delete_file($path);
}
```
---
## HTTP Request Security
### Outbound Requests (Google Fonts)
```php
$response = wp_remote_get($url, [
'timeout' => 15,
'sslverify' => true, // Always verify SSL
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
]);
// Check for errors
if (is_wp_error($response)) {
// Log error, return gracefully
error_log('MLF: Google Fonts request failed - ' . $response->get_error_message());
return new WP_Error('request_failed', 'Could not connect to Google Fonts');
}
// Check HTTP status
$status = wp_remote_retrieve_response_code($response);
if ($status !== 200) {
return new WP_Error('http_error', 'Google Fonts returned status ' . $status);
}
// Get body
$body = wp_remote_retrieve_body($response);
if (empty($body)) {
return new WP_Error('empty_response', 'Empty response from Google Fonts');
}
```
### URL Validation (Google Fonts only)
```php
/**
* Validate that a URL is a legitimate Google Fonts URL.
*
* @param string $url URL to validate
* @return bool True if valid Google Fonts URL
*/
function mlf_is_valid_google_fonts_url($url) {
$parsed = wp_parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
// Only allow Google Fonts domains
$allowed_hosts = [
'fonts.googleapis.com',
'fonts.gstatic.com',
];
return in_array($parsed['host'], $allowed_hosts, true);
}
```
---
## AJAX Handler Complete Template
```php
<?php
if (!defined('ABSPATH')) {
exit;
}
class MLF_Ajax_Handler {
public function __construct() {
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
// NEVER add wp_ajax_nopriv_ - admin only functionality
}
/**
* Handle font download AJAX request.
*/
public function handle_download() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_download_font', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
// 2. CAPABILITY CHECK
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => 'Unauthorized'], 403);
}
// 3. INPUT VALIDATION
$font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : '';
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name) || strlen($font_name) > 100) {
wp_send_json_error(['message' => 'Invalid font name']);
}
$weights = isset($_POST['weights']) ? array_map('absint', (array) $_POST['weights']) : [];
$weights = array_intersect($weights, [100, 200, 300, 400, 500, 600, 700, 800, 900]);
if (empty($weights)) {
wp_send_json_error(['message' => 'At least one weight required']);
}
$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : [];
$styles = array_intersect($styles, ['normal', 'italic']);
if (empty($styles)) {
wp_send_json_error(['message' => 'At least one style required']);
}
// 4. PROCESS REQUEST
try {
$downloader = new MLF_Font_Downloader();
$result = $downloader->download($font_name, $weights, $styles);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success([
'message' => sprintf('Successfully installed %s', esc_html($font_name)),
'font_id' => $result,
]);
} catch (Exception $e) {
error_log('MLF Download Error: ' . $e->getMessage());
wp_send_json_error(['message' => 'An unexpected error occurred']);
}
}
/**
* Handle font deletion AJAX request.
*/
public function handle_delete() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_delete_font', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
// 2. CAPABILITY CHECK
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => 'Unauthorized'], 403);
}
// 3. INPUT VALIDATION
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
if ($font_id < 1) {
wp_send_json_error(['message' => 'Invalid font ID']);
}
// Verify font exists and is ours
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => 'Font not found']);
}
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => 'Cannot delete theme fonts']);
}
// 4. PROCESS REQUEST
try {
$registry = new MLF_Font_Registry();
$result = $registry->delete_font($font_id);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success(['message' => 'Font deleted successfully']);
} catch (Exception $e) {
error_log('MLF Delete Error: ' . $e->getMessage());
wp_send_json_error(['message' => 'An unexpected error occurred']);
}
}
}
```
---
## Security Checklist
Before committing any code:
- [ ] ABSPATH check at top of every PHP file
- [ ] index.php exists in every directory
- [ ] All AJAX handlers verify nonce first
- [ ] All AJAX handlers check capability second
- [ ] All user input sanitized with appropriate function
- [ ] All user input validated against allowlists where applicable
- [ ] All output escaped with appropriate function
- [ ] File paths validated to prevent traversal
- [ ] No direct SQL queries (use WordPress APIs)
- [ ] No `unserialize()` on user input
- [ ] No `eval()` or similar dynamic execution
- [ ] External URLs validated before use
- [ ] Error messages don't expose sensitive info

View file

@ -1,560 +0,0 @@
# WORDPRESS_COMPATIBILITY.md — WordPress & Plugin Compatibility
## Overview
This document covers compatibility requirements for WordPress core systems and popular plugins. Reference this when building the font registry class and integration points.
---
## WordPress Version Requirements
**Minimum: WordPress 6.5**
WordPress 6.5 introduced the Font Library API which this plugin depends on. Earlier versions will not work.
```php
// Check on activation
register_activation_hook(__FILE__, function() {
if (version_compare(get_bloginfo('version'), '6.5', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die(
'Maple Local Fonts requires WordPress 6.5 or higher for Font Library support.',
'Plugin Activation Error',
['back_link' => true]
);
}
});
// Also check on admin init (in case WP was downgraded)
add_action('admin_init', function() {
if (version_compare(get_bloginfo('version'), '6.5', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
add_action('admin_notices', function() {
echo '<div class="error"><p>Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.</p></div>';
});
}
});
```
---
## WordPress Font Library API
### How It Works
WordPress 6.5+ stores fonts using custom post types:
| Post Type | Purpose |
|-----------|---------|
| `wp_font_family` | Font family (e.g., "Open Sans") |
| `wp_font_face` | Individual weight/style variant (child of family) |
Fonts are stored in `wp-content/fonts/` by default.
### Getting the Fonts Directory
```php
// ALWAYS use this function, never hardcode paths
$font_dir = wp_get_font_dir();
// Returns:
[
'path' => '/var/www/html/wp-content/fonts',
'url' => 'https://example.com/wp-content/fonts',
'subdir' => '',
'basedir' => '/var/www/html/wp-content/fonts',
'baseurl' => 'https://example.com/wp-content/fonts',
]
```
### Registering a Font Family
```php
/**
* Register a font family with WordPress Font Library.
*
* @param string $font_name Display name (e.g., "Open Sans")
* @param string $font_slug Slug (e.g., "open-sans")
* @param array $files Array of downloaded file data
* @return int|WP_Error Font family post ID or error
*/
function mlf_register_font_family($font_name, $font_slug, $files) {
// Check if font already exists
$existing = get_posts([
'post_type' => 'wp_font_family',
'name' => $font_slug,
'posts_per_page' => 1,
'post_status' => 'publish',
]);
if (!empty($existing)) {
return new WP_Error('font_exists', 'Font family already installed');
}
// Build font family settings
$font_family_settings = [
'name' => $font_name,
'slug' => $font_slug,
'fontFamily' => sprintf('"%s", sans-serif', $font_name),
'fontFace' => [],
];
// Add each font face
foreach ($files as $file) {
$font_dir = wp_get_font_dir();
$relative_path = str_replace($font_dir['path'], '', $file['path']);
$font_family_settings['fontFace'][] = [
'fontFamily' => $font_name,
'fontWeight' => $file['weight'],
'fontStyle' => $file['style'],
'src' => 'file:.' . $font_dir['basedir'] . $relative_path,
];
}
// Create font family post
$family_id = wp_insert_post([
'post_type' => 'wp_font_family',
'post_title' => $font_name,
'post_name' => $font_slug,
'post_status' => 'publish',
'post_content' => wp_json_encode($font_family_settings),
]);
if (is_wp_error($family_id)) {
return $family_id;
}
// Mark as imported by our plugin (for identification)
update_post_meta($family_id, '_mlf_imported', '1');
update_post_meta($family_id, '_mlf_import_date', current_time('mysql'));
// Create font face posts (children)
foreach ($files as $file) {
$font_dir = wp_get_font_dir();
$face_settings = [
'fontFamily' => $font_name,
'fontWeight' => $file['weight'],
'fontStyle' => $file['style'],
'src' => 'file:.' . $font_dir['baseurl'] . '/' . basename($file['path']),
];
wp_insert_post([
'post_type' => 'wp_font_face',
'post_parent' => $family_id,
'post_status' => 'publish',
'post_content' => wp_json_encode($face_settings),
]);
}
// Clear font caches
delete_transient('wp_font_library_fonts');
return $family_id;
}
```
### Deleting a Font Family
```php
/**
* Delete a font family and its files.
*
* @param int $family_id Font family post ID
* @return bool|WP_Error True on success, error on failure
*/
function mlf_delete_font_family($family_id) {
$family = get_post($family_id);
if (!$family || $family->post_type !== 'wp_font_family') {
return new WP_Error('not_found', 'Font family not found');
}
// Verify it's one we imported
if (get_post_meta($family_id, '_mlf_imported', true) !== '1') {
return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin');
}
// Get font faces
$faces = get_children([
'post_parent' => $family_id,
'post_type' => 'wp_font_face',
]);
$font_dir = wp_get_font_dir();
// Delete font face files and posts
foreach ($faces as $face) {
$settings = json_decode($face->post_content, true);
if (isset($settings['src'])) {
// Convert file:. URL to path
$src = $settings['src'];
$src = str_replace('file:.', '', $src);
// Handle both URL and path formats
if (strpos($src, $font_dir['baseurl']) !== false) {
$file_path = str_replace($font_dir['baseurl'], $font_dir['path'], $src);
} else {
$file_path = $font_dir['path'] . '/' . basename($src);
}
// Validate path before deletion
if (mlf_validate_font_path($file_path) && file_exists($file_path)) {
wp_delete_file($file_path);
}
}
wp_delete_post($face->ID, true);
}
// Delete family post
wp_delete_post($family_id, true);
// Clear caches
delete_transient('wp_font_library_fonts');
return true;
}
```
### Listing Installed Fonts
```php
/**
* Get all fonts imported by this plugin.
*
* @return array Array of font data
*/
function mlf_get_imported_fonts() {
$fonts = get_posts([
'post_type' => 'wp_font_family',
'posts_per_page' => 100,
'post_status' => 'publish',
'meta_key' => '_mlf_imported',
'meta_value' => '1',
]);
$result = [];
foreach ($fonts as $font) {
$settings = json_decode($font->post_content, true);
// Get variants
$faces = get_children([
'post_parent' => $font->ID,
'post_type' => 'wp_font_face',
]);
$variants = [];
foreach ($faces as $face) {
$face_settings = json_decode($face->post_content, true);
$variants[] = [
'weight' => $face_settings['fontWeight'] ?? '400',
'style' => $face_settings['fontStyle'] ?? 'normal',
];
}
$result[] = [
'id' => $font->ID,
'name' => $settings['name'] ?? $font->post_title,
'slug' => $settings['slug'] ?? $font->post_name,
'variants' => $variants,
'import_date' => get_post_meta($font->ID, '_mlf_import_date', true),
];
}
return $result;
}
```
---
## Gutenberg FSE Integration
### How Fonts Appear in the Editor
Once registered via the Font Library API, fonts automatically appear in:
1. **Global Styles** → Typography → Font dropdown
2. **Block settings** → Typography → Font dropdown (when per-block typography is enabled)
No additional integration code is needed — WordPress handles this automatically.
### Theme.json Compatibility
**DO NOT:**
- Directly modify theme.json
- Filter `wp_theme_json_data_theme` to inject fonts (let Font Library handle it)
- Override global styles CSS directly
**DO:**
- Use the Font Library API (post types)
- Let WordPress generate CSS custom properties
- Trust the system
### CSS Custom Properties
When a font is applied in Global Styles, WordPress generates:
```css
body {
--wp--preset--font-family--open-sans: "Open Sans", sans-serif;
}
```
And applies it:
```css
body {
font-family: var(--wp--preset--font-family--open-sans);
}
```
Our plugin doesn't need to touch this — it's automatic.
---
## WooCommerce Compatibility
### HPOS (High-Performance Order Storage)
WooCommerce's HPOS moves order data from post meta to custom tables. We must declare compatibility.
```php
// Declare HPOS compatibility
add_action('before_woocommerce_init', function() {
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
});
```
### Why We're Compatible
Our plugin:
- Does NOT interact with orders at all
- Does NOT query wp_posts for order data
- Does NOT use wp_postmeta for order data
- Only uses wp_font_family and wp_font_face post types
We're inherently compatible because we don't touch WooCommerce data.
### Frontend Considerations
**DO NOT:**
- Override `.woocommerce` class styles
- Override `.wc-block-*` styles
- Target cart/checkout elements specifically
**DO:**
- Let WooCommerce elements inherit from body/heading fonts
- Let global styles cascade naturally
WooCommerce product titles, descriptions, and other text will naturally inherit the fonts set via Global Styles. No special handling needed.
---
## Wordfence Compatibility
### Potential Concerns
1. **Outbound requests** to Google Fonts during import
2. **AJAX endpoints** for admin actions
3. **File operations** in wp-content
### Why We're Compatible
**Outbound Requests:**
- Only occur during admin import (user-initiated action)
- Target well-known domains (fonts.googleapis.com, fonts.gstatic.com)
- Use standard `wp_remote_get()` which Wordfence allows
- No runtime external requests on frontend
**AJAX Endpoints:**
- Use standard `admin-ajax.php` (not custom endpoints)
- Include proper nonces
- Follow WordPress patterns that Wordfence expects
**File Operations:**
- Write only to `wp-content/fonts/` (WordPress default directory)
- Use WordPress Filesystem API
- Don't create executable files
### Testing with Wordfence
Test these scenarios with Wordfence active:
- [ ] Learning Mode: Import should succeed
- [ ] Enabled Mode: Import should succeed
- [ ] Rate Limiting: Admin AJAX not blocked
- [ ] Firewall: No false positives on font download
---
## LearnDash Compatibility
### Overview
LearnDash is a WordPress LMS that uses:
- Custom post types (courses, lessons, topics, quizzes)
- Custom templates
- Focus Mode (distraction-free learning)
### Why We're Compatible
Our plugin:
- Doesn't touch LearnDash post types
- Doesn't modify LearnDash templates
- Doesn't inject CSS on frontend
- Lets Global Styles cascade to LearnDash content
LearnDash course content, lesson text, and quiz questions will inherit the fonts set in Global Styles automatically.
### Focus Mode Consideration
LearnDash Focus Mode uses its own template. Fonts set via Global Styles will apply because:
- Focus Mode still loads theme.json styles
- CSS custom properties cascade to all content
- No special handling needed
**DO NOT:**
- Target `.learndash-*` classes specifically
- Override Focus Mode styles
- Inject custom CSS for LearnDash
---
## WPForms Compatibility
### Overview
WPForms renders forms via shortcodes and blocks. Form styling is handled by WPForms.
### Why We're Compatible
- Form labels and text inherit from body font
- We don't override `.wpforms-*` classes
- No JavaScript conflicts (we have no frontend JS)
### Consideration
If a user wants form text in a different font, they should use WPForms' built-in styling options or custom CSS — not expect our plugin to handle it.
---
## General Best Practices
### What We Hook Into
```php
// Admin menu
add_action('admin_menu', [$this, 'register_menu']);
// Admin assets (only on our page)
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
// AJAX handlers
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
// WooCommerce HPOS compatibility
add_action('before_woocommerce_init', [$this, 'declare_hpos_compatibility']);
```
### What We DON'T Hook Into
```php
// NO frontend hooks
// add_action('wp_enqueue_scripts', ...); // DON'T DO THIS
// add_action('wp_head', ...); // DON'T DO THIS
// add_action('wp_footer', ...); // DON'T DO THIS
// NO theme modification hooks
// add_filter('wp_theme_json_data_theme', ...); // Let Font Library handle it
// NO WooCommerce hooks
// add_action('woocommerce_*', ...); // DON'T DO THIS
// NO content filters
// add_filter('the_content', ...); // DON'T DO THIS
```
---
## Conflict Debugging
If a user reports a conflict, check:
### 1. Plugin Load Order
Our plugin should load with default priority. Check if another plugin is:
- Modifying the Font Library
- Overriding font CSS
- Filtering theme.json
### 2. CSS Specificity
If fonts aren't applying:
- Check browser DevTools for CSS cascade
- Look for more specific selectors overriding global styles
- Check for `!important` declarations
### 3. Cache Issues
Font changes not appearing:
- Clear browser cache
- Clear any caching plugins (WP Rocket, W3TC, etc.)
- Clear CDN cache if applicable
- WordPress transients: `delete_transient('wp_font_library_fonts')`
### 4. JavaScript Errors
If admin page isn't working:
- Check browser console for JS errors
- Look for conflicts with other admin scripts
- Verify jQuery isn't being dequeued
---
## Compatibility Checklist
Before releasing:
### WordPress Core
- [ ] Works on WordPress 6.5
- [ ] Works on WordPress 6.6+
- [ ] Font Library API integration works
- [ ] Fonts appear in Global Styles
- [ ] Fonts apply correctly on frontend
### WooCommerce
- [ ] HPOS compatibility declared
- [ ] No errors in WooCommerce status page
- [ ] Product pages render correctly with custom fonts
- [ ] Cart/Checkout not affected
### Wordfence
- [ ] Import works with firewall enabled
- [ ] No blocked requests
- [ ] No false positive security alerts
### LearnDash
- [ ] Course content inherits fonts
- [ ] Focus Mode renders correctly
- [ ] No JavaScript conflicts
### WPForms
- [ ] Forms render correctly
- [ ] No styling conflicts
### Other
- [ ] No PHP errors in debug.log
- [ ] No JavaScript errors in console
- [ ] Admin page loads correctly
- [ ] No memory issues during import

View file

@ -1,575 +0,0 @@
/**
* Maple Local Fonts - Admin Styles
*
* @package Maple_Local_Fonts
*/
/* Container */
.mlf-wrap {
max-width: 800px;
}
.mlf-wrap > h1 {
margin-bottom: 0;
}
.mlf-description {
color: #646970;
margin-top: 8px;
margin-bottom: 20px;
}
.mlf-container {
margin-top: 20px;
}
/* Sections */
.mlf-section {
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
padding: 20px 24px;
margin-bottom: 20px;
}
.mlf-section h2 {
margin-top: 0;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
font-size: 1.1em;
}
/* Form */
.mlf-form-row {
margin-bottom: 20px;
}
.mlf-form-row:last-of-type {
margin-bottom: 0;
}
.mlf-form-row > label {
display: block;
font-weight: 600;
margin-bottom: 8px;
}
.mlf-form-row input[type="text"] {
width: 100%;
max-width: 400px;
padding: 8px 12px;
font-size: 14px;
}
.mlf-form-row .description {
margin-top: 8px;
color: #646970;
font-style: italic;
font-size: 13px;
}
/* Search Input */
.mlf-search-wrapper {
display: flex;
align-items: center;
gap: 8px;
max-width: 500px;
}
.mlf-search-input {
flex: 1;
padding: 6px 12px !important;
font-size: 14px;
}
.mlf-search-btn {
flex-shrink: 0;
height: 36px;
padding: 0 16px !important;
}
.mlf-search-spinner {
flex-shrink: 0;
float: none !important;
margin: 0 !important;
}
/* Search Results */
.mlf-search-results {
margin-top: 12px;
border: 1px solid #c3c4c7;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
background: #fff;
}
.mlf-results-list {
padding: 0;
}
.mlf-result-item {
padding: 16px;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
transition: background-color 0.15s ease;
}
.mlf-result-item:last-child {
border-bottom: none;
}
.mlf-result-item:hover {
background: #f0f6fc;
}
.mlf-result-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.mlf-result-name {
font-weight: 600;
font-size: 15px;
color: #1d2327;
}
.mlf-result-category {
font-size: 12px;
color: #646970;
padding: 2px 8px;
background: #f0f0f1;
border-radius: 3px;
}
.mlf-result-badges {
display: flex;
gap: 6px;
}
.mlf-badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 3px;
font-weight: 500;
}
.mlf-badge-variable {
background: #d4edda;
color: #155724;
}
.mlf-result-preview {
font-size: 22px;
color: #50575e;
line-height: 1.3;
padding-top: 4px;
}
.mlf-no-results {
padding: 24px;
text-align: center;
color: #646970;
font-style: italic;
}
.mlf-search-error {
padding: 16px 24px;
text-align: center;
color: #8b6914;
background: #fcf9e8;
border-bottom: 1px solid #f0e6c8;
}
/* Selected Font */
.mlf-selected-font {
margin-top: 16px;
padding: 16px;
background: #f0f6fc;
border: 1px solid #c5d9ed;
border-radius: 4px;
}
.mlf-selected-font-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #c5d9ed;
}
.mlf-selected-label {
color: #646970;
font-size: 13px;
}
.mlf-selected-name {
font-weight: 600;
font-size: 16px;
color: #1d2327;
flex: 1;
}
.mlf-change-font {
background: none;
border: none;
color: #2271b1;
cursor: pointer;
font-size: 13px;
padding: 4px 8px;
}
.mlf-change-font:hover {
color: #135e96;
text-decoration: underline;
}
.mlf-italic-row {
margin-bottom: 16px;
}
/* Italic Toggle */
.mlf-italic-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
cursor: pointer;
font-weight: normal !important;
transition: background-color 0.15s ease;
}
.mlf-italic-toggle:hover {
background: #f6f7f7;
}
.mlf-italic-toggle input[type="checkbox"] {
margin: 0;
}
/* Submit Row */
.mlf-form-row-submit {
display: flex;
align-items: center;
gap: 12px;
margin-top: 0;
padding-top: 0;
border-top: none;
}
.mlf-selected-font .mlf-form-row-submit {
margin-top: 0;
}
.mlf-form-row-submit .spinner {
float: none;
margin: 0;
}
/* Messages */
.mlf-message {
padding: 12px 16px;
border-radius: 4px;
margin-top: 16px;
}
.mlf-message-success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.mlf-message-error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
/* Info Note */
.mlf-info-note {
display: flex;
align-items: flex-start;
gap: 8px;
margin-top: 20px;
padding: 12px 16px;
background: #f0f6fc;
border: 1px solid #c5d9ed;
border-radius: 4px;
font-size: 13px;
color: #1d2327;
}
.mlf-info-note .dashicons {
color: #2271b1;
font-size: 18px;
width: 18px;
height: 18px;
margin-top: 1px;
}
/* Section Header */
.mlf-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
.mlf-section-header h2 {
margin: 0;
padding: 0;
border: none;
}
.mlf-check-updates-btn {
flex-shrink: 0;
}
/* Font List */
.mlf-font-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.mlf-font-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f6f7f7;
border: 1px solid #dcdcde;
border-radius: 4px;
}
.mlf-font-info {
flex: 1;
}
.mlf-font-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 4px;
}
.mlf-font-name {
margin: 0;
font-size: 1.1em;
}
.mlf-update-badge {
display: inline-block;
padding: 2px 8px;
background: #fcf9e8;
border: 1px solid #d4b106;
border-radius: 3px;
color: #8b6914;
font-size: 11px;
font-weight: 500;
}
.mlf-font-variants {
margin: 0 0 4px 0;
color: #646970;
font-size: 0.9em;
}
.mlf-font-meta {
margin: 0;
color: #8c8f94;
font-size: 0.8em;
display: flex;
gap: 12px;
}
.mlf-font-version {
font-family: monospace;
background: #f0f0f1;
padding: 1px 6px;
border-radius: 3px;
}
.mlf-update-btn {
color: #2271b1;
border-color: #2271b1;
}
.mlf-update-btn:hover {
background: #2271b1;
color: #fff;
border-color: #2271b1;
}
.mlf-font-actions {
margin-left: 20px;
}
.mlf-delete-btn {
color: #b32d2e;
border-color: #b32d2e;
}
.mlf-delete-btn:hover {
background: #b32d2e;
color: #fff;
border-color: #b32d2e;
}
.mlf-delete-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* No Fonts Message */
.mlf-no-fonts {
color: #646970;
font-style: italic;
padding: 24px;
text-align: center;
background: #f6f7f7;
border-radius: 4px;
margin: 0;
}
/* Info Box */
.mlf-info-box {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: #f0f6fc;
border: 1px solid #c5d9ed;
border-radius: 4px;
}
.mlf-info-box .dashicons {
color: #2271b1;
font-size: 20px;
width: 20px;
height: 20px;
margin-top: 2px;
}
.mlf-info-box p {
margin: 0 0 8px 0;
}
.mlf-info-box p:last-child {
margin-bottom: 0;
}
.mlf-info-box a {
color: #2271b1;
text-decoration: none;
}
.mlf-info-box a:hover {
text-decoration: underline;
}
/* Classic Theme Info Box */
.mlf-info-box-classic {
flex-direction: row;
align-items: flex-start;
background: #fcf9e8;
border-color: #d4b106;
}
.mlf-info-box-classic .dashicons {
color: #9d7e05;
}
.mlf-classic-theme-info {
flex: 1;
}
.mlf-classic-theme-info p {
margin: 0 0 12px 0;
}
.mlf-classic-theme-info p:last-child {
margin-bottom: 0;
}
.mlf-classic-theme-info .description {
color: #646970;
font-style: italic;
margin-top: 12px;
}
.mlf-code-example {
background: #1d2327;
color: #f0f0f1;
padding: 12px 16px;
border-radius: 4px;
font-family: Consolas, Monaco, monospace;
font-size: 12px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
/* Settings Toggle */
.mlf-setting-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
cursor: pointer;
font-weight: normal !important;
transition: background-color 0.15s ease;
}
.mlf-setting-toggle:hover {
background: #f6f7f7;
}
.mlf-setting-toggle input[type="checkbox"] {
margin: 0;
}
.mlf-settings-section .description {
margin-top: 8px;
max-width: 600px;
}
/* Loading State */
.mlf-loading {
opacity: 0.6;
pointer-events: none;
}
/* Responsive */
@media screen and (max-width: 782px) {
.mlf-font-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.mlf-font-actions {
margin-left: 0;
}
.mlf-result-info {
flex-wrap: wrap;
}
.mlf-selected-font-header {
flex-wrap: wrap;
}
}

View file

@ -1,668 +0,0 @@
/**
* Maple Local Fonts - Admin JavaScript
*
* @package Maple_Local_Fonts
*/
(function($) {
'use strict';
var MLF = {
/**
* Search debounce timer.
*/
searchTimer: null,
/**
* Currently selected font.
*/
selectedFont: null,
/**
* Currently selected font version info.
*/
selectedFontVersion: null,
selectedFontLastModified: null,
/**
* Loaded font preview stylesheets.
*/
loadedFonts: {},
/**
* Initialize the admin functionality.
*/
init: function() {
this.bindEvents();
},
/**
* Bind event handlers.
*/
bindEvents: function() {
// Search button click
$('#mlf-search-btn').on('click', this.handleSearchClick.bind(this));
// Search on Enter key
$('#mlf-font-search').on('keypress', this.handleSearchKeypress.bind(this));
// Click outside to close search results
$(document).on('click', this.handleDocumentClick.bind(this));
// Prevent closing when clicking inside search area (but not on result items)
$('.mlf-import-section').on('click', function(e) {
// Don't stop propagation if clicking on a result item
if (!$(e.target).closest('.mlf-result-item').length) {
e.stopPropagation();
}
});
// Font selection from results - bind to results container
$('#mlf-search-results').on('click', '.mlf-result-item', this.handleFontSelect.bind(this));
// Change font button
$('#mlf-change-font').on('click', this.handleChangeFont.bind(this));
// Form submission
$('#mlf-import-form').on('submit', this.handleDownload.bind(this));
// Delete button clicks
$(document).on('click', '.mlf-delete-btn', this.handleDelete.bind(this));
// Check for updates button
$('#mlf-check-updates').on('click', this.handleCheckUpdates.bind(this));
// Update button clicks
$(document).on('click', '.mlf-update-btn', this.handleUpdateFont.bind(this));
},
/**
* Handle search button click.
*
* @param {Event} e Click event.
*/
handleSearchClick: function(e) {
e.preventDefault();
this.triggerSearch();
},
/**
* Handle Enter key in search input.
*
* @param {Event} e Keypress event.
*/
handleSearchKeypress: function(e) {
if (e.which === 13) {
e.preventDefault();
this.triggerSearch();
}
},
/**
* Trigger a search with the current input value.
*/
triggerSearch: function() {
var query = $('#mlf-font-search').val().trim();
if (query.length < 2) {
this.displayError(mapleLocalFontsData.strings.minChars || 'Please enter at least 2 characters.');
return;
}
this.performSearch(query);
},
/**
* Perform the search.
*
* @param {string} query Search query.
*/
performSearch: function(query) {
var $spinner = $('#mlf-search-spinner');
var $button = $('#mlf-search-btn');
// Show spinner, disable button
$spinner.addClass('is-active');
$button.prop('disabled', true);
// Send AJAX request
$.ajax({
url: mapleLocalFontsData.ajaxUrl,
type: 'POST',
data: {
action: 'mlf_search_fonts',
nonce: mapleLocalFontsData.searchNonce,
query: query
},
success: function(response) {
if (response.success && response.data && response.data.fonts) {
if (response.data.fonts.length > 0) {
MLF.displaySearchResults(response.data.fonts);
} else {
MLF.displayNoResults();
}
} else {
// Handle error response
var errorMsg = (response.data && response.data.message)
? response.data.message
: (mapleLocalFontsData.strings.error || 'An error occurred.');
MLF.displayError(errorMsg);
}
},
error: function(xhr) {
var errorMsg = mapleLocalFontsData.strings.error || 'An error occurred.';
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
errorMsg = xhr.responseJSON.data.message;
}
MLF.displayError(errorMsg);
},
complete: function() {
$spinner.removeClass('is-active');
$button.prop('disabled', false);
}
});
},
/**
* Display search results.
*
* @param {Array} fonts Array of font objects.
*/
displaySearchResults: function(fonts) {
var $results = $('#mlf-search-results');
var $list = $('#mlf-results-list');
if (!fonts || fonts.length === 0) {
this.displayNoResults();
return;
}
var html = '';
var previewText = mapleLocalFontsData.strings.previewText || 'Maple Fonts Preview';
var validFonts = 0;
fonts.forEach(function(font) {
// Skip fonts with missing family name
if (!font || !font.family) {
return;
}
var fontFamily = font.family;
var category = font.category || 'sans-serif';
var categoryLabel = MLF.getCategoryLabel(category);
var badges = [];
if (font.has_variable) {
badges.push('<span class="mlf-badge mlf-badge-variable">Variable</span>');
}
html += '<div class="mlf-result-item" data-font-family="' + MLF.escapeHtml(fontFamily) + '" data-font-version="' + MLF.escapeHtml(font.version || '') + '" data-font-modified="' + MLF.escapeHtml(font.lastModified || '') + '">';
html += ' <div class="mlf-result-info">';
html += ' <span class="mlf-result-name">' + MLF.escapeHtml(fontFamily) + '</span>';
html += ' <span class="mlf-result-category">' + MLF.escapeHtml(categoryLabel) + '</span>';
if (badges.length > 0) {
html += ' <span class="mlf-result-badges">' + badges.join('') + '</span>';
}
html += ' </div>';
html += ' <div class="mlf-result-preview" style="font-family: \'' + MLF.escapeHtml(fontFamily) + '\', ' + category + ';">';
html += MLF.escapeHtml(previewText);
html += ' </div>';
html += '</div>';
// Load font for preview
MLF.loadFontPreview(fontFamily);
validFonts++;
});
// If no valid fonts were found, show no results
if (validFonts === 0) {
this.displayNoResults();
return;
}
$list.html(html);
$results.show();
},
/**
* Display no results message.
*/
displayNoResults: function() {
var $results = $('#mlf-search-results');
var $list = $('#mlf-results-list');
var message = mapleLocalFontsData.strings.noResults || 'No fonts found. Try a different search term.';
$list.html('<div class="mlf-no-results">' + MLF.escapeHtml(message) + '</div>');
$results.show();
},
/**
* Display an error message in search results.
*
* @param {string} message Error message.
*/
displayError: function(message) {
var $results = $('#mlf-search-results');
var $list = $('#mlf-results-list');
$list.html('<div class="mlf-search-error">' + MLF.escapeHtml(message) + '</div>');
$results.show();
},
/**
* Hide search results.
*/
hideSearchResults: function() {
$('#mlf-search-results').hide();
},
/**
* Maximum number of font previews to load.
*/
maxLoadedFonts: 50,
/**
* Load a font for preview from Google Fonts.
*
* @param {string} fontFamily Font family name.
*/
loadFontPreview: function(fontFamily) {
// Skip if already loaded
if (this.loadedFonts[fontFamily]) {
return;
}
// Limit number of loaded fonts to prevent memory accumulation
if (Object.keys(this.loadedFonts).length >= this.maxLoadedFonts) {
return;
}
// Mark as loading
this.loadedFonts[fontFamily] = true;
// Create Google Fonts link (spaces become %20 which Google accepts)
var fontUrl = 'https://fonts.googleapis.com/css2?family=' +
encodeURIComponent(fontFamily) +
':wght@400&display=swap';
// Create and append link element
var $link = $('<link>', {
rel: 'stylesheet',
href: fontUrl
});
$('head').append($link);
},
/**
* Handle font selection.
*
* @param {Event} e Click event.
*/
handleFontSelect: function(e) {
var $item = $(e.currentTarget);
var fontFamily = $item.data('font-family');
// Store selected font and version info
this.selectedFont = fontFamily;
this.selectedFontVersion = $item.data('font-version') || '';
this.selectedFontLastModified = $item.data('font-modified') || '';
// Update UI
$('#mlf-font-name').val(fontFamily);
$('#mlf-selected-name').text(fontFamily);
// Hide search, show selected font panel
this.hideSearchResults();
$('#mlf-font-search').closest('.mlf-form-row').hide();
$('#mlf-selected-font').show();
// Clear search input
$('#mlf-font-search').val('');
},
/**
* Handle change font button.
*
* @param {Event} e Click event.
*/
handleChangeFont: function(e) {
e.preventDefault();
// Clear selection
this.selectedFont = null;
$('#mlf-font-name').val('');
// Show search, hide selected font panel
$('#mlf-selected-font').hide();
$('#mlf-font-search').closest('.mlf-form-row').show();
$('#mlf-font-search').focus();
},
/**
* Handle document click (close search results).
*
* @param {Event} e Click event.
*/
handleDocumentClick: function(e) {
if (!$(e.target).closest('.mlf-import-section').length) {
this.hideSearchResults();
}
},
/**
* Get human-readable category label.
*
* @param {string} category Category slug.
* @return {string} Category label.
*/
getCategoryLabel: function(category) {
var labels = {
'sans-serif': 'Sans Serif',
'serif': 'Serif',
'display': 'Display',
'handwriting': 'Handwriting',
'monospace': 'Monospace'
};
return labels[category] || category;
},
/**
* Escape HTML entities.
*
* @param {string} str String to escape.
* @return {string} Escaped string.
*/
escapeHtml: function(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
},
/**
* Handle font download form submission.
*
* @param {Event} e Form submit event.
*/
handleDownload: function(e) {
e.preventDefault();
var $form = $('#mlf-import-form');
var $button = $('#mlf-download-btn');
var $spinner = $('#mlf-spinner');
var $message = $('#mlf-message');
// Get form values
var fontName = $('#mlf-font-name').val().trim();
var includeItalic = $('#mlf-include-italic').is(':checked') ? '1' : '0';
// Validate
if (!fontName) {
this.showMessage($message, mapleLocalFontsData.strings.selectFont || 'Please select a font.', 'error');
return;
}
// Store original button text
if (!$button.data('original-text')) {
$button.data('original-text', $button.text());
}
// Disable form
$form.addClass('mlf-loading');
$button.prop('disabled', true).text(mapleLocalFontsData.strings.downloading);
$spinner.addClass('is-active');
$message.hide();
// Send AJAX request
$.ajax({
url: mapleLocalFontsData.ajaxUrl,
type: 'POST',
data: {
action: 'mlf_download_font',
nonce: mapleLocalFontsData.downloadNonce,
font_name: fontName,
include_italic: includeItalic,
font_version: this.selectedFontVersion || '',
font_last_modified: this.selectedFontLastModified || ''
},
success: function(response) {
if (response.success) {
MLF.showMessage($message, response.data.message, 'success');
// Reload page to show new font
setTimeout(function() {
window.location.reload();
}, 1500);
} else {
MLF.showMessage($message, response.data.message || mapleLocalFontsData.strings.error, 'error');
}
},
error: function(xhr) {
var message = mapleLocalFontsData.strings.error;
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
message = xhr.responseJSON.data.message;
}
MLF.showMessage($message, message, 'error');
},
complete: function() {
$form.removeClass('mlf-loading');
$button.prop('disabled', false).text($button.data('original-text'));
$spinner.removeClass('is-active');
}
});
},
/**
* Handle font deletion.
*
* @param {Event} e Click event.
*/
handleDelete: function(e) {
e.preventDefault();
var $button = $(e.currentTarget);
var fontId = $button.data('font-id');
var $fontItem = $button.closest('.mlf-font-item');
// Confirm deletion
if (!confirm(mapleLocalFontsData.strings.confirmDelete)) {
return;
}
// Store original button text
var originalText = $button.text();
// Disable button
$button.prop('disabled', true).text(mapleLocalFontsData.strings.deleting);
$fontItem.addClass('mlf-loading');
// Send AJAX request
$.ajax({
url: mapleLocalFontsData.ajaxUrl,
type: 'POST',
data: {
action: 'mlf_delete_font',
nonce: mapleLocalFontsData.deleteNonce,
font_id: fontId
},
success: function(response) {
if (response.success) {
// Remove font item with animation
$fontItem.slideUp(300, function() {
$(this).remove();
// Check if any fonts remain
if ($('.mlf-font-item').length === 0) {
$('#mlf-font-list').replaceWith(
'<p class="mlf-no-fonts">No fonts installed yet. Search and select a font above to get started.</p>'
);
}
});
} else {
alert(response.data.message || mapleLocalFontsData.strings.error);
$button.prop('disabled', false).text(originalText);
$fontItem.removeClass('mlf-loading');
}
},
error: function(xhr) {
var message = mapleLocalFontsData.strings.error;
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
message = xhr.responseJSON.data.message;
}
alert(message);
$button.prop('disabled', false).text(originalText);
$fontItem.removeClass('mlf-loading');
}
});
},
/**
* Show a message to the user.
*
* @param {jQuery} $element Message element.
* @param {string} message Message text.
* @param {string} type Message type (success or error).
*/
showMessage: function($element, message, type) {
$element
.removeClass('mlf-message-success mlf-message-error')
.addClass('mlf-message-' + type)
.text(message)
.show();
},
/**
* Handle check for updates button click.
*
* @param {Event} e Click event.
*/
handleCheckUpdates: function(e) {
e.preventDefault();
var $button = $('#mlf-check-updates');
var originalText = $button.text();
// Disable button and show checking state
$button.prop('disabled', true).text(mapleLocalFontsData.strings.checking || 'Checking...');
// Send AJAX request
$.ajax({
url: mapleLocalFontsData.ajaxUrl,
type: 'POST',
data: {
action: 'mlf_check_updates',
nonce: mapleLocalFontsData.checkUpdatesNonce
},
success: function(response) {
if (response.success) {
var updates = response.data.updates || {};
var updateCount = Object.keys(updates).length;
// Update UI for each font
$('.mlf-font-item').each(function() {
var $item = $(this);
var fontId = $item.data('font-id');
var $badge = $item.find('.mlf-update-badge');
var $updateBtn = $item.find('.mlf-update-btn');
if (updates[fontId]) {
// Show update available badge and button
$badge.show();
$updateBtn.show();
// Store the latest version info on the button
$updateBtn.data('latest-version', updates[fontId].latest_version);
} else {
// Hide update badge and button
$badge.hide();
$updateBtn.hide();
}
});
// Show summary message
if (updateCount > 0) {
var message = mapleLocalFontsData.strings.updatesFound || 'Updates available for %d font(s).';
alert(message.replace('%d', updateCount));
} else {
alert(mapleLocalFontsData.strings.noUpdates || 'All fonts are up to date.');
}
} else {
alert(response.data.message || mapleLocalFontsData.strings.error);
}
},
error: function(xhr) {
var message = mapleLocalFontsData.strings.error;
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
message = xhr.responseJSON.data.message;
}
alert(message);
},
complete: function() {
$button.prop('disabled', false).text(originalText);
}
});
},
/**
* Handle font update button click.
*
* @param {Event} e Click event.
*/
handleUpdateFont: function(e) {
e.preventDefault();
var $button = $(e.currentTarget);
var fontId = $button.data('font-id');
var fontName = $button.data('font-name');
var $fontItem = $button.closest('.mlf-font-item');
// Confirm update
if (!confirm('Update ' + fontName + ' to the latest version?')) {
return;
}
// Store original button text
var originalText = $button.text();
// Disable button and show updating state
$button.prop('disabled', true).text(mapleLocalFontsData.strings.updating || 'Updating...');
$fontItem.addClass('mlf-loading');
// Send AJAX request
$.ajax({
url: mapleLocalFontsData.ajaxUrl,
type: 'POST',
data: {
action: 'mlf_update_font',
nonce: mapleLocalFontsData.updateFontNonce,
font_id: fontId
},
success: function(response) {
if (response.success) {
// Reload page to show updated font
alert(response.data.message);
window.location.reload();
} else {
alert(response.data.message || mapleLocalFontsData.strings.error);
$button.prop('disabled', false).text(originalText);
$fontItem.removeClass('mlf-loading');
}
},
error: function(xhr) {
var message = mapleLocalFontsData.strings.error;
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
message = xhr.responseJSON.data.message;
}
alert(message);
$button.prop('disabled', false).text(originalText);
$fontItem.removeClass('mlf-loading');
}
});
}
};
// Initialize on document ready
$(document).ready(function() {
MLF.init();
});
})(jQuery);

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}

View file

@ -1,39 +0,0 @@
{
"name": "mapleopentech/maple-local-fonts",
"description": "Import Google Fonts to local storage for GDPR-compliant, privacy-friendly typography in WordPress.",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Maple Open Technologies",
"homepage": "https://mapleopentech.org"
}
],
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
"wp-coding-standards/wpcs": "^3.0",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"yoast/phpunit-polyfills": "^2.0"
},
"scripts": {
"test": "phpunit",
"phpcs": "phpcs",
"phpcbf": "phpcbf",
"compat": "phpcs -p --standard=PHPCompatibilityWP --runtime-set testVersion 7.4- --extensions=php --ignore=vendor,tests ."
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
},
"sort-packages": true
},
"autoload-dev": {
"psr-4": {
"MapleLocalFonts\\Tests\\": "tests/"
}
}
}

View file

@ -1,271 +0,0 @@
<?php
/**
* Admin Page for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Admin_Page
*
* Handles the admin settings page rendering.
*/
class MLF_Admin_Page {
/**
* Constructor.
*/
public function __construct() {
// Empty constructor - class is instantiated for rendering
}
/**
* Handle settings form submission.
*/
private function handle_settings_save() {
if (!isset($_POST['mlf_save_settings'])) {
return;
}
// Verify nonce
if (!isset($_POST['mlf_settings_nonce']) || !wp_verify_nonce($_POST['mlf_settings_nonce'], 'mlf_save_settings')) {
add_settings_error('mlf_settings', 'nonce_error', __('Security check failed.', 'maple-local-fonts'), 'error');
return;
}
// Verify capability
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
return;
}
// Save compatibility mode setting
$compatibility_mode = isset($_POST['mlf_compatibility_mode']) && $_POST['mlf_compatibility_mode'] === '1';
update_option('mlf_compatibility_mode', $compatibility_mode);
// Clear font caches to apply new URL format
delete_transient('mlf_imported_fonts_list');
if (class_exists('WP_Theme_JSON_Resolver')) {
WP_Theme_JSON_Resolver::clean_cached_data();
}
add_settings_error('mlf_settings', 'settings_saved', __('Settings saved.', 'maple-local-fonts'), 'success');
}
/**
* Render the admin page.
*/
public function render() {
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'maple-local-fonts'));
}
// Handle settings save
$this->handle_settings_save();
$registry = new MLF_Font_Registry();
$installed_fonts = $registry->get_imported_fonts();
?>
<div class="wrap mlf-wrap">
<h1><?php esc_html_e('Maple Fonts', 'maple-local-fonts'); ?></h1>
<?php settings_errors('mlf_settings'); ?>
<p class="mlf-description"><?php esc_html_e('Import Google Fonts to your local server for privacy-friendly, GDPR-compliant typography.', 'maple-local-fonts'); ?></p>
<div class="mlf-container">
<!-- Import Section -->
<div class="mlf-section mlf-import-section">
<h2><?php esc_html_e('Import from Google Fonts', 'maple-local-fonts'); ?></h2>
<form id="mlf-import-form" class="mlf-form">
<!-- Search Input -->
<div class="mlf-form-row">
<label for="mlf-font-search"><?php esc_html_e('Search Fonts', 'maple-local-fonts'); ?></label>
<div class="mlf-search-wrapper">
<input type="text"
id="mlf-font-search"
class="mlf-search-input"
placeholder="<?php esc_attr_e('Enter font name...', 'maple-local-fonts'); ?>"
autocomplete="off" />
<button type="button" class="button mlf-search-btn" id="mlf-search-btn">
<?php esc_html_e('Search', 'maple-local-fonts'); ?>
</button>
<span class="spinner mlf-search-spinner" id="mlf-search-spinner"></span>
</div>
<p class="description"><?php esc_html_e('Enter at least 2 characters and click Search.', 'maple-local-fonts'); ?></p>
</div>
<!-- Search Results -->
<div class="mlf-search-results" id="mlf-search-results" style="display: none;">
<div class="mlf-results-list" id="mlf-results-list"></div>
</div>
<!-- Selected Font (hidden until a font is selected) -->
<div class="mlf-selected-font" id="mlf-selected-font" style="display: none;">
<div class="mlf-selected-font-header">
<span class="mlf-selected-label"><?php esc_html_e('Selected Font:', 'maple-local-fonts'); ?></span>
<span class="mlf-selected-name" id="mlf-selected-name"></span>
<button type="button" class="mlf-change-font" id="mlf-change-font">
<?php esc_html_e('Change', 'maple-local-fonts'); ?>
</button>
</div>
<!-- Hidden input for font name -->
<input type="hidden" id="mlf-font-name" name="font_name" value="" />
<div class="mlf-form-row mlf-italic-row">
<label class="mlf-checkbox-label mlf-italic-toggle">
<input type="checkbox" name="include_italic" id="mlf-include-italic" value="1" checked />
<span><?php esc_html_e('Include Italic styles', 'maple-local-fonts'); ?></span>
</label>
<p class="description"><?php esc_html_e('Italic styles are useful for emphasized text. Uncheck to reduce download size.', 'maple-local-fonts'); ?></p>
</div>
<div class="mlf-form-row mlf-form-row-submit">
<button type="submit" class="button button-primary" id="mlf-download-btn">
<?php esc_html_e('Download & Install', 'maple-local-fonts'); ?>
</button>
<span class="spinner" id="mlf-spinner"></span>
</div>
</div>
<div id="mlf-message" class="mlf-message" style="display: none;"></div>
</form>
<div class="mlf-info-note">
<span class="dashicons dashicons-info-outline"></span>
<span><?php esc_html_e('Weights 300-900 (Light to Black) will be downloaded. Variable fonts are used when available, which include all weights in a single efficient file.', 'maple-local-fonts'); ?></span>
</div>
</div>
<!-- Installed Fonts Section -->
<div class="mlf-section mlf-installed-section">
<div class="mlf-section-header">
<h2><?php esc_html_e('Installed Fonts', 'maple-local-fonts'); ?></h2>
<?php if (!empty($installed_fonts)) : ?>
<button type="button" class="button mlf-check-updates-btn" id="mlf-check-updates">
<?php esc_html_e('Check for Updates', 'maple-local-fonts'); ?>
</button>
<?php endif; ?>
</div>
<?php if (empty($installed_fonts)) : ?>
<p class="mlf-no-fonts"><?php esc_html_e('No fonts installed yet. Search and select a font above to get started.', 'maple-local-fonts'); ?></p>
<?php else : ?>
<div id="mlf-font-list" class="mlf-font-list">
<?php foreach ($installed_fonts as $font) : ?>
<div class="mlf-font-item" data-font-id="<?php echo esc_attr($font['id']); ?>" data-font-name="<?php echo esc_attr($font['name']); ?>" data-font-version="<?php echo esc_attr($font['version']); ?>">
<div class="mlf-font-info">
<div class="mlf-font-header">
<h3 class="mlf-font-name"><?php echo esc_html($font['name']); ?></h3>
<span class="mlf-update-badge" style="display: none;"><?php esc_html_e('Update available', 'maple-local-fonts'); ?></span>
</div>
<p class="mlf-font-variants">
<?php
$variant_strings = [];
foreach ($font['variants'] as $variant) {
$variant_strings[] = sprintf('%s %s', $variant['weight'], $variant['style']);
}
echo esc_html(implode(', ', $variant_strings));
?>
</p>
<p class="mlf-font-meta">
<?php if (!empty($font['version'])) : ?>
<span class="mlf-font-version"><?php echo esc_html($font['version']); ?></span>
<?php endif; ?>
<?php if (!empty($font['last_modified'])) : ?>
<span class="mlf-font-modified"><?php
/* translators: %s: date */
printf(esc_html__('Updated: %s', 'maple-local-fonts'), esc_html($font['last_modified']));
?></span>
<?php endif; ?>
</p>
</div>
<div class="mlf-font-actions">
<button type="button" class="button mlf-update-btn" data-font-id="<?php echo esc_attr($font['id']); ?>" data-font-name="<?php echo esc_attr($font['name']); ?>" style="display: none;">
<?php esc_html_e('Update', 'maple-local-fonts'); ?>
</button>
<button type="button" class="button mlf-delete-btn" data-font-id="<?php echo esc_attr($font['id']); ?>">
<?php esc_html_e('Delete', 'maple-local-fonts'); ?>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Settings Section -->
<div class="mlf-section mlf-settings-section">
<h2><?php esc_html_e('Settings', 'maple-local-fonts'); ?></h2>
<form method="post" action="">
<?php wp_nonce_field('mlf_save_settings', 'mlf_settings_nonce'); ?>
<div class="mlf-form-row">
<label class="mlf-checkbox-label mlf-setting-toggle">
<input type="checkbox" name="mlf_compatibility_mode" value="1" <?php checked(get_option('mlf_compatibility_mode', true)); ?> />
<span><strong><?php esc_html_e('Compatibility Mode', 'maple-local-fonts'); ?></strong></span>
</label>
<p class="description">
<?php esc_html_e('Enable this if fonts are not loading on mobile devices or behind reverse proxies (Caddy, Nginx, Cloudflare). This serves fonts through WordPress for guaranteed header compatibility.', 'maple-local-fonts'); ?>
</p>
</div>
<div class="mlf-form-row mlf-form-row-submit">
<button type="submit" name="mlf_save_settings" class="button button-secondary">
<?php esc_html_e('Save Settings', 'maple-local-fonts'); ?>
</button>
</div>
</form>
</div>
<!-- Info Section -->
<div class="mlf-section mlf-info-section">
<?php if (wp_is_block_theme()) : ?>
<div class="mlf-info-box">
<span class="dashicons dashicons-editor-textcolor"></span>
<div>
<p><strong><?php esc_html_e('How to use your fonts', 'maple-local-fonts'); ?></strong></p>
<p>
<?php
printf(
/* translators: %s: link to WordPress Editor */
esc_html__('Go to %s to apply fonts to your site.', 'maple-local-fonts'),
'<a href="' . esc_url(admin_url('site-editor.php?path=%2Fwp_global_styles')) . '">' . esc_html__('Appearance → Editor → Styles → Typography', 'maple-local-fonts') . '</a>'
);
?>
</p>
</div>
</div>
<?php else : ?>
<div class="mlf-info-box mlf-info-box-classic">
<span class="dashicons dashicons-editor-textcolor"></span>
<div class="mlf-classic-theme-info">
<p><strong><?php esc_html_e('Classic Theme Detected', 'maple-local-fonts'); ?></strong></p>
<p><?php esc_html_e('Your theme does not support the Full Site Editor. To use imported fonts, add custom CSS to your theme:', 'maple-local-fonts'); ?></p>
<pre class="mlf-code-example">body {
font-family: "Open Sans", sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Open Sans", sans-serif;
}</pre>
<p class="description">
<?php
printf(
/* translators: %s: link to Customizer */
esc_html__('Add this CSS in %s or your theme\'s style.css file.', 'maple-local-fonts'),
'<a href="' . esc_url(admin_url('customize.php')) . '">' . esc_html__('Appearance → Customize → Additional CSS', 'maple-local-fonts') . '</a>'
);
?>
</p>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
}

View file

@ -1,372 +0,0 @@
<?php
/**
* AJAX Handler for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Ajax_Handler
*
* Handles all AJAX requests for font download and deletion.
*/
class MLF_Ajax_Handler {
/**
* Rate limiter instance.
*
* @var MLF_Rate_Limiter
*/
private $rate_limiter;
/**
* Constructor.
*/
public function __construct() {
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
add_action('wp_ajax_mlf_check_updates', [$this, 'handle_check_updates']);
add_action('wp_ajax_mlf_update_font', [$this, 'handle_update_font']);
// NEVER add wp_ajax_nopriv_ - admin only functionality
// Initialize rate limiter: 10 requests per minute
$this->rate_limiter = new MLF_Rate_Limiter(10, 60);
}
/**
* Handle font download AJAX request.
*/
public function handle_download() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_download_font', 'nonce', false)) {
wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403);
}
// 2. CAPABILITY CHECK
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403);
}
// 3. RATE LIMIT CHECK
if (!$this->rate_limiter->check_and_record('download')) {
wp_send_json_error([
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
], 429);
}
// 4. INPUT VALIDATION
// Validate font name
$font_name = isset($_POST['font_name']) ? sanitize_text_field(wp_unslash($_POST['font_name'])) : '';
if (empty($font_name)) {
wp_send_json_error(['message' => __('Font name is required.', 'maple-local-fonts')]);
}
// Strict allowlist pattern - alphanumeric, spaces, hyphens only
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
wp_send_json_error(['message' => __('Invalid font name: only letters, numbers, spaces, and hyphens allowed.', 'maple-local-fonts')]);
}
if (strlen($font_name) > 100) {
wp_send_json_error(['message' => __('Font name is too long.', 'maple-local-fonts')]);
}
// Validate include_italic (boolean)
$include_italic = isset($_POST['include_italic']) && $_POST['include_italic'] === '1';
// Get version info (optional)
$font_version = isset($_POST['font_version']) ? sanitize_text_field(wp_unslash($_POST['font_version'])) : '';
$font_last_modified = isset($_POST['font_last_modified']) ? sanitize_text_field(wp_unslash($_POST['font_last_modified'])) : '';
// 5. PROCESS REQUEST
try {
$downloader = new MLF_Font_Downloader();
$download_result = $downloader->download($font_name, $include_italic);
if (is_wp_error($download_result)) {
wp_send_json_error(['message' => $this->get_user_error_message($download_result)]);
}
// Register font with WordPress
$registry = new MLF_Font_Registry();
$result = $registry->register_font(
$download_result['font_name'],
$download_result['font_slug'],
$download_result['files'],
$font_version,
$font_last_modified
);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $this->get_user_error_message($result)]);
}
wp_send_json_success([
'message' => sprintf(
/* translators: %s: font name */
__('Successfully installed %s.', 'maple-local-fonts'),
esc_html($font_name)
),
'font_id' => $result,
]);
} catch (Exception $e) {
// Sanitize exception message before logging (defense in depth)
error_log('MLF Download Error: ' . sanitize_text_field($e->getMessage()));
wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
}
}
/**
* Handle font deletion AJAX request.
*/
public function handle_delete() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_delete_font', 'nonce', false)) {
wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403);
}
// 2. CAPABILITY CHECK
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403);
}
// 3. RATE LIMIT CHECK
if (!$this->rate_limiter->check_and_record('delete')) {
wp_send_json_error([
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
], 429);
}
// 4. INPUT VALIDATION
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
if ($font_id < 1) {
wp_send_json_error(['message' => __('Invalid font ID.', 'maple-local-fonts')]);
}
// Verify font exists and is a font family
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => __('Font not found.', 'maple-local-fonts')]);
}
// Verify it's one we imported (not a theme font)
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => __('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts')]);
}
// 5. PROCESS REQUEST
try {
$registry = new MLF_Font_Registry();
$result = $registry->delete_font($font_id);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $this->get_user_error_message($result)]);
}
wp_send_json_success(['message' => __('Font deleted successfully.', 'maple-local-fonts')]);
} catch (Exception $e) {
// Sanitize exception message before logging (defense in depth)
error_log('MLF Delete Error: ' . sanitize_text_field($e->getMessage()));
wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
}
}
/**
* Handle check for updates AJAX request.
*/
public function handle_check_updates() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_check_updates', 'nonce', false)) {
wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403);
}
// 2. CAPABILITY CHECK
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403);
}
// 3. RATE LIMIT CHECK
if (!$this->rate_limiter->check_and_record('check_updates')) {
wp_send_json_error([
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
], 429);
}
// 4. GET INSTALLED FONTS AND CHECK VERSIONS
try {
$registry = new MLF_Font_Registry();
$installed_fonts = $registry->get_imported_fonts();
if (empty($installed_fonts)) {
wp_send_json_success(['updates' => []]);
}
$font_search = new MLF_Font_Search();
$updates = [];
foreach ($installed_fonts as $font) {
$current_version = $font_search->get_font_version($font['name']);
if ($current_version && !empty($current_version['version'])) {
$installed_version = $font['version'] ?? '';
// Compare versions
if (!empty($installed_version) && $installed_version !== $current_version['version']) {
$updates[$font['id']] = [
'installed_version' => $installed_version,
'latest_version' => $current_version['version'],
'last_modified' => $current_version['lastModified'],
];
} elseif (empty($installed_version)) {
// No version stored, consider it needs update
$updates[$font['id']] = [
'installed_version' => '',
'latest_version' => $current_version['version'],
'last_modified' => $current_version['lastModified'],
];
}
}
}
wp_send_json_success(['updates' => $updates]);
} catch (Exception $e) {
error_log('MLF Check Updates Error: ' . sanitize_text_field($e->getMessage()));
wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
}
}
/**
* Handle font update (re-download) AJAX request.
*/
public function handle_update_font() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_update_font', 'nonce', false)) {
wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403);
}
// 2. CAPABILITY CHECK
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403);
}
// 3. RATE LIMIT CHECK
if (!$this->rate_limiter->check_and_record('update')) {
wp_send_json_error([
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
], 429);
}
// 4. INPUT VALIDATION
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
if ($font_id < 1) {
wp_send_json_error(['message' => __('Invalid font ID.', 'maple-local-fonts')]);
}
// Verify font exists and is one we imported
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => __('Font not found.', 'maple-local-fonts')]);
}
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => __('Cannot update fonts not imported by this plugin.', 'maple-local-fonts')]);
}
// Get font name from post content
$settings = json_decode($font->post_content, true);
$font_name = $settings['name'] ?? $font->post_title;
// 5. DELETE OLD FONT AND RE-DOWNLOAD
try {
$registry = new MLF_Font_Registry();
// Delete old font
$delete_result = $registry->delete_font($font_id);
if (is_wp_error($delete_result)) {
wp_send_json_error(['message' => $this->get_user_error_message($delete_result)]);
}
// Get latest version info
$font_search = new MLF_Font_Search();
$version_info = $font_search->get_font_version($font_name);
$font_version = $version_info['version'] ?? '';
$font_last_modified = $version_info['lastModified'] ?? '';
// Re-download font (include italic by default)
$downloader = new MLF_Font_Downloader();
$download_result = $downloader->download($font_name, true);
if (is_wp_error($download_result)) {
wp_send_json_error(['message' => $this->get_user_error_message($download_result)]);
}
// Register font with new version info
$result = $registry->register_font(
$download_result['font_name'],
$download_result['font_slug'],
$download_result['files'],
$font_version,
$font_last_modified
);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $this->get_user_error_message($result)]);
}
wp_send_json_success([
'message' => sprintf(
/* translators: %s: font name */
__('Successfully updated %s.', 'maple-local-fonts'),
esc_html($font_name)
),
'font_id' => $result,
'version' => $font_version,
]);
} catch (Exception $e) {
error_log('MLF Update Font Error: ' . sanitize_text_field($e->getMessage()));
wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
}
}
/**
* Convert internal error codes to user-friendly messages.
*
* @param WP_Error $error The error object.
* @return string User-friendly message.
*/
private function get_user_error_message($error) {
$code = $error->get_error_code();
$messages = [
'font_not_found' => __('Font not found on Google Fonts. Please check the spelling and try again.', 'maple-local-fonts'),
'font_exists' => __('This font is already installed.', 'maple-local-fonts'),
'request_failed' => __('Could not connect to Google Fonts. Please check your internet connection and try again.', 'maple-local-fonts'),
'http_error' => __('Google Fonts returned an error. Please try again later.', 'maple-local-fonts'),
'parse_failed' => __('Could not process the font data. The font may not be available.', 'maple-local-fonts'),
'download_failed' => __('Could not download the font files. Please try again.', 'maple-local-fonts'),
'write_failed' => __('Could not save font files. Please check that wp-content/fonts is writable.', 'maple-local-fonts'),
'mkdir_failed' => __('Could not create fonts directory. Please check file permissions.', 'maple-local-fonts'),
'invalid_path' => __('Invalid file path.', 'maple-local-fonts'),
'invalid_url' => __('Invalid font URL.', 'maple-local-fonts'),
'invalid_name' => __('Invalid font name.', 'maple-local-fonts'),
'not_found' => __('Font not found.', 'maple-local-fonts'),
'not_ours' => __('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts'),
'response_too_large' => __('The font data is too large to process.', 'maple-local-fonts'),
'file_too_large' => __('The font file is too large to download.', 'maple-local-fonts'),
'no_variable' => __('Variable font not available, trying static fonts...', 'maple-local-fonts'),
'no_fonts' => __('No font files found. The font may not support the requested styles.', 'maple-local-fonts'),
];
return $messages[$code] ?? __('An unexpected error occurred. Please try again.', 'maple-local-fonts');
}
}

View file

@ -1,709 +0,0 @@
<?php
/**
* Font Downloader for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Font_Downloader
*
* Handles downloading fonts from Google Fonts CSS2 API.
* Attempts variable fonts first, falls back to static fonts.
*/
class MLF_Font_Downloader {
/**
* User agent to send with requests (needed to get WOFF2 format).
*
* @var string
*/
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
/**
* Common font weights to request.
*
* Most fonts support at least 300-700. We also include 800-900 for fonts
* that support Black/Heavy weights.
*
* @var array
*/
private $all_weights = [300, 400, 500, 600, 700, 800, 900];
/**
* Download a font from Google Fonts.
*
* Attempts variable font first, falls back to static if not available.
*
* @param string $font_name Font family name.
* @param bool $include_italic Whether to include italic styles.
* @return array|WP_Error Download result or error.
*/
public function download($font_name, $include_italic = true) {
// Validate font name
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
return new WP_Error('invalid_name', 'Invalid font name');
}
if (strlen($font_name) > 100) {
return new WP_Error('invalid_name', 'Font name too long');
}
// Try variable font first
$result = $this->try_variable_font($font_name, $include_italic);
if (!is_wp_error($result)) {
return $result;
}
// Fall back to static fonts
return $this->download_static_fonts($font_name, $include_italic);
}
/**
* Attempt to download variable font.
*
* @param string $font_name Font family name.
* @param bool $include_italic Whether to include italic styles.
* @return array|WP_Error Download result or error.
*/
private function try_variable_font($font_name, $include_italic) {
$font_slug = sanitize_title($font_name);
$downloaded = [];
// Try to fetch variable font CSS (roman/upright)
$css = $this->fetch_variable_css($font_name, false);
if (is_wp_error($css)) {
return $css;
}
// Parse and download roman variable font
$roman_faces = $this->parse_variable_css($css, $font_name);
if (is_wp_error($roman_faces) || empty($roman_faces)) {
return new WP_Error('no_variable', 'Variable font not available');
}
// Download roman variable font file(s)
foreach ($roman_faces as $face) {
$result = $this->download_single_file(
$face['url'],
$font_slug,
$face['weight'],
'normal',
true // is_variable
);
if (!is_wp_error($result)) {
$downloaded[] = [
'path' => $result,
'weight' => $face['weight'],
'style' => 'normal',
'is_variable' => true,
];
}
}
// Try italic variable font if requested
if ($include_italic) {
$italic_css = $this->fetch_variable_css($font_name, true);
if (!is_wp_error($italic_css)) {
$italic_faces = $this->parse_variable_css($italic_css, $font_name);
if (!is_wp_error($italic_faces) && !empty($italic_faces)) {
foreach ($italic_faces as $face) {
$result = $this->download_single_file(
$face['url'],
$font_slug,
$face['weight'],
'italic',
true
);
if (!is_wp_error($result)) {
$downloaded[] = [
'path' => $result,
'weight' => $face['weight'],
'style' => 'italic',
'is_variable' => true,
];
}
}
}
}
}
if (empty($downloaded)) {
return new WP_Error('download_failed', 'Could not download variable font files');
}
return [
'font_name' => $font_name,
'font_slug' => $font_slug,
'files' => $downloaded,
'is_variable' => true,
];
}
/**
* Download static fonts (fallback when variable not available).
*
* Tries different weight combinations since fonts support varying weights.
*
* @param string $font_name Font family name.
* @param bool $include_italic Whether to include italic styles.
* @return array|WP_Error Download result or error.
*/
private function download_static_fonts($font_name, $include_italic) {
$styles = $include_italic ? ['normal', 'italic'] : ['normal'];
// Try different weight combinations - fonts support varying weights
$weight_sets = [
[300, 400, 500, 600, 700, 800, 900], // Full range
[300, 400, 500, 600, 700, 800], // Without 900
[300, 400, 500, 600, 700], // Common range
[400, 500, 600, 700], // Without light
[400, 700], // Just regular and bold
[400], // Single weight (display fonts)
];
$css = null;
foreach ($weight_sets as $weights) {
$css = $this->fetch_static_css($font_name, $weights, $styles);
if (!is_wp_error($css)) {
break;
}
// If error is not "font not found", stop trying
if ($css->get_error_code() !== 'font_not_found') {
return $css;
}
}
if (is_wp_error($css)) {
return $css;
}
// Parse CSS to get font face data
$font_faces = $this->parse_static_css($css, $font_name);
if (is_wp_error($font_faces)) {
return $font_faces;
}
// Download each font file
$font_slug = sanitize_title($font_name);
$downloaded = $this->download_files($font_faces, $font_slug);
if (is_wp_error($downloaded)) {
return $downloaded;
}
return [
'font_name' => $font_name,
'font_slug' => $font_slug,
'files' => $downloaded,
'is_variable' => false,
];
}
/**
* Fetch variable font CSS from Google Fonts API.
*
* Tries progressively smaller weight ranges until one works,
* since different fonts support different weight ranges.
*
* @param string $font_name Font family name.
* @param bool $italic Whether to fetch italic variant.
* @return string|WP_Error CSS content or error.
*/
private function fetch_variable_css($font_name, $italic = false) {
$family = str_replace(' ', '+', $font_name);
// Try different weight ranges - fonts support varying ranges
// Also try single weight 400 for fonts that only have one weight
$weight_ranges = ['300..900', '300..800', '300..700', '400..700', '400'];
foreach ($weight_ranges as $range) {
if ($italic) {
$url = "https://fonts.googleapis.com/css2?family={$family}:ital,wght@1,{$range}&display=swap";
} else {
$url = "https://fonts.googleapis.com/css2?family={$family}:wght@{$range}&display=swap";
}
$result = $this->fetch_css($url);
// If successful or error is not "font not found", return
if (!is_wp_error($result) || $result->get_error_code() !== 'font_not_found') {
return $result;
}
}
// All ranges failed
return new WP_Error('no_variable', 'Variable font not available');
}
/**
* Fetch static font CSS from Google Fonts API.
*
* @param string $font_name Font family name.
* @param array $weights Weights to fetch.
* @param array $styles Styles to fetch.
* @return string|WP_Error CSS content or error.
*/
private function fetch_static_css($font_name, $weights, $styles) {
$url = $this->build_static_url($font_name, $weights, $styles);
return $this->fetch_css($url);
}
/**
* Fetch CSS from a Google Fonts URL.
*
* @param string $url Google Fonts CSS URL.
* @return string|WP_Error CSS content or error.
*/
private function fetch_css($url) {
// Validate URL
if (!$this->is_valid_google_fonts_url($url)) {
return new WP_Error('invalid_url', 'Invalid Google Fonts URL');
}
$response = wp_remote_get($url, [
'timeout' => MLF_REQUEST_TIMEOUT,
'sslverify' => true,
'user-agent' => $this->user_agent,
]);
if (is_wp_error($response)) {
return new WP_Error('request_failed', $response->get_error_message());
}
$status = wp_remote_retrieve_response_code($response);
if ($status === 400) {
return new WP_Error('font_not_found', 'Font not found');
}
if ($status !== 200) {
return new WP_Error('http_error', 'HTTP ' . $status);
}
$css = wp_remote_retrieve_body($response);
if (empty($css)) {
return new WP_Error('empty_response', 'Empty response from Google Fonts');
}
// Check CSS response size
$max_size = defined('MLF_MAX_CSS_SIZE') ? MLF_MAX_CSS_SIZE : 512 * 1024;
if (strlen($css) > $max_size) {
return new WP_Error('response_too_large', 'CSS response exceeds maximum size limit');
}
// Verify we got WOFF2
if (strpos($css, '.woff2)') === false) {
return new WP_Error('wrong_format', 'Did not receive WOFF2 format');
}
return $css;
}
/**
* Build static font URL.
*
* @param string $font_name Font family name.
* @param array $weights Array of weights.
* @param array $styles Array of styles.
* @return string Google Fonts CSS2 URL.
*/
private function build_static_url($font_name, $weights, $styles) {
$family = str_replace(' ', '+', $font_name);
sort($weights);
$has_italic = in_array('italic', $styles, true);
$has_normal = in_array('normal', $styles, true);
if ($has_normal && !$has_italic) {
$wght = implode(';', $weights);
return "https://fonts.googleapis.com/css2?family={$family}:wght@{$wght}&display=swap";
}
$variations = [];
foreach ($weights as $weight) {
if ($has_normal) {
$variations[] = "0,{$weight}";
}
if ($has_italic) {
$variations[] = "1,{$weight}";
}
}
$variation_string = implode(';', $variations);
return "https://fonts.googleapis.com/css2?family={$family}:ital,wght@{$variation_string}&display=swap";
}
/**
* Parse variable font CSS.
*
* @param string $css CSS content.
* @param string $font_name Expected font family name.
* @return array|WP_Error Array of font face data or error.
*/
private function parse_variable_css($css, $font_name) {
$font_faces = [];
// Match all @font-face blocks
$pattern = '/@font-face\s*\{([^}]+)\}/s';
if (!preg_match_all($pattern, $css, $matches)) {
return new WP_Error('parse_failed', 'No @font-face rules found');
}
foreach ($matches[1] as $block) {
$face_data = $this->parse_font_face_block($block, true);
if (is_wp_error($face_data)) {
continue;
}
// Verify font family matches
if (strcasecmp($face_data['family'], $font_name) !== 0) {
continue;
}
// For variable fonts, prefer latin subset
$key = $face_data['weight'] . '-' . $face_data['style'];
$is_latin = $this->is_latin_subset($face_data['unicode_range']);
if (!isset($font_faces[$key]) || $is_latin) {
$font_faces[$key] = $face_data;
}
}
return array_values($font_faces);
}
/**
* Parse static font CSS.
*
* @param string $css CSS content.
* @param string $font_name Expected font family name.
* @return array|WP_Error Array of font face data or error.
*/
private function parse_static_css($css, $font_name) {
$font_faces = [];
$pattern = '/@font-face\s*\{([^}]+)\}/s';
if (!preg_match_all($pattern, $css, $matches)) {
return new WP_Error('parse_failed', 'No @font-face rules found');
}
foreach ($matches[1] as $block) {
$face_data = $this->parse_font_face_block($block, false);
if (is_wp_error($face_data)) {
continue;
}
if (strcasecmp($face_data['family'], $font_name) !== 0) {
continue;
}
$key = $face_data['weight'] . '-' . $face_data['style'];
$is_latin = $this->is_latin_subset($face_data['unicode_range']);
if (!isset($font_faces[$key]) || $is_latin) {
$font_faces[$key] = $face_data;
}
}
if (empty($font_faces)) {
return new WP_Error('no_fonts', 'No valid font faces found');
}
// Limit number of font faces
$max_faces = defined('MLF_MAX_FONT_FACES') ? MLF_MAX_FONT_FACES : 20;
$result = array_values($font_faces);
if (count($result) > $max_faces) {
$result = array_slice($result, 0, $max_faces);
}
return $result;
}
/**
* Parse a single @font-face block.
*
* @param string $block Content inside @font-face { }.
* @param bool $is_variable Whether this is a variable font.
* @return array|WP_Error Parsed data or error.
*/
private function parse_font_face_block($block, $is_variable = false) {
$data = [];
// Extract font-family
if (preg_match('/font-family:\s*[\'"]?([^;\'"]+)[\'"]?;/i', $block, $m)) {
$data['family'] = trim($m[1]);
} else {
return new WP_Error('missing_family', 'Missing font-family');
}
// Extract font-weight (can be single value or range for variable)
if (preg_match('/font-weight:\s*(\d+(?:\s+\d+)?);/i', $block, $m)) {
$data['weight'] = trim($m[1]);
} else {
return new WP_Error('missing_weight', 'Missing font-weight');
}
// Extract font-style
if (preg_match('/font-style:\s*(\w+);/i', $block, $m)) {
$data['style'] = $m[1];
} else {
$data['style'] = 'normal';
}
// Extract src URL - MUST be fonts.gstatic.com
if (preg_match('/src:\s*url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/i', $block, $m)) {
$data['url'] = $m[1];
} else {
return new WP_Error('missing_src', 'Missing or invalid src URL');
}
// Extract unicode-range
if (preg_match('/unicode-range:\s*([^;]+);/i', $block, $m)) {
$data['unicode_range'] = trim($m[1]);
} else {
$data['unicode_range'] = '';
}
return $data;
}
/**
* Check if unicode-range indicates latin subset.
*
* @param string $range Unicode range string.
* @return bool True if appears to be latin subset.
*/
private function is_latin_subset($range) {
if (empty($range)) {
return true;
}
if (preg_match('/U\+0000/', $range) && !preg_match('/^U\+0100/', $range)) {
return true;
}
return false;
}
/**
* Download all font files.
*
* @param array $font_faces Array of font face data.
* @param string $font_slug Font slug for filename.
* @return array|WP_Error Array of downloaded file info or error.
*/
private function download_files($font_faces, $font_slug) {
$downloaded = [];
$errors = [];
foreach ($font_faces as $face) {
$result = $this->download_single_file(
$face['url'],
$font_slug,
$face['weight'],
$face['style'],
false
);
if (is_wp_error($result)) {
$errors[] = $result->get_error_message();
continue;
}
$downloaded[] = [
'path' => $result,
'weight' => $face['weight'],
'style' => $face['style'],
'is_variable' => false,
];
}
if (empty($downloaded)) {
return new WP_Error(
'download_failed',
'Could not download any font files: ' . implode(', ', $errors)
);
}
return $downloaded;
}
/**
* Download a single WOFF2 file.
*
* @param string $url Google Fonts static URL.
* @param string $font_slug Font slug for filename.
* @param string $weight Font weight (single or range).
* @param string $style Font style.
* @param bool $is_variable Whether this is a variable font.
* @return string|WP_Error Local file path or error.
*/
private function download_single_file($url, $font_slug, $weight, $style, $is_variable = false) {
if (!$this->is_valid_google_fonts_url($url)) {
return new WP_Error('invalid_url', 'URL is not from Google Fonts');
}
// Build filename
$weight_slug = str_replace(' ', '-', $weight);
if ($is_variable) {
$filename = sprintf('%s_%s_variable.woff2', $font_slug, $style);
} else {
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight_slug);
}
$filename = sanitize_file_name($filename);
$filename = $this->sanitize_font_filename($filename);
if ($filename === false) {
return new WP_Error('invalid_filename', 'Invalid filename');
}
$font_dir = wp_get_font_dir();
$destination = trailingslashit($font_dir['path']) . $filename;
if (!$this->validate_font_path($destination)) {
return new WP_Error('invalid_path', 'Invalid destination path');
}
if (!wp_mkdir_p($font_dir['path'])) {
return new WP_Error('mkdir_failed', 'Could not create fonts directory');
}
$response = wp_remote_get($url, [
'timeout' => MLF_REQUEST_TIMEOUT,
'sslverify' => true,
'user-agent' => $this->user_agent,
]);
if (is_wp_error($response)) {
return new WP_Error('download_failed', 'Failed to download: ' . $response->get_error_message());
}
$status = wp_remote_retrieve_response_code($response);
if ($status !== 200) {
return new WP_Error('http_error', 'Download returned HTTP ' . $status);
}
$content = wp_remote_retrieve_body($response);
if (empty($content)) {
return new WP_Error('empty_file', 'Downloaded file is empty');
}
$max_size = defined('MLF_MAX_FONT_FILE_SIZE') ? MLF_MAX_FONT_FILE_SIZE : 5 * 1024 * 1024;
if (strlen($content) > $max_size) {
return new WP_Error('file_too_large', 'Font file exceeds maximum size');
}
// Verify WOFF2 magic bytes
if (substr($content, 0, 4) !== 'wOF2') {
return new WP_Error('invalid_format', 'Downloaded file is not valid WOFF2');
}
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) {
return new WP_Error('write_failed', 'Could not write font file');
}
return $destination;
}
/**
* Validate Google Fonts URL.
*
* @param string $url URL to validate.
* @return bool True if valid.
*/
private function is_valid_google_fonts_url($url) {
$parsed = wp_parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
$allowed_hosts = [
'fonts.googleapis.com',
'fonts.gstatic.com',
];
return in_array($parsed['host'], $allowed_hosts, true);
}
/**
* Sanitize font filename.
*
* @param string $filename Filename to sanitize.
* @return string|false Sanitized filename or false.
*/
private function sanitize_font_filename($filename) {
$filename = sanitize_file_name($filename);
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') {
return false;
}
if ($filename !== basename($filename)) {
return false;
}
if (strlen($filename) > 200) {
return false;
}
return $filename;
}
/**
* Validate font path is within fonts directory.
*
* @param string $path Path to validate.
* @return bool True if valid.
*/
private function validate_font_path($path) {
$font_dir = wp_get_font_dir();
$fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));
$real_path = realpath($path);
if ($real_path === false) {
$dir = dirname($path);
$real_dir = realpath($dir);
if ($real_dir === false) {
$parent_dir = dirname($dir);
$real_parent = realpath($parent_dir);
if ($real_parent === false) {
return false;
}
$real_path = wp_normalize_path($real_parent . '/' . basename($dir) . '/' . basename($path));
} else {
$real_path = wp_normalize_path($real_dir . '/' . basename($path));
}
} else {
$real_path = wp_normalize_path($real_path);
}
return strpos($real_path, $fonts_path) === 0;
}
}

View file

@ -1,502 +0,0 @@
<?php
/**
* Font Registry for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Font_Registry
*
* Handles registering fonts with WordPress Font Library API.
*/
class MLF_Font_Registry {
/**
* Register a font family with WordPress Font Library.
*
* @param string $font_name Display name (e.g., "Open Sans").
* @param string $font_slug Slug (e.g., "open-sans").
* @param array $files Array of downloaded file data.
* @param string $font_version Google Fonts version (e.g., "v35").
* @param string $last_modified Google Fonts last modified date.
* @return int|WP_Error Font family post ID or error.
*/
public function register_font($font_name, $font_slug, $files, $font_version = '', $last_modified = '') {
// Check if font already exists
$existing = get_posts([
'post_type' => 'wp_font_family',
'name' => $font_slug,
'posts_per_page' => 1,
'post_status' => 'any',
]);
if (!empty($existing)) {
return new WP_Error('font_exists', 'Font family already installed');
}
// Get font directory info
$font_dir = wp_get_font_dir();
// Build font face array for WordPress
$font_faces = [];
foreach ($files as $file) {
$filename = basename($file['path']);
// Determine if this is a variable font (weight is a range like "100 900")
$is_variable = isset($file['is_variable']) && $file['is_variable'];
$weight = $file['weight'];
$face_data = [
'fontFamily' => $font_name,
'fontStyle' => $file['style'],
'fontWeight' => $weight,
'src' => 'file:./' . $filename,
];
$font_faces[] = $face_data;
}
// Determine font category for fallback
// Default to sans-serif, but could be enhanced to detect from Google Fonts metadata
$fallback = 'sans-serif';
// Build font family settings (this is what Gutenberg reads)
$font_family_settings = [
'name' => $font_name,
'slug' => $font_slug,
'fontFamily' => "'{$font_name}', {$fallback}",
'fontFace' => $font_faces,
];
// Create font family post
$family_id = wp_insert_post([
'post_type' => 'wp_font_family',
'post_title' => $font_name,
'post_name' => $font_slug,
'post_status' => 'publish',
'post_content' => wp_json_encode($font_family_settings),
], true);
if (is_wp_error($family_id)) {
return $family_id;
}
// Mark as imported by our plugin (for identification)
update_post_meta($family_id, '_mlf_imported', '1');
update_post_meta($family_id, '_mlf_import_date', current_time('mysql'));
// Store version info for update checking
if (!empty($font_version)) {
update_post_meta($family_id, '_mlf_font_version', $font_version);
}
if (!empty($last_modified)) {
update_post_meta($family_id, '_mlf_font_last_modified', $last_modified);
}
// Create font face posts (children) - WordPress also reads these
foreach ($files as $file) {
$filename = basename($file['path']);
$weight = $file['weight'];
$face_settings = [
'fontFamily' => $font_name,
'fontWeight' => $weight,
'fontStyle' => $file['style'],
'src' => 'file:./' . $filename,
];
wp_insert_post([
'post_type' => 'wp_font_face',
'post_parent' => $family_id,
'post_title' => sprintf('%s %s %s', $font_name, $weight, $file['style']),
'post_name' => sanitize_title(sprintf('%s-%s-%s', $font_slug, $weight, $file['style'])),
'post_status' => 'publish',
'post_content' => wp_json_encode($face_settings),
]);
}
// Clear all font-related caches
$this->clear_font_caches();
return $family_id;
}
/**
* Delete a font family and its files.
*
* @param int $family_id Font family post ID.
* @return bool|WP_Error True on success, error on failure.
*/
public function delete_font($family_id) {
$family = get_post($family_id);
if (!$family || $family->post_type !== 'wp_font_family') {
return new WP_Error('not_found', 'Font family not found');
}
// Verify it's one we imported
if (get_post_meta($family_id, '_mlf_imported', true) !== '1') {
return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin');
}
// Get font info before deletion for cleanup
$settings = json_decode($family->post_content, true);
$font_slug = $settings['slug'] ?? $family->post_name;
// Get font faces (children)
$faces = get_children([
'post_parent' => $family_id,
'post_type' => 'wp_font_face',
]);
$font_dir = wp_get_font_dir();
// Delete font face files and posts
foreach ($faces as $face) {
$face_settings = json_decode($face->post_content, true);
if (isset($face_settings['src'])) {
// Convert file:. URL to path
$src = $face_settings['src'];
$src = str_replace('file:./', '', $src);
$file_path = trailingslashit($font_dir['path']) . basename($src);
// Validate path and extension before deletion
if ($this->validate_font_path($file_path)
&& pathinfo($file_path, PATHINFO_EXTENSION) === 'woff2'
&& file_exists($file_path)) {
wp_delete_file($file_path);
}
}
wp_delete_post($face->ID, true);
}
// Delete family post
wp_delete_post($family_id, true);
// Clean up global styles references to this font
$this->remove_font_from_global_styles($font_slug);
// Clear all font-related caches
$this->clear_font_caches();
return true;
}
/**
* Remove references to a deleted font from global styles.
*
* This prevents block errors when a font is deleted but still referenced.
*
* @param string $font_slug The font slug to remove.
*/
private function remove_font_from_global_styles($font_slug) {
// Get the global styles post for the current theme
$global_styles = get_posts([
'post_type' => 'wp_global_styles',
'posts_per_page' => 1,
'post_status' => 'publish',
'tax_query' => [
[
'taxonomy' => 'wp_theme',
'field' => 'name',
'terms' => get_stylesheet(),
],
],
]);
if (empty($global_styles)) {
return;
}
$global_styles_post = $global_styles[0];
$content = json_decode($global_styles_post->post_content, true);
if (empty($content)) {
return;
}
$modified = false;
// Helper function to recursively remove font references
$remove_font_refs = function (&$data) use ($font_slug, &$modified, &$remove_font_refs) {
if (!is_array($data)) {
return;
}
foreach ($data as $key => &$value) {
// Check for fontFamily references using the slug pattern
if ($key === 'fontFamily' && is_string($value)) {
// WordPress uses format like: var(--wp--preset--font-family--font-slug)
if (strpos($value, '--font-family--' . $font_slug) !== false) {
unset($data[$key]);
$modified = true;
}
}
// Check for typography.fontFamily in element/block styles
if (is_array($value)) {
$remove_font_refs($value);
}
}
};
$remove_font_refs($content);
if ($modified) {
wp_update_post([
'ID' => $global_styles_post->ID,
'post_content' => wp_json_encode($content),
]);
}
}
/**
* Clear all font-related caches.
*/
private function clear_font_caches() {
// Clear WordPress Font Library cache
delete_transient('wp_font_library_fonts');
// Clear our plugin's cache
delete_transient('mlf_imported_fonts_list');
// Clear global settings cache (used by Gutenberg)
wp_cache_delete('wp_get_global_settings', 'theme_json');
wp_cache_delete('wp_get_global_stylesheet', 'theme_json');
// Clear theme.json related caches
delete_transient('global_styles');
delete_transient('global_styles_' . get_stylesheet());
// Clear WP_Theme_JSON caches
if (class_exists('WP_Theme_JSON_Resolver')) {
WP_Theme_JSON_Resolver::clean_cached_data();
}
// Clear object cache for post queries
wp_cache_flush_group('posts');
// Clear any theme mods cache
delete_transient('theme_mods_' . get_stylesheet());
// Trigger action for other plugins/themes that might cache fonts
do_action('mlf_fonts_cache_cleared');
}
/**
* Get all fonts imported by this plugin.
*
* Uses optimized queries to avoid N+1 pattern.
*
* @return array Array of font data.
*/
public function get_imported_fonts() {
// Check transient cache first
$cached = get_transient('mlf_imported_fonts_list');
if ($cached !== false) {
return $cached;
}
$fonts = get_posts([
'post_type' => 'wp_font_family',
'posts_per_page' => 100,
'post_status' => 'publish',
'meta_key' => '_mlf_imported',
'meta_value' => '1',
]);
if (empty($fonts)) {
set_transient('mlf_imported_fonts_list', [], 5 * MINUTE_IN_SECONDS);
return [];
}
// Collect all font IDs for batch query
$font_ids = wp_list_pluck($fonts, 'ID');
// Single query to get ALL font faces for ALL fonts (fixes N+1)
$all_faces = get_posts([
'post_type' => 'wp_font_face',
'posts_per_page' => 1000,
'post_status' => 'publish',
'post_parent__in' => $font_ids,
]);
// Group faces by parent font ID
$faces_by_font = [];
foreach ($all_faces as $face) {
$parent_id = $face->post_parent;
if (!isset($faces_by_font[$parent_id])) {
$faces_by_font[$parent_id] = [];
}
$faces_by_font[$parent_id][] = $face;
}
// Batch get all metadata
$import_dates = [];
$font_versions = [];
$font_last_modified = [];
foreach ($font_ids as $font_id) {
$import_dates[$font_id] = get_post_meta($font_id, '_mlf_import_date', true);
$font_versions[$font_id] = get_post_meta($font_id, '_mlf_font_version', true);
$font_last_modified[$font_id] = get_post_meta($font_id, '_mlf_font_last_modified', true);
}
$result = [];
foreach ($fonts as $font) {
$settings = json_decode($font->post_content, true);
// Get variants from pre-fetched data
$faces = $faces_by_font[$font->ID] ?? [];
$variants = [];
foreach ($faces as $face) {
$face_settings = json_decode($face->post_content, true);
$variants[] = [
'weight' => $face_settings['fontWeight'] ?? '400',
'style' => $face_settings['fontStyle'] ?? 'normal',
];
}
// Sort variants by weight then style
usort($variants, function($a, $b) {
// Handle weight ranges (variable fonts)
$a_weight = is_numeric($a['weight']) ? intval($a['weight']) : intval(explode(' ', $a['weight'])[0]);
$b_weight = is_numeric($b['weight']) ? intval($b['weight']) : intval(explode(' ', $b['weight'])[0]);
$weight_cmp = $a_weight - $b_weight;
if ($weight_cmp !== 0) {
return $weight_cmp;
}
return strcmp($a['style'], $b['style']);
});
$result[] = [
'id' => $font->ID,
'name' => $settings['name'] ?? $font->post_title,
'slug' => $settings['slug'] ?? $font->post_name,
'variants' => $variants,
'import_date' => $import_dates[$font->ID] ?? '',
'version' => $font_versions[$font->ID] ?? '',
'last_modified' => $font_last_modified[$font->ID] ?? '',
];
}
// Cache for 5 minutes
set_transient('mlf_imported_fonts_list', $result, 5 * MINUTE_IN_SECONDS);
return $result;
}
/**
* Get all fonts imported by this plugin, including actual filenames.
*
* This method includes the actual filename stored in the database,
* which is needed to correctly reference variable vs static font files.
*
* @return array Array of font data with filenames.
*/
public function get_imported_fonts_with_src() {
$fonts = get_posts([
'post_type' => 'wp_font_family',
'posts_per_page' => 100,
'post_status' => 'publish',
'meta_key' => '_mlf_imported',
'meta_value' => '1',
]);
if (empty($fonts)) {
return [];
}
// Collect all font IDs for batch query
$font_ids = wp_list_pluck($fonts, 'ID');
// Single query to get ALL font faces for ALL fonts
$all_faces = get_posts([
'post_type' => 'wp_font_face',
'posts_per_page' => 1000,
'post_status' => 'publish',
'post_parent__in' => $font_ids,
]);
// Group faces by parent font ID
$faces_by_font = [];
foreach ($all_faces as $face) {
$parent_id = $face->post_parent;
if (!isset($faces_by_font[$parent_id])) {
$faces_by_font[$parent_id] = [];
}
$faces_by_font[$parent_id][] = $face;
}
$result = [];
foreach ($fonts as $font) {
$settings = json_decode($font->post_content, true);
// Get variants from pre-fetched data
$faces = $faces_by_font[$font->ID] ?? [];
$variants = [];
foreach ($faces as $face) {
$face_settings = json_decode($face->post_content, true);
// Extract filename from src (format: "file:./filename.woff2")
$src = $face_settings['src'] ?? '';
$filename = str_replace('file:./', '', $src);
$variants[] = [
'weight' => $face_settings['fontWeight'] ?? '400',
'style' => $face_settings['fontStyle'] ?? 'normal',
'filename' => $filename,
];
}
$result[] = [
'id' => $font->ID,
'name' => $settings['name'] ?? $font->post_title,
'slug' => $settings['slug'] ?? $font->post_name,
'variants' => $variants,
];
}
return $result;
}
/**
* Validate that a path is within the WordPress fonts directory.
*
* @param string $path Full path to validate.
* @return bool True if path is safe, false otherwise.
*/
private function validate_font_path($path) {
$font_dir = wp_get_font_dir();
$fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));
// Resolve to real path (handles ../ etc)
$real_path = realpath($path);
// If realpath fails, file doesn't exist yet - validate the directory
if ($real_path === false) {
$dir = dirname($path);
$real_dir = realpath($dir);
if ($real_dir === false) {
return false;
}
$real_path = wp_normalize_path($real_dir . '/' . basename($path));
} else {
$real_path = wp_normalize_path($real_path);
}
// Must be within fonts directory
return strpos($real_path, $fonts_path) === 0;
}
}

View file

@ -1,298 +0,0 @@
<?php
/**
* Font Search for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Font_Search
*
* Handles searching Google Fonts metadata.
*/
class MLF_Font_Search {
/**
* Cache key for fonts metadata.
*
* @var string
*/
const CACHE_KEY = 'mlf_google_fonts_metadata';
/**
* Cache duration in seconds (24 hours).
*
* @var int
*/
const CACHE_DURATION = DAY_IN_SECONDS;
/**
* Google Fonts metadata URL.
*
* @var string
*/
const METADATA_URL = 'https://fonts.google.com/metadata/fonts';
/**
* Maximum metadata response size (10MB).
*
* Google Fonts metadata for 1500+ fonts can be 5-8MB.
*
* @var int
*/
const MAX_METADATA_SIZE = 10 * 1024 * 1024;
/**
* Rate limiter instance.
*
* @var MLF_Rate_Limiter
*/
private $rate_limiter;
/**
* Constructor.
*/
public function __construct() {
add_action('wp_ajax_mlf_search_fonts', [$this, 'handle_search']);
// No nopriv - admin only
// Initialize rate limiter: 30 requests per minute (search can be rapid while typing)
$this->rate_limiter = new MLF_Rate_Limiter(30, 60);
}
/**
* Handle font search AJAX request.
*/
public function handle_search() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_search_fonts', 'nonce', false)) {
wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403);
}
// 2. CAPABILITY CHECK
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403);
}
// 3. RATE LIMIT CHECK
if (!$this->rate_limiter->check_and_record('search')) {
wp_send_json_error([
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
], 429);
}
// 4. INPUT VALIDATION
$query = isset($_POST['query']) ? sanitize_text_field(wp_unslash($_POST['query'])) : '';
if (strlen($query) < 2) {
wp_send_json_success(['fonts' => []]);
}
// Limit query length
if (strlen($query) > 100) {
wp_send_json_error(['message' => __('Search query too long.', 'maple-local-fonts')]);
}
// 5. PROCESS REQUEST
$fonts = $this->get_fonts_metadata();
if (is_wp_error($fonts)) {
wp_send_json_error(['message' => $fonts->get_error_message()]);
}
// Search fonts
$results = $this->search_fonts($fonts, $query);
wp_send_json_success(['fonts' => $results]);
}
/**
* Get Google Fonts metadata (cached).
*
* @return array|WP_Error Array of fonts or error.
*/
public function get_fonts_metadata() {
// Check cache first
$cached = get_transient(self::CACHE_KEY);
if ($cached !== false) {
return $cached;
}
// Fetch from Google
$response = wp_remote_get(self::METADATA_URL, [
'timeout' => 30,
'sslverify' => true,
]);
if (is_wp_error($response)) {
return new WP_Error('fetch_failed', __('Could not fetch fonts list from Google.', 'maple-local-fonts'));
}
$status = wp_remote_retrieve_response_code($response);
if ($status !== 200) {
return new WP_Error('fetch_failed', __('Google Fonts returned an error.', 'maple-local-fonts'));
}
$body = wp_remote_retrieve_body($response);
// Validate response size
if (strlen($body) > self::MAX_METADATA_SIZE) {
return new WP_Error('response_too_large', __('Font metadata response too large.', 'maple-local-fonts'));
}
// Validate response is not empty
if (empty($body)) {
return new WP_Error('empty_response', __('Empty response from Google Fonts.', 'maple-local-fonts'));
}
// Google's metadata has a )]}' prefix for security, remove it
$body = preg_replace('/^\)\]\}\'\s*/', '', $body);
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return new WP_Error('parse_failed', __('Could not parse fonts data.', 'maple-local-fonts'));
}
if (!$data || !isset($data['familyMetadataList'])) {
return new WP_Error('parse_failed', __('Could not parse fonts data.', 'maple-local-fonts'));
}
// Process and simplify the data
$fonts = [];
foreach ($data['familyMetadataList'] as $font) {
$fonts[] = [
'family' => $font['family'],
'category' => $font['category'] ?? 'sans-serif',
'variants' => $font['fonts'] ?? [],
'subsets' => $font['subsets'] ?? ['latin'],
'version' => $font['version'] ?? '',
'lastModified' => $font['lastModified'] ?? '',
];
}
// Cache for 24 hours
set_transient(self::CACHE_KEY, $fonts, self::CACHE_DURATION);
return $fonts;
}
/**
* Search fonts by query.
*
* @param array $fonts All fonts.
* @param string $query Search query.
* @return array Matching fonts (max 20).
*/
private function search_fonts($fonts, $query) {
$query_lower = strtolower($query);
$exact_matches = [];
$starts_with = [];
$contains = [];
foreach ($fonts as $font) {
$family_lower = strtolower($font['family']);
// Exact match
if ($family_lower === $query_lower) {
$exact_matches[] = $this->format_font_result($font);
}
// Starts with query
elseif (strpos($family_lower, $query_lower) === 0) {
$starts_with[] = $this->format_font_result($font);
}
// Contains query
elseif (strpos($family_lower, $query_lower) !== false) {
$contains[] = $this->format_font_result($font);
}
}
// Combine results: exact matches first, then starts with, then contains
$results = array_merge($exact_matches, $starts_with, $contains);
// Limit to 20 results
return array_slice($results, 0, 20);
}
/**
* Format a font for the search results.
*
* @param array $font Font data.
* @return array Formatted font.
*/
private function format_font_result($font) {
// Check if font has variable version
$has_variable = false;
$has_italic = false;
$weights = [];
if (!empty($font['variants'])) {
foreach ($font['variants'] as $variant => $data) {
// Variable fonts have ranges like "100..900"
if (strpos($variant, '..') !== false) {
$has_variable = true;
}
if (strpos($variant, 'i') !== false || strpos($variant, 'italic') !== false) {
$has_italic = true;
}
// Extract weight
$weight = preg_replace('/[^0-9]/', '', $variant);
if ($weight && !in_array($weight, $weights, true)) {
$weights[] = $weight;
}
}
}
// Sort weights
sort($weights, SORT_NUMERIC);
return [
'family' => $font['family'],
'category' => $font['category'],
'has_variable' => $has_variable,
'has_italic' => $has_italic,
'weights' => $weights,
'version' => $font['version'] ?? '',
'lastModified' => $font['lastModified'] ?? '',
];
}
/**
* Clear the fonts metadata cache.
*/
public function clear_cache() {
delete_transient(self::CACHE_KEY);
}
/**
* Get version info for a specific font.
*
* @param string $font_family Font family name.
* @return array|null Version info or null if not found.
*/
public function get_font_version($font_family) {
$fonts = $this->get_fonts_metadata();
if (is_wp_error($fonts)) {
return null;
}
$font_family_lower = strtolower($font_family);
foreach ($fonts as $font) {
if (strtolower($font['family']) === $font_family_lower) {
return [
'version' => $font['version'] ?? '',
'lastModified' => $font['lastModified'] ?? '',
];
}
}
return null;
}
}

View file

@ -1,182 +0,0 @@
<?php
/**
* Font Server for Maple Local Fonts.
*
* Serves font files and CSS through PHP with proper headers.
* This ensures CORS and MIME types work regardless of server configuration.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Font_Server
*
* Handles serving font files and CSS with proper headers via REST API.
*/
class MLF_Font_Server {
/**
* Register REST API routes.
*/
public function register_routes() {
// Serve individual font files
register_rest_route('mlf/v1', '/font/(?P<filename>[a-zA-Z0-9_\-]+\.woff2)', [
'methods' => 'GET',
'callback' => [$this, 'serve_font'],
'permission_callback' => '__return_true',
'args' => [
'filename' => [
'required' => true,
'validate_callback' => [$this, 'validate_filename'],
],
],
]);
// Serve CSS file (like Google Fonts does)
register_rest_route('mlf/v1', '/css', [
'methods' => 'GET',
'callback' => [$this, 'serve_css'],
'permission_callback' => '__return_true',
]);
}
/**
* Validate font filename.
*
* @param string $filename The filename to validate.
* @return bool True if valid.
*/
public function validate_filename($filename) {
// Only allow alphanumeric, underscore, hyphen, and .woff2 extension
if (!preg_match('/^[a-zA-Z0-9_\-]+\.woff2$/', $filename)) {
return false;
}
// Prevent directory traversal
if (strpos($filename, '..') !== false || strpos($filename, '/') !== false) {
return false;
}
return true;
}
/**
* Serve a font file with proper headers.
*
* @param WP_REST_Request $request The request object.
* @return WP_REST_Response|WP_Error Response or error.
*/
public function serve_font($request) {
$filename = $request->get_param('filename');
// Get font directory
$font_dir = wp_get_font_dir();
$file_path = trailingslashit($font_dir['path']) . $filename;
// Validate file exists and is within fonts directory
$real_path = realpath($file_path);
$fonts_path = realpath($font_dir['path']);
if (!$real_path || !$fonts_path || strpos($real_path, $fonts_path) !== 0) {
return new WP_Error('not_found', 'Font not found', ['status' => 404]);
}
if (!file_exists($real_path)) {
return new WP_Error('not_found', 'Font not found', ['status' => 404]);
}
// Verify it's a woff2 file
if (pathinfo($real_path, PATHINFO_EXTENSION) !== 'woff2') {
return new WP_Error('invalid_type', 'Invalid file type', ['status' => 400]);
}
// Read file content
$content = file_get_contents($real_path);
if ($content === false) {
return new WP_Error('read_error', 'Could not read font file', ['status' => 500]);
}
// Verify WOFF2 magic bytes
if (substr($content, 0, 4) !== 'wOF2') {
return new WP_Error('invalid_format', 'Invalid font format', ['status' => 400]);
}
// Send font with proper headers
$response = new WP_REST_Response($content);
$response->set_status(200);
$response->set_headers([
'Content-Type' => 'font/woff2',
'Content-Length' => strlen($content),
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, OPTIONS',
'Cross-Origin-Resource-Policy' => 'cross-origin',
'Cache-Control' => 'public, max-age=31536000, immutable',
'Vary' => 'Accept-Encoding',
]);
return $response;
}
/**
* Serve CSS file with @font-face rules (mimics Google Fonts).
*
* @param WP_REST_Request $request The request object.
* @return WP_REST_Response Response with CSS content.
*/
public function serve_css($request) {
$registry = new MLF_Font_Registry();
$fonts = $registry->get_imported_fonts();
if (empty($fonts)) {
$response = new WP_REST_Response('/* No fonts installed */');
$response->set_headers(['Content-Type' => 'text/css; charset=UTF-8']);
return $response;
}
$css = "/* Maple Local Fonts - Generated CSS */\n\n";
foreach ($fonts as $font) {
$font_slug = sanitize_title($font['name']);
foreach ($font['variants'] as $variant) {
$weight = $variant['weight'];
$style = $variant['style'];
// Build filename
if (strpos($weight, ' ') !== false) {
$filename = sprintf('%s_%s_variable.woff2', $font_slug, $style);
} else {
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight);
}
// Use REST API URL for font file
$font_url = rest_url('mlf/v1/font/' . $filename);
// Generate @font-face rule (format like Google Fonts)
$css .= "/* {$style} {$weight} */\n";
$css .= "@font-face {\n";
$css .= " font-family: '{$font['name']}';\n";
$css .= " font-style: {$style};\n";
$css .= " font-weight: {$weight};\n";
$css .= " font-display: swap;\n";
$css .= " src: url({$font_url}) format('woff2');\n";
$css .= "}\n\n";
}
}
$response = new WP_REST_Response($css);
$response->set_status(200);
$response->set_headers([
'Content-Type' => 'text/css; charset=UTF-8',
'Access-Control-Allow-Origin' => '*',
'Cache-Control' => 'public, max-age=86400',
]);
return $response;
}
}

View file

@ -1,188 +0,0 @@
<?php
/**
* Rate Limiter for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Rate_Limiter
*
* Provides rate limiting functionality to prevent abuse of AJAX endpoints.
*/
class MLF_Rate_Limiter {
/**
* Default rate limit (requests per window).
*
* @var int
*/
private $limit = 10;
/**
* Default time window in seconds.
*
* @var int
*/
private $window = 60;
/**
* Constructor.
*
* @param int $limit Maximum requests allowed per window.
* @param int $window Time window in seconds.
*/
public function __construct($limit = 10, $window = 60) {
$this->limit = absint($limit);
$this->window = absint($window);
}
/**
* Check if the current user/IP is rate limited.
*
* @param string $action The action being rate limited.
* @return bool True if rate limited (should block), false if allowed.
*/
public function is_limited($action) {
$key = $this->get_rate_limit_key($action);
$data = get_transient($key);
if ($data === false) {
// First request, not limited
return false;
}
return $data['count'] >= $this->limit;
}
/**
* Record a request for rate limiting purposes.
*
* @param string $action The action being recorded.
* @return void
*/
public function record_request($action) {
$key = $this->get_rate_limit_key($action);
$data = get_transient($key);
if ($data === false) {
// First request in this window
$data = [
'count' => 1,
'start' => time(),
];
} else {
$data['count']++;
}
// Set/update transient with remaining window time
$elapsed = time() - $data['start'];
$remaining = max(1, $this->window - $elapsed);
set_transient($key, $data, $remaining);
}
/**
* Check rate limit and record request in one call.
*
* @param string $action The action being checked.
* @return bool True if request is allowed, false if rate limited.
*/
public function check_and_record($action) {
if ($this->is_limited($action)) {
return false;
}
$this->record_request($action);
return true;
}
/**
* Get the number of remaining requests in the current window.
*
* @param string $action The action to check.
* @return int Number of remaining requests.
*/
public function get_remaining($action) {
$key = $this->get_rate_limit_key($action);
$data = get_transient($key);
if ($data === false) {
return $this->limit;
}
return max(0, $this->limit - $data['count']);
}
/**
* Get the rate limit key for the current user/IP.
*
* @param string $action The action being rate limited.
* @return string Transient key.
*/
private function get_rate_limit_key($action) {
// Use user ID if logged in, otherwise IP
$user_id = get_current_user_id();
if ($user_id > 0) {
$identifier = 'user_' . $user_id;
} else {
// Sanitize and hash IP for privacy
$ip = $this->get_client_ip();
$identifier = 'ip_' . md5($ip);
}
return 'mlf_rate_' . sanitize_key($action) . '_' . $identifier;
}
/**
* Get the client IP address.
*
* @return string Client IP address.
*/
private function get_client_ip() {
$ip = '';
// Check for various headers (in order of reliability)
$headers = [
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR',
];
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
// X-Forwarded-For can contain multiple IPs, get the first one
$ips = explode(',', sanitize_text_field(wp_unslash($_SERVER[$header])));
$ip = trim($ips[0]);
// Validate IP
if (filter_var($ip, FILTER_VALIDATE_IP)) {
break;
}
}
}
// Fallback to localhost if no valid IP found
return $ip ?: '127.0.0.1';
}
/**
* Clear rate limit for a specific action and user/IP.
*
* @param string $action The action to clear.
* @return void
*/
public function clear($action) {
$key = $this->get_rate_limit_key($action);
delete_transient($key);
}
}

View file

@ -1,477 +0,0 @@
<?php
/**
* REST API Controller for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class MLF_Rest_Controller
*
* Provides REST API endpoints for font management.
*/
class MLF_Rest_Controller extends WP_REST_Controller {
/**
* Namespace for the API.
*
* @var string
*/
protected $namespace = 'mlf/v1';
/**
* Resource name.
*
* @var string
*/
protected $rest_base = 'fonts';
/**
* Rate limiter instance.
*
* @var MLF_Rate_Limiter
*/
private $rate_limiter;
/**
* Constructor.
*/
public function __construct() {
$this->rate_limiter = new MLF_Rate_Limiter(10, 60);
}
/**
* Register the routes for the controller.
*/
public function register_routes() {
// GET /wp-json/mlf/v1/fonts - List all fonts
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_items'],
'permission_callback' => [$this, 'get_items_permissions_check'],
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'create_item'],
'permission_callback' => [$this, 'create_item_permissions_check'],
'args' => $this->get_create_item_args(),
],
'schema' => [$this, 'get_public_item_schema'],
]
);
// DELETE /wp-json/mlf/v1/fonts/{id} - Delete a font
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
[
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [$this, 'delete_item'],
'permission_callback' => [$this, 'delete_item_permissions_check'],
'args' => [
'id' => [
'description' => __('Unique identifier for the font.', 'maple-local-fonts'),
'type' => 'integer',
'required' => true,
],
],
],
]
);
// GET /wp-json/mlf/v1/fonts/{id} - Get a single font
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_item'],
'permission_callback' => [$this, 'get_items_permissions_check'],
'args' => [
'id' => [
'description' => __('Unique identifier for the font.', 'maple-local-fonts'),
'type' => 'integer',
'required' => true,
],
],
],
]
);
}
/**
* Check if a given request has access to get items.
*
* @param WP_REST_Request $request Full data about the request.
* @return bool|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check($request) {
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
return new WP_Error(
'rest_forbidden',
__('Sorry, you are not allowed to view fonts.', 'maple-local-fonts'),
['status' => rest_authorization_required_code()]
);
}
return true;
}
/**
* Check if a given request has access to create items.
*
* @param WP_REST_Request $request Full data about the request.
* @return bool|WP_Error True if the request has create access, WP_Error object otherwise.
*/
public function create_item_permissions_check($request) {
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
return new WP_Error(
'rest_forbidden',
__('Sorry, you are not allowed to create fonts.', 'maple-local-fonts'),
['status' => rest_authorization_required_code()]
);
}
// Rate limit check
if (!$this->rate_limiter->check_and_record('rest_create')) {
return new WP_Error(
'rest_rate_limited',
__('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
['status' => 429]
);
}
return true;
}
/**
* Check if a given request has access to delete a specific item.
*
* @param WP_REST_Request $request Full data about the request.
* @return bool|WP_Error True if the request has delete access, WP_Error object otherwise.
*/
public function delete_item_permissions_check($request) {
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
if (!current_user_can($capability)) {
return new WP_Error(
'rest_forbidden',
__('Sorry, you are not allowed to delete fonts.', 'maple-local-fonts'),
['status' => rest_authorization_required_code()]
);
}
// Rate limit check
if (!$this->rate_limiter->check_and_record('rest_delete')) {
return new WP_Error(
'rest_rate_limited',
__('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
['status' => 429]
);
}
return true;
}
/**
* Get a collection of fonts.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items($request) {
$registry = new MLF_Font_Registry();
$fonts = $registry->get_imported_fonts();
$data = [];
foreach ($fonts as $font) {
$data[] = $this->prepare_item_for_response($font, $request);
}
return rest_ensure_response($data);
}
/**
* Get a single font.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item($request) {
$font_id = absint($request->get_param('id'));
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
return new WP_Error(
'rest_font_not_found',
__('Font not found.', 'maple-local-fonts'),
['status' => 404]
);
}
// Verify it's one we imported
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
return new WP_Error(
'rest_font_not_found',
__('Font not found.', 'maple-local-fonts'),
['status' => 404]
);
}
$registry = new MLF_Font_Registry();
$fonts = $registry->get_imported_fonts();
$font_data = null;
foreach ($fonts as $f) {
if ($f['id'] === $font_id) {
$font_data = $f;
break;
}
}
if (!$font_data) {
return new WP_Error(
'rest_font_not_found',
__('Font not found.', 'maple-local-fonts'),
['status' => 404]
);
}
return rest_ensure_response($this->prepare_item_for_response($font_data, $request));
}
/**
* Create a font.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item($request) {
$font_name = sanitize_text_field($request->get_param('font_name'));
$include_italic = (bool) $request->get_param('include_italic');
// Validate font name
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
return new WP_Error(
'rest_invalid_param',
__('Invalid font name: only letters, numbers, spaces, and hyphens allowed.', 'maple-local-fonts'),
['status' => 400]
);
}
if (strlen($font_name) > 100) {
return new WP_Error(
'rest_invalid_param',
__('Font name is too long.', 'maple-local-fonts'),
['status' => 400]
);
}
try {
$downloader = new MLF_Font_Downloader();
$download_result = $downloader->download($font_name, $include_italic);
if (is_wp_error($download_result)) {
return new WP_Error(
'rest_download_failed',
$download_result->get_error_message(),
['status' => 400]
);
}
// Register font with WordPress
$registry = new MLF_Font_Registry();
$result = $registry->register_font(
$download_result['font_name'],
$download_result['font_slug'],
$download_result['files']
);
if (is_wp_error($result)) {
return new WP_Error(
'rest_register_failed',
$result->get_error_message(),
['status' => 400]
);
}
// Return the created font
$fonts = $registry->get_imported_fonts();
$created_font = null;
foreach ($fonts as $font) {
if ($font['id'] === $result) {
$created_font = $font;
break;
}
}
$response = rest_ensure_response($this->prepare_item_for_response($created_font, $request));
$response->set_status(201);
$response->header('Location', rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $result)));
return $response;
} catch (Exception $e) {
error_log('MLF REST Create Error: ' . sanitize_text_field($e->getMessage()));
return new WP_Error(
'rest_internal_error',
__('An unexpected error occurred.', 'maple-local-fonts'),
['status' => 500]
);
}
}
/**
* Delete a font.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item($request) {
$font_id = absint($request->get_param('id'));
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
return new WP_Error(
'rest_font_not_found',
__('Font not found.', 'maple-local-fonts'),
['status' => 404]
);
}
// Verify it's one we imported
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
return new WP_Error(
'rest_cannot_delete',
__('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts'),
['status' => 403]
);
}
try {
$registry = new MLF_Font_Registry();
$result = $registry->delete_font($font_id);
if (is_wp_error($result)) {
return new WP_Error(
'rest_delete_failed',
$result->get_error_message(),
['status' => 400]
);
}
return rest_ensure_response([
'deleted' => true,
'message' => __('Font deleted successfully.', 'maple-local-fonts'),
]);
} catch (Exception $e) {
error_log('MLF REST Delete Error: ' . sanitize_text_field($e->getMessage()));
return new WP_Error(
'rest_internal_error',
__('An unexpected error occurred.', 'maple-local-fonts'),
['status' => 500]
);
}
}
/**
* Prepare a font for the REST response.
*
* @param array $font Font data.
* @param WP_REST_Request $request Request object.
* @return array Prepared font data.
*/
public function prepare_item_for_response($font, $request) {
return [
'id' => $font['id'],
'name' => $font['name'],
'slug' => $font['slug'],
'variants' => $font['variants'],
'_links' => [
'self' => [
['href' => rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $font['id']))],
],
'collection' => [
['href' => rest_url(sprintf('%s/%s', $this->namespace, $this->rest_base))],
],
],
];
}
/**
* Get the argument schema for creating items.
*
* @return array Arguments schema.
*/
protected function get_create_item_args() {
return [
'font_name' => [
'description' => __('The font family name from Google Fonts.', 'maple-local-fonts'),
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'include_italic' => [
'description' => __('Whether to include italic styles.', 'maple-local-fonts'),
'type' => 'boolean',
'default' => true,
],
];
}
/**
* Get the font schema.
*
* @return array Schema definition.
*/
public function get_item_schema() {
return [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'font',
'type' => 'object',
'properties' => [
'id' => [
'description' => __('Unique identifier for the font.', 'maple-local-fonts'),
'type' => 'integer',
'context' => ['view'],
'readonly' => true,
],
'name' => [
'description' => __('The font family name.', 'maple-local-fonts'),
'type' => 'string',
'context' => ['view'],
],
'slug' => [
'description' => __('The font slug.', 'maple-local-fonts'),
'type' => 'string',
'context' => ['view'],
],
'variants' => [
'description' => __('Array of font variants.', 'maple-local-fonts'),
'type' => 'array',
'context' => ['view'],
'items' => [
'type' => 'object',
'properties' => [
'weight' => [
'type' => 'string',
],
'style' => [
'type' => 'string',
],
],
],
],
],
];
}
}

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Arabic
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Arabic\n"
"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "استورد خطوط Google Fonts إلى التخزين المحلي وسجلها في مكتبة خطوط WordPress للحصول على طباعة متوافقة مع GDPR وصديقة للخصوصية."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "يتطلب Maple Local Fonts إصدار WordPress 6.5 أو أعلى لدعم مكتبة الخطوط."
msgid "Plugin Activation Error"
msgstr "خطأ في تفعيل الإضافة"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "تم إلغاء تفعيل Maple Local Fonts. يتطلب WordPress 6.5 أو أعلى."
msgid "Local Fonts"
msgstr "الخطوط المحلية"
msgid "Downloading..."
msgstr "جارٍ التنزيل..."
msgid "Deleting..."
msgstr "جارٍ الحذف..."
msgid "Are you sure you want to delete this font?"
msgstr "هل أنت متأكد أنك تريد حذف هذا الخط؟"
msgid "An error occurred. Please try again."
msgstr "حدث خطأ. يرجى المحاولة مرة أخرى."
msgid "Please select at least one weight."
msgstr "يرجى اختيار وزن واحد على الأقل."
msgid "Please select at least one style."
msgstr "يرجى اختيار نمط واحد على الأقل."
msgid "Please enter a font name."
msgstr "يرجى إدخال اسم الخط."
msgid "Security check failed."
msgstr "فشل التحقق الأمني."
msgid "Unauthorized."
msgstr "غير مصرح."
msgid "Font name is required."
msgstr "اسم الخط مطلوب."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "اسم خط غير صالح: يُسمح فقط بالأحرف والأرقام والمسافات والشرطات."
msgid "Font name is too long."
msgstr "اسم الخط طويل جداً."
msgid "At least one weight is required."
msgstr "مطلوب وزن واحد على الأقل."
msgid "Too many weights selected."
msgstr "تم اختيار أوزان كثيرة جداً."
msgid "At least one style is required."
msgstr "مطلوب نمط واحد على الأقل."
msgid "Successfully installed %s."
msgstr "تم تثبيت %s بنجاح."
msgid "An unexpected error occurred."
msgstr "حدث خطأ غير متوقع."
msgid "Invalid font ID."
msgstr "معرف خط غير صالح."
msgid "Font not found."
msgstr "الخط غير موجود."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "لا يمكن حذف الخطوط التي لم يتم استيرادها بواسطة هذه الإضافة."
msgid "Font deleted successfully."
msgstr "تم حذف الخط بنجاح."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "الخط غير موجود في Google Fonts. يرجى التحقق من الإملاء والمحاولة مرة أخرى."
msgid "This font is already installed."
msgstr "هذا الخط مثبت بالفعل."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "تعذر الاتصال بـ Google Fonts. يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى."
msgid "Google Fonts returned an error. Please try again later."
msgstr "أرجع Google Fonts خطأ. يرجى المحاولة لاحقاً."
msgid "Could not process the font data. The font may not be available."
msgstr "تعذرت معالجة بيانات الخط. قد لا يكون الخط متاحاً."
msgid "Could not download the font files. Please try again."
msgstr "تعذر تنزيل ملفات الخط. يرجى المحاولة مرة أخرى."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "تعذر حفظ ملفات الخط. يرجى التحقق من أن wp-content/fonts قابل للكتابة."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "تعذر إنشاء دليل الخطوط. يرجى التحقق من أذونات الملفات."
msgid "Invalid file path."
msgstr "مسار ملف غير صالح."
msgid "Invalid font URL."
msgstr "رابط خط غير صالح."
msgid "Invalid font name."
msgstr "اسم خط غير صالح."
msgid "No valid weights specified."
msgstr "لم يتم تحديد أوزان صالحة."
msgid "No valid styles specified."
msgstr "لم يتم تحديد أنماط صالحة."
msgid "An unexpected error occurred. Please try again."
msgstr "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى."
msgid "Thin"
msgstr "رفيع"
msgid "Extra Light"
msgstr "خفيف جداً"
msgid "Light"
msgstr "خفيف"
msgid "Regular"
msgstr "عادي"
msgid "Medium"
msgstr "متوسط"
msgid "Semi Bold"
msgstr "شبه عريض"
msgid "Bold"
msgstr "عريض"
msgid "Extra Bold"
msgstr "عريض جداً"
msgid "Black"
msgstr "أسود"
msgid "You do not have sufficient permissions to access this page."
msgstr "ليس لديك الصلاحيات الكافية للوصول إلى هذه الصفحة."
msgid "Import from Google Fonts"
msgstr "استيراد من Google Fonts"
msgid "Font Name"
msgstr "اسم الخط"
msgid "e.g., Open Sans"
msgstr "مثال: Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "أدخل اسم الخط الدقيق كما يظهر في Google Fonts."
msgid "Weights"
msgstr "الأوزان"
msgid "Styles"
msgstr "الأنماط"
msgid "Normal"
msgstr "عادي"
msgid "Italic"
msgstr "مائل"
msgid "Files to download:"
msgstr "الملفات للتنزيل:"
msgid "Download & Install"
msgstr "تنزيل وتثبيت"
msgid "Installed Fonts"
msgstr "الخطوط المثبتة"
msgid "No fonts installed yet."
msgstr "لم يتم تثبيت أي خطوط بعد."
msgid "Delete"
msgstr "حذف"
msgid "Use %s to apply fonts to your site."
msgstr "استخدم %s لتطبيق الخطوط على موقعك."
msgid "Appearance → Editor → Styles → Typography"
msgstr "المظهر ← المحرر ← الأنماط ← الطباعة"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in German
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: German\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importieren Sie Google Fonts in den lokalen Speicher und registrieren Sie sie in der WordPress-Schriftbibliothek für DSGVO-konforme, datenschutzfreundliche Typografie."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts erfordert WordPress 6.5 oder höher für die Unterstützung der Schriftbibliothek."
msgid "Plugin Activation Error"
msgstr "Plugin-Aktivierungsfehler"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts wurde deaktiviert. Es erfordert WordPress 6.5 oder höher."
msgid "Local Fonts"
msgstr "Lokale Schriften"
msgid "Downloading..."
msgstr "Wird heruntergeladen..."
msgid "Deleting..."
msgstr "Wird gelöscht..."
msgid "Are you sure you want to delete this font?"
msgstr "Sind Sie sicher, dass Sie diese Schrift löschen möchten?"
msgid "An error occurred. Please try again."
msgstr "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut."
msgid "Please select at least one weight."
msgstr "Bitte wählen Sie mindestens eine Schriftstärke aus."
msgid "Please select at least one style."
msgstr "Bitte wählen Sie mindestens einen Stil aus."
msgid "Please enter a font name."
msgstr "Bitte geben Sie einen Schriftnamen ein."
msgid "Security check failed."
msgstr "Sicherheitsprüfung fehlgeschlagen."
msgid "Unauthorized."
msgstr "Nicht autorisiert."
msgid "Font name is required."
msgstr "Schriftname ist erforderlich."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Ungültiger Schriftname: Nur Buchstaben, Zahlen, Leerzeichen und Bindestriche erlaubt."
msgid "Font name is too long."
msgstr "Schriftname ist zu lang."
msgid "At least one weight is required."
msgstr "Mindestens eine Schriftstärke ist erforderlich."
msgid "Too many weights selected."
msgstr "Zu viele Schriftstärken ausgewählt."
msgid "At least one style is required."
msgstr "Mindestens ein Stil ist erforderlich."
msgid "Successfully installed %s."
msgstr "%s wurde erfolgreich installiert."
msgid "An unexpected error occurred."
msgstr "Ein unerwarteter Fehler ist aufgetreten."
msgid "Invalid font ID."
msgstr "Ungültige Schrift-ID."
msgid "Font not found."
msgstr "Schrift nicht gefunden."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Schriften, die nicht von diesem Plugin importiert wurden, können nicht gelöscht werden."
msgid "Font deleted successfully."
msgstr "Schrift erfolgreich gelöscht."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Schrift bei Google Fonts nicht gefunden. Bitte überprüfen Sie die Schreibweise und versuchen Sie es erneut."
msgid "This font is already installed."
msgstr "Diese Schrift ist bereits installiert."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Verbindung zu Google Fonts konnte nicht hergestellt werden. Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts hat einen Fehler zurückgegeben. Bitte versuchen Sie es später erneut."
msgid "Could not process the font data. The font may not be available."
msgstr "Schriftdaten konnten nicht verarbeitet werden. Die Schrift ist möglicherweise nicht verfügbar."
msgid "Could not download the font files. Please try again."
msgstr "Schriftdateien konnten nicht heruntergeladen werden. Bitte versuchen Sie es erneut."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Schriftdateien konnten nicht gespeichert werden. Bitte überprüfen Sie, ob wp-content/fonts beschreibbar ist."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Schriftverzeichnis konnte nicht erstellt werden. Bitte überprüfen Sie die Dateiberechtigungen."
msgid "Invalid file path."
msgstr "Ungültiger Dateipfad."
msgid "Invalid font URL."
msgstr "Ungültige Schrift-URL."
msgid "Invalid font name."
msgstr "Ungültiger Schriftname."
msgid "No valid weights specified."
msgstr "Keine gültigen Schriftstärken angegeben."
msgid "No valid styles specified."
msgstr "Keine gültigen Stile angegeben."
msgid "An unexpected error occurred. Please try again."
msgstr "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut."
msgid "Thin"
msgstr "Dünn"
msgid "Extra Light"
msgstr "Extraleicht"
msgid "Light"
msgstr "Leicht"
msgid "Regular"
msgstr "Normal"
msgid "Medium"
msgstr "Mittel"
msgid "Semi Bold"
msgstr "Halbfett"
msgid "Bold"
msgstr "Fett"
msgid "Extra Bold"
msgstr "Extrafett"
msgid "Black"
msgstr "Schwarz"
msgid "You do not have sufficient permissions to access this page."
msgstr "Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen."
msgid "Import from Google Fonts"
msgstr "Von Google Fonts importieren"
msgid "Font Name"
msgstr "Schriftname"
msgid "e.g., Open Sans"
msgstr "z.B. Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Geben Sie den genauen Schriftnamen ein, wie er bei Google Fonts erscheint."
msgid "Weights"
msgstr "Schriftstärken"
msgid "Styles"
msgstr "Stile"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Kursiv"
msgid "Files to download:"
msgstr "Herunterzuladende Dateien:"
msgid "Download & Install"
msgstr "Herunterladen & Installieren"
msgid "Installed Fonts"
msgstr "Installierte Schriften"
msgid "No fonts installed yet."
msgstr "Noch keine Schriften installiert."
msgid "Delete"
msgstr "Löschen"
msgid "Use %s to apply fonts to your site."
msgstr "Verwenden Sie %s, um Schriften auf Ihre Website anzuwenden."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Design → Editor → Stile → Typografie"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in English (Canada)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: English (Canada)\n"
"Language: en_CA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgid "Plugin Activation Error"
msgstr "Plugin Activation Error"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgid "Local Fonts"
msgstr "Local Fonts"
msgid "Downloading..."
msgstr "Downloading..."
msgid "Deleting..."
msgstr "Deleting..."
msgid "Are you sure you want to delete this font?"
msgstr "Are you sure you want to delete this font?"
msgid "An error occurred. Please try again."
msgstr "An error occurred. Please try again."
msgid "Please select at least one weight."
msgstr "Please select at least one weight."
msgid "Please select at least one style."
msgstr "Please select at least one style."
msgid "Please enter a font name."
msgstr "Please enter a font name."
msgid "Security check failed."
msgstr "Security check failed."
msgid "Unauthorized."
msgstr "Unauthorized."
msgid "Font name is required."
msgstr "Font name is required."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgid "Font name is too long."
msgstr "Font name is too long."
msgid "At least one weight is required."
msgstr "At least one weight is required."
msgid "Too many weights selected."
msgstr "Too many weights selected."
msgid "At least one style is required."
msgstr "At least one style is required."
msgid "Successfully installed %s."
msgstr "Successfully installed %s."
msgid "An unexpected error occurred."
msgstr "An unexpected error occurred."
msgid "Invalid font ID."
msgstr "Invalid font ID."
msgid "Font not found."
msgstr "Font not found."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Cannot delete fonts not imported by this plugin."
msgid "Font deleted successfully."
msgstr "Font deleted successfully."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Font not found on Google Fonts. Please check the spelling and try again."
msgid "This font is already installed."
msgstr "This font is already installed."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Could not connect to Google Fonts. Please check your internet connection and try again."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts returned an error. Please try again later."
msgid "Could not process the font data. The font may not be available."
msgstr "Could not process the font data. The font may not be available."
msgid "Could not download the font files. Please try again."
msgstr "Could not download the font files. Please try again."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Could not save font files. Please check that wp-content/fonts is writable."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Could not create fonts directory. Please check file permissions."
msgid "Invalid file path."
msgstr "Invalid file path."
msgid "Invalid font URL."
msgstr "Invalid font URL."
msgid "Invalid font name."
msgstr "Invalid font name."
msgid "No valid weights specified."
msgstr "No valid weights specified."
msgid "No valid styles specified."
msgstr "No valid styles specified."
msgid "An unexpected error occurred. Please try again."
msgstr "An unexpected error occurred. Please try again."
msgid "Thin"
msgstr "Thin"
msgid "Extra Light"
msgstr "Extra Light"
msgid "Light"
msgstr "Light"
msgid "Regular"
msgstr "Regular"
msgid "Medium"
msgstr "Medium"
msgid "Semi Bold"
msgstr "Semi Bold"
msgid "Bold"
msgstr "Bold"
msgid "Extra Bold"
msgstr "Extra Bold"
msgid "Black"
msgstr "Black"
msgid "You do not have sufficient permissions to access this page."
msgstr "You do not have sufficient permissions to access this page."
msgid "Import from Google Fonts"
msgstr "Import from Google Fonts"
msgid "Font Name"
msgstr "Font Name"
msgid "e.g., Open Sans"
msgstr "e.g., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Enter the exact font name as it appears on Google Fonts."
msgid "Weights"
msgstr "Weights"
msgid "Styles"
msgstr "Styles"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Italic"
msgid "Files to download:"
msgstr "Files to download:"
msgid "Download & Install"
msgstr "Download & Install"
msgid "Installed Fonts"
msgstr "Installed Fonts"
msgid "No fonts installed yet."
msgstr "No fonts installed yet."
msgid "Delete"
msgstr "Delete"
msgid "Use %s to apply fonts to your site."
msgstr "Use %s to apply fonts to your site."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Appearance → Editor → Styles → Typography"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in English (United Kingdom)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: English (United Kingdom)\n"
"Language: en_GB\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgid "Plugin Activation Error"
msgstr "Plugin Activation Error"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgid "Local Fonts"
msgstr "Local Fonts"
msgid "Downloading..."
msgstr "Downloading..."
msgid "Deleting..."
msgstr "Deleting..."
msgid "Are you sure you want to delete this font?"
msgstr "Are you sure you want to delete this font?"
msgid "An error occurred. Please try again."
msgstr "An error occurred. Please try again."
msgid "Please select at least one weight."
msgstr "Please select at least one weight."
msgid "Please select at least one style."
msgstr "Please select at least one style."
msgid "Please enter a font name."
msgstr "Please enter a font name."
msgid "Security check failed."
msgstr "Security check failed."
msgid "Unauthorised."
msgstr "Unauthorised."
msgid "Font name is required."
msgstr "Font name is required."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgid "Font name is too long."
msgstr "Font name is too long."
msgid "At least one weight is required."
msgstr "At least one weight is required."
msgid "Too many weights selected."
msgstr "Too many weights selected."
msgid "At least one style is required."
msgstr "At least one style is required."
msgid "Successfully installed %s."
msgstr "Successfully installed %s."
msgid "An unexpected error occurred."
msgstr "An unexpected error occurred."
msgid "Invalid font ID."
msgstr "Invalid font ID."
msgid "Font not found."
msgstr "Font not found."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Cannot delete fonts not imported by this plugin."
msgid "Font deleted successfully."
msgstr "Font deleted successfully."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Font not found on Google Fonts. Please check the spelling and try again."
msgid "This font is already installed."
msgstr "This font is already installed."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Could not connect to Google Fonts. Please check your internet connection and try again."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts returned an error. Please try again later."
msgid "Could not process the font data. The font may not be available."
msgstr "Could not process the font data. The font may not be available."
msgid "Could not download the font files. Please try again."
msgstr "Could not download the font files. Please try again."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Could not save font files. Please check that wp-content/fonts is writable."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Could not create fonts directory. Please check file permissions."
msgid "Invalid file path."
msgstr "Invalid file path."
msgid "Invalid font URL."
msgstr "Invalid font URL."
msgid "Invalid font name."
msgstr "Invalid font name."
msgid "No valid weights specified."
msgstr "No valid weights specified."
msgid "No valid styles specified."
msgstr "No valid styles specified."
msgid "An unexpected error occurred. Please try again."
msgstr "An unexpected error occurred. Please try again."
msgid "Thin"
msgstr "Thin"
msgid "Extra Light"
msgstr "Extra Light"
msgid "Light"
msgstr "Light"
msgid "Regular"
msgstr "Regular"
msgid "Medium"
msgstr "Medium"
msgid "Semi Bold"
msgstr "Semi Bold"
msgid "Bold"
msgstr "Bold"
msgid "Extra Bold"
msgstr "Extra Bold"
msgid "Black"
msgstr "Black"
msgid "You do not have sufficient permissions to access this page."
msgstr "You do not have sufficient permissions to access this page."
msgid "Import from Google Fonts"
msgstr "Import from Google Fonts"
msgid "Font Name"
msgstr "Font Name"
msgid "e.g., Open Sans"
msgstr "e.g., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Enter the exact font name as it appears on Google Fonts."
msgid "Weights"
msgstr "Weights"
msgid "Styles"
msgstr "Styles"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Italic"
msgid "Files to download:"
msgstr "Files to download:"
msgid "Download & Install"
msgstr "Download & Install"
msgid "Installed Fonts"
msgstr "Installed Fonts"
msgid "No fonts installed yet."
msgstr "No fonts installed yet."
msgid "Delete"
msgstr "Delete"
msgid "Use %s to apply fonts to your site."
msgstr "Use %s to apply fonts to your site."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Appearance → Editor → Styles → Typography"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in English (United States)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: English (United States)\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgid "Plugin Activation Error"
msgstr "Plugin Activation Error"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgid "Local Fonts"
msgstr "Local Fonts"
msgid "Downloading..."
msgstr "Downloading..."
msgid "Deleting..."
msgstr "Deleting..."
msgid "Are you sure you want to delete this font?"
msgstr "Are you sure you want to delete this font?"
msgid "An error occurred. Please try again."
msgstr "An error occurred. Please try again."
msgid "Please select at least one weight."
msgstr "Please select at least one weight."
msgid "Please select at least one style."
msgstr "Please select at least one style."
msgid "Please enter a font name."
msgstr "Please enter a font name."
msgid "Security check failed."
msgstr "Security check failed."
msgid "Unauthorized."
msgstr "Unauthorized."
msgid "Font name is required."
msgstr "Font name is required."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgid "Font name is too long."
msgstr "Font name is too long."
msgid "At least one weight is required."
msgstr "At least one weight is required."
msgid "Too many weights selected."
msgstr "Too many weights selected."
msgid "At least one style is required."
msgstr "At least one style is required."
msgid "Successfully installed %s."
msgstr "Successfully installed %s."
msgid "An unexpected error occurred."
msgstr "An unexpected error occurred."
msgid "Invalid font ID."
msgstr "Invalid font ID."
msgid "Font not found."
msgstr "Font not found."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Cannot delete fonts not imported by this plugin."
msgid "Font deleted successfully."
msgstr "Font deleted successfully."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Font not found on Google Fonts. Please check the spelling and try again."
msgid "This font is already installed."
msgstr "This font is already installed."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Could not connect to Google Fonts. Please check your internet connection and try again."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts returned an error. Please try again later."
msgid "Could not process the font data. The font may not be available."
msgstr "Could not process the font data. The font may not be available."
msgid "Could not download the font files. Please try again."
msgstr "Could not download the font files. Please try again."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Could not save font files. Please check that wp-content/fonts is writable."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Could not create fonts directory. Please check file permissions."
msgid "Invalid file path."
msgstr "Invalid file path."
msgid "Invalid font URL."
msgstr "Invalid font URL."
msgid "Invalid font name."
msgstr "Invalid font name."
msgid "No valid weights specified."
msgstr "No valid weights specified."
msgid "No valid styles specified."
msgstr "No valid styles specified."
msgid "An unexpected error occurred. Please try again."
msgstr "An unexpected error occurred. Please try again."
msgid "Thin"
msgstr "Thin"
msgid "Extra Light"
msgstr "Extra Light"
msgid "Light"
msgstr "Light"
msgid "Regular"
msgstr "Regular"
msgid "Medium"
msgstr "Medium"
msgid "Semi Bold"
msgstr "Semi Bold"
msgid "Bold"
msgstr "Bold"
msgid "Extra Bold"
msgstr "Extra Bold"
msgid "Black"
msgstr "Black"
msgid "You do not have sufficient permissions to access this page."
msgstr "You do not have sufficient permissions to access this page."
msgid "Import from Google Fonts"
msgstr "Import from Google Fonts"
msgid "Font Name"
msgstr "Font Name"
msgid "e.g., Open Sans"
msgstr "e.g., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Enter the exact font name as it appears on Google Fonts."
msgid "Weights"
msgstr "Weights"
msgid "Styles"
msgstr "Styles"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Italic"
msgid "Files to download:"
msgstr "Files to download:"
msgid "Download & Install"
msgstr "Download & Install"
msgid "Installed Fonts"
msgstr "Installed Fonts"
msgid "No fonts installed yet."
msgstr "No fonts installed yet."
msgid "Delete"
msgstr "Delete"
msgid "Use %s to apply fonts to your site."
msgstr "Use %s to apply fonts to your site."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Appearance → Editor → Styles → Typography"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Spanish (Latin America)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Spanish (Latin America)\n"
"Language: es_419\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importa fuentes de Google Fonts al almacenamiento local y regístralas con la biblioteca de fuentes de WordPress para una tipografía compatible con GDPR y respetuosa con la privacidad."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts requiere WordPress 6.5 o superior para el soporte de la biblioteca de fuentes."
msgid "Plugin Activation Error"
msgstr "Error de activación del plugin"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts ha sido desactivado. Requiere WordPress 6.5 o superior."
msgid "Local Fonts"
msgstr "Fuentes locales"
msgid "Downloading..."
msgstr "Descargando..."
msgid "Deleting..."
msgstr "Eliminando..."
msgid "Are you sure you want to delete this font?"
msgstr "¿Estás seguro de que quieres eliminar esta fuente?"
msgid "An error occurred. Please try again."
msgstr "Ocurrió un error. Por favor, intenta de nuevo."
msgid "Please select at least one weight."
msgstr "Por favor, selecciona al menos un peso."
msgid "Please select at least one style."
msgstr "Por favor, selecciona al menos un estilo."
msgid "Please enter a font name."
msgstr "Por favor, ingresa un nombre de fuente."
msgid "Security check failed."
msgstr "Falló la verificación de seguridad."
msgid "Unauthorized."
msgstr "No autorizado."
msgid "Font name is required."
msgstr "El nombre de la fuente es requerido."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nombre de fuente inválido: solo se permiten letras, números, espacios y guiones."
msgid "Font name is too long."
msgstr "El nombre de la fuente es demasiado largo."
msgid "At least one weight is required."
msgstr "Se requiere al menos un peso."
msgid "Too many weights selected."
msgstr "Demasiados pesos seleccionados."
msgid "At least one style is required."
msgstr "Se requiere al menos un estilo."
msgid "Successfully installed %s."
msgstr "%s se instaló correctamente."
msgid "An unexpected error occurred."
msgstr "Ocurrió un error inesperado."
msgid "Invalid font ID."
msgstr "ID de fuente inválido."
msgid "Font not found."
msgstr "Fuente no encontrada."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "No se pueden eliminar fuentes que no fueron importadas por este plugin."
msgid "Font deleted successfully."
msgstr "Fuente eliminada correctamente."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Fuente no encontrada en Google Fonts. Por favor, verifica la ortografía e intenta de nuevo."
msgid "This font is already installed."
msgstr "Esta fuente ya está instalada."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "No se pudo conectar a Google Fonts. Por favor, verifica tu conexión a internet e intenta de nuevo."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts devolvió un error. Por favor, intenta más tarde."
msgid "Could not process the font data. The font may not be available."
msgstr "No se pudieron procesar los datos de la fuente. La fuente puede no estar disponible."
msgid "Could not download the font files. Please try again."
msgstr "No se pudieron descargar los archivos de la fuente. Por favor, intenta de nuevo."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "No se pudieron guardar los archivos de la fuente. Por favor, verifica que wp-content/fonts tenga permisos de escritura."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "No se pudo crear el directorio de fuentes. Por favor, verifica los permisos de archivos."
msgid "Invalid file path."
msgstr "Ruta de archivo inválida."
msgid "Invalid font URL."
msgstr "URL de fuente inválida."
msgid "Invalid font name."
msgstr "Nombre de fuente inválido."
msgid "No valid weights specified."
msgstr "No se especificaron pesos válidos."
msgid "No valid styles specified."
msgstr "No se especificaron estilos válidos."
msgid "An unexpected error occurred. Please try again."
msgstr "Ocurrió un error inesperado. Por favor, intenta de nuevo."
msgid "Thin"
msgstr "Delgada"
msgid "Extra Light"
msgstr "Extra ligera"
msgid "Light"
msgstr "Ligera"
msgid "Regular"
msgstr "Normal"
msgid "Medium"
msgstr "Media"
msgid "Semi Bold"
msgstr "Semi negrita"
msgid "Bold"
msgstr "Negrita"
msgid "Extra Bold"
msgstr "Extra negrita"
msgid "Black"
msgstr "Negra"
msgid "You do not have sufficient permissions to access this page."
msgstr "No tienes permisos suficientes para acceder a esta página."
msgid "Import from Google Fonts"
msgstr "Importar desde Google Fonts"
msgid "Font Name"
msgstr "Nombre de la fuente"
msgid "e.g., Open Sans"
msgstr "ej., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Ingresa el nombre exacto de la fuente como aparece en Google Fonts."
msgid "Weights"
msgstr "Pesos"
msgid "Styles"
msgstr "Estilos"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Cursiva"
msgid "Files to download:"
msgstr "Archivos a descargar:"
msgid "Download & Install"
msgstr "Descargar e instalar"
msgid "Installed Fonts"
msgstr "Fuentes instaladas"
msgid "No fonts installed yet."
msgstr "Aún no hay fuentes instaladas."
msgid "Delete"
msgstr "Eliminar"
msgid "Use %s to apply fonts to your site."
msgstr "Usa %s para aplicar fuentes a tu sitio."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Apariencia → Editor → Estilos → Tipografía"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Spanish (Spain)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Spanish (Spain)\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importa fuentes de Google Fonts al almacenamiento local y regístralas con la biblioteca de fuentes de WordPress para una tipografía compatible con el RGPD y respetuosa con la privacidad."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts requiere WordPress 6.5 o superior para el soporte de la biblioteca de fuentes."
msgid "Plugin Activation Error"
msgstr "Error de activación del plugin"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts ha sido desactivado. Requiere WordPress 6.5 o superior."
msgid "Local Fonts"
msgstr "Fuentes locales"
msgid "Downloading..."
msgstr "Descargando..."
msgid "Deleting..."
msgstr "Eliminando..."
msgid "Are you sure you want to delete this font?"
msgstr "¿Estás seguro de que quieres eliminar esta fuente?"
msgid "An error occurred. Please try again."
msgstr "Se ha producido un error. Por favor, inténtalo de nuevo."
msgid "Please select at least one weight."
msgstr "Por favor, selecciona al menos un peso."
msgid "Please select at least one style."
msgstr "Por favor, selecciona al menos un estilo."
msgid "Please enter a font name."
msgstr "Por favor, introduce un nombre de fuente."
msgid "Security check failed."
msgstr "Ha fallado la verificación de seguridad."
msgid "Unauthorized."
msgstr "No autorizado."
msgid "Font name is required."
msgstr "El nombre de la fuente es obligatorio."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nombre de fuente no válido: solo se permiten letras, números, espacios y guiones."
msgid "Font name is too long."
msgstr "El nombre de la fuente es demasiado largo."
msgid "At least one weight is required."
msgstr "Se requiere al menos un peso."
msgid "Too many weights selected."
msgstr "Demasiados pesos seleccionados."
msgid "At least one style is required."
msgstr "Se requiere al menos un estilo."
msgid "Successfully installed %s."
msgstr "%s se ha instalado correctamente."
msgid "An unexpected error occurred."
msgstr "Se ha producido un error inesperado."
msgid "Invalid font ID."
msgstr "ID de fuente no válido."
msgid "Font not found."
msgstr "Fuente no encontrada."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "No se pueden eliminar fuentes que no hayan sido importadas por este plugin."
msgid "Font deleted successfully."
msgstr "Fuente eliminada correctamente."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Fuente no encontrada en Google Fonts. Por favor, comprueba la ortografía e inténtalo de nuevo."
msgid "This font is already installed."
msgstr "Esta fuente ya está instalada."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "No se ha podido conectar a Google Fonts. Por favor, comprueba tu conexión a internet e inténtalo de nuevo."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts ha devuelto un error. Por favor, inténtalo más tarde."
msgid "Could not process the font data. The font may not be available."
msgstr "No se han podido procesar los datos de la fuente. La fuente puede no estar disponible."
msgid "Could not download the font files. Please try again."
msgstr "No se han podido descargar los archivos de la fuente. Por favor, inténtalo de nuevo."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "No se han podido guardar los archivos de la fuente. Por favor, comprueba que wp-content/fonts tenga permisos de escritura."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "No se ha podido crear el directorio de fuentes. Por favor, comprueba los permisos de archivos."
msgid "Invalid file path."
msgstr "Ruta de archivo no válida."
msgid "Invalid font URL."
msgstr "URL de fuente no válida."
msgid "Invalid font name."
msgstr "Nombre de fuente no válido."
msgid "No valid weights specified."
msgstr "No se han especificado pesos válidos."
msgid "No valid styles specified."
msgstr "No se han especificado estilos válidos."
msgid "An unexpected error occurred. Please try again."
msgstr "Se ha producido un error inesperado. Por favor, inténtalo de nuevo."
msgid "Thin"
msgstr "Fina"
msgid "Extra Light"
msgstr "Extra ligera"
msgid "Light"
msgstr "Ligera"
msgid "Regular"
msgstr "Normal"
msgid "Medium"
msgstr "Media"
msgid "Semi Bold"
msgstr "Semi negrita"
msgid "Bold"
msgstr "Negrita"
msgid "Extra Bold"
msgstr "Extra negrita"
msgid "Black"
msgstr "Negra"
msgid "You do not have sufficient permissions to access this page."
msgstr "No tienes permisos suficientes para acceder a esta página."
msgid "Import from Google Fonts"
msgstr "Importar desde Google Fonts"
msgid "Font Name"
msgstr "Nombre de la fuente"
msgid "e.g., Open Sans"
msgstr "ej., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Introduce el nombre exacto de la fuente como aparece en Google Fonts."
msgid "Weights"
msgstr "Pesos"
msgid "Styles"
msgstr "Estilos"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Cursiva"
msgid "Files to download:"
msgstr "Archivos a descargar:"
msgid "Download & Install"
msgstr "Descargar e instalar"
msgid "Installed Fonts"
msgstr "Fuentes instaladas"
msgid "No fonts installed yet."
msgstr "Aún no hay fuentes instaladas."
msgid "Delete"
msgstr "Eliminar"
msgid "Use %s to apply fonts to your site."
msgstr "Usa %s para aplicar fuentes a tu sitio."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Apariencia → Editor → Estilos → Tipografía"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Spanish (Mexico)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Spanish (Mexico)\n"
"Language: es_MX\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importa fuentes de Google Fonts al almacenamiento local y regístralas con la biblioteca de fuentes de WordPress para una tipografía compatible con GDPR y respetuosa con la privacidad."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts requiere WordPress 6.5 o superior para el soporte de la biblioteca de fuentes."
msgid "Plugin Activation Error"
msgstr "Error de activación del plugin"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts ha sido desactivado. Requiere WordPress 6.5 o superior."
msgid "Local Fonts"
msgstr "Fuentes locales"
msgid "Downloading..."
msgstr "Descargando..."
msgid "Deleting..."
msgstr "Eliminando..."
msgid "Are you sure you want to delete this font?"
msgstr "¿Estás seguro de que quieres eliminar esta fuente?"
msgid "An error occurred. Please try again."
msgstr "Ocurrió un error. Por favor, inténtalo de nuevo."
msgid "Please select at least one weight."
msgstr "Por favor, selecciona al menos un peso."
msgid "Please select at least one style."
msgstr "Por favor, selecciona al menos un estilo."
msgid "Please enter a font name."
msgstr "Por favor, ingresa un nombre de fuente."
msgid "Security check failed."
msgstr "Falló la verificación de seguridad."
msgid "Unauthorized."
msgstr "No autorizado."
msgid "Font name is required."
msgstr "El nombre de la fuente es requerido."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nombre de fuente inválido: solo se permiten letras, números, espacios y guiones."
msgid "Font name is too long."
msgstr "El nombre de la fuente es demasiado largo."
msgid "At least one weight is required."
msgstr "Se requiere al menos un peso."
msgid "Too many weights selected."
msgstr "Demasiados pesos seleccionados."
msgid "At least one style is required."
msgstr "Se requiere al menos un estilo."
msgid "Successfully installed %s."
msgstr "%s se instaló correctamente."
msgid "An unexpected error occurred."
msgstr "Ocurrió un error inesperado."
msgid "Invalid font ID."
msgstr "ID de fuente inválido."
msgid "Font not found."
msgstr "Fuente no encontrada."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "No se pueden eliminar fuentes que no fueron importadas por este plugin."
msgid "Font deleted successfully."
msgstr "Fuente eliminada correctamente."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Fuente no encontrada en Google Fonts. Por favor, verifica la ortografía e inténtalo de nuevo."
msgid "This font is already installed."
msgstr "Esta fuente ya está instalada."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "No se pudo conectar a Google Fonts. Por favor, verifica tu conexión a internet e inténtalo de nuevo."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts devolvió un error. Por favor, inténtalo más tarde."
msgid "Could not process the font data. The font may not be available."
msgstr "No se pudieron procesar los datos de la fuente. La fuente puede no estar disponible."
msgid "Could not download the font files. Please try again."
msgstr "No se pudieron descargar los archivos de la fuente. Por favor, inténtalo de nuevo."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "No se pudieron guardar los archivos de la fuente. Por favor, verifica que wp-content/fonts tenga permisos de escritura."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "No se pudo crear el directorio de fuentes. Por favor, verifica los permisos de archivos."
msgid "Invalid file path."
msgstr "Ruta de archivo inválida."
msgid "Invalid font URL."
msgstr "URL de fuente inválida."
msgid "Invalid font name."
msgstr "Nombre de fuente inválido."
msgid "No valid weights specified."
msgstr "No se especificaron pesos válidos."
msgid "No valid styles specified."
msgstr "No se especificaron estilos válidos."
msgid "An unexpected error occurred. Please try again."
msgstr "Ocurrió un error inesperado. Por favor, inténtalo de nuevo."
msgid "Thin"
msgstr "Delgada"
msgid "Extra Light"
msgstr "Extra ligera"
msgid "Light"
msgstr "Ligera"
msgid "Regular"
msgstr "Normal"
msgid "Medium"
msgstr "Media"
msgid "Semi Bold"
msgstr "Semi negrita"
msgid "Bold"
msgstr "Negrita"
msgid "Extra Bold"
msgstr "Extra negrita"
msgid "Black"
msgstr "Negra"
msgid "You do not have sufficient permissions to access this page."
msgstr "No tienes permisos suficientes para acceder a esta página."
msgid "Import from Google Fonts"
msgstr "Importar desde Google Fonts"
msgid "Font Name"
msgstr "Nombre de la fuente"
msgid "e.g., Open Sans"
msgstr "ej., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Ingresa el nombre exacto de la fuente como aparece en Google Fonts."
msgid "Weights"
msgstr "Pesos"
msgid "Styles"
msgstr "Estilos"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Cursiva"
msgid "Files to download:"
msgstr "Archivos a descargar:"
msgid "Download & Install"
msgstr "Descargar e instalar"
msgid "Installed Fonts"
msgstr "Fuentes instaladas"
msgid "No fonts installed yet."
msgstr "Aún no hay fuentes instaladas."
msgid "Delete"
msgstr "Eliminar"
msgid "Use %s to apply fonts to your site."
msgstr "Usa %s para aplicar fuentes a tu sitio."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Apariencia → Editor → Estilos → Tipografía"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in French (Canada)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: French (Canada)\n"
"Language: fr_CA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importez des polices Google Fonts vers le stockage local et enregistrez-les avec la bibliothèque de polices WordPress pour une typographie conforme au RGPD et respectueuse de la vie privée."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts nécessite WordPress 6.5 ou supérieur pour la prise en charge de la bibliothèque de polices."
msgid "Plugin Activation Error"
msgstr "Erreur d'activation de l'extension"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts a été désactivé. Il nécessite WordPress 6.5 ou supérieur."
msgid "Local Fonts"
msgstr "Polices locales"
msgid "Downloading..."
msgstr "Téléchargement..."
msgid "Deleting..."
msgstr "Suppression..."
msgid "Are you sure you want to delete this font?"
msgstr "Êtes-vous certain de vouloir supprimer cette police?"
msgid "An error occurred. Please try again."
msgstr "Une erreur s'est produite. Veuillez réessayer."
msgid "Please select at least one weight."
msgstr "Veuillez sélectionner au moins une graisse."
msgid "Please select at least one style."
msgstr "Veuillez sélectionner au moins un style."
msgid "Please enter a font name."
msgstr "Veuillez entrer un nom de police."
msgid "Security check failed."
msgstr "Échec de la vérification de sécurité."
msgid "Unauthorized."
msgstr "Non autorisé."
msgid "Font name is required."
msgstr "Le nom de la police est requis."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nom de police invalide : seuls les lettres, chiffres, espaces et tirets sont autorisés."
msgid "Font name is too long."
msgstr "Le nom de la police est trop long."
msgid "At least one weight is required."
msgstr "Au moins une graisse est requise."
msgid "Too many weights selected."
msgstr "Trop de graisses sélectionnées."
msgid "At least one style is required."
msgstr "Au moins un style est requis."
msgid "Successfully installed %s."
msgstr "%s a été installé avec succès."
msgid "An unexpected error occurred."
msgstr "Une erreur inattendue s'est produite."
msgid "Invalid font ID."
msgstr "ID de police invalide."
msgid "Font not found."
msgstr "Police introuvable."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Impossible de supprimer les polices non importées par cette extension."
msgid "Font deleted successfully."
msgstr "Police supprimée avec succès."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Police introuvable sur Google Fonts. Veuillez vérifier l'orthographe et réessayer."
msgid "This font is already installed."
msgstr "Cette police est déjà installée."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Connexion à Google Fonts impossible. Veuillez vérifier votre connexion Internet et réessayer."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts a retourné une erreur. Veuillez réessayer plus tard."
msgid "Could not process the font data. The font may not be available."
msgstr "Impossible de traiter les données de la police. La police n'est peut-être pas disponible."
msgid "Could not download the font files. Please try again."
msgstr "Impossible de télécharger les fichiers de police. Veuillez réessayer."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Impossible d'enregistrer les fichiers de police. Veuillez vérifier que wp-content/fonts est accessible en écriture."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Impossible de créer le répertoire des polices. Veuillez vérifier les permissions de fichiers."
msgid "Invalid file path."
msgstr "Chemin de fichier invalide."
msgid "Invalid font URL."
msgstr "URL de police invalide."
msgid "Invalid font name."
msgstr "Nom de police invalide."
msgid "No valid weights specified."
msgstr "Aucune graisse valide spécifiée."
msgid "No valid styles specified."
msgstr "Aucun style valide spécifié."
msgid "An unexpected error occurred. Please try again."
msgstr "Une erreur inattendue s'est produite. Veuillez réessayer."
msgid "Thin"
msgstr "Fin"
msgid "Extra Light"
msgstr "Extra léger"
msgid "Light"
msgstr "Léger"
msgid "Regular"
msgstr "Normal"
msgid "Medium"
msgstr "Moyen"
msgid "Semi Bold"
msgstr "Semi-gras"
msgid "Bold"
msgstr "Gras"
msgid "Extra Bold"
msgstr "Extra gras"
msgid "Black"
msgstr "Noir"
msgid "You do not have sufficient permissions to access this page."
msgstr "Vous n'avez pas les permissions suffisantes pour accéder à cette page."
msgid "Import from Google Fonts"
msgstr "Importer depuis Google Fonts"
msgid "Font Name"
msgstr "Nom de la police"
msgid "e.g., Open Sans"
msgstr "ex. : Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Entrez le nom exact de la police tel qu'il apparaît sur Google Fonts."
msgid "Weights"
msgstr "Graisses"
msgid "Styles"
msgstr "Styles"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Italique"
msgid "Files to download:"
msgstr "Fichiers à télécharger :"
msgid "Download & Install"
msgstr "Télécharger et installer"
msgid "Installed Fonts"
msgstr "Polices installées"
msgid "No fonts installed yet."
msgstr "Aucune police installée pour le moment."
msgid "Delete"
msgstr "Supprimer"
msgid "Use %s to apply fonts to your site."
msgstr "Utilisez %s pour appliquer les polices à votre site."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Apparence → Éditeur → Styles → Typographie"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in French (France)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: French (France)\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importez des polices Google Fonts vers le stockage local et enregistrez-les avec la bibliothèque de polices WordPress pour une typographie conforme au RGPD et respectueuse de la vie privée."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts nécessite WordPress 6.5 ou supérieur pour la prise en charge de la bibliothèque de polices."
msgid "Plugin Activation Error"
msgstr "Erreur d'activation de l'extension"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts a été désactivé. Il nécessite WordPress 6.5 ou supérieur."
msgid "Local Fonts"
msgstr "Polices locales"
msgid "Downloading..."
msgstr "Téléchargement..."
msgid "Deleting..."
msgstr "Suppression..."
msgid "Are you sure you want to delete this font?"
msgstr "Êtes-vous sûr de vouloir supprimer cette police ?"
msgid "An error occurred. Please try again."
msgstr "Une erreur s'est produite. Veuillez réessayer."
msgid "Please select at least one weight."
msgstr "Veuillez sélectionner au moins une graisse."
msgid "Please select at least one style."
msgstr "Veuillez sélectionner au moins un style."
msgid "Please enter a font name."
msgstr "Veuillez saisir un nom de police."
msgid "Security check failed."
msgstr "Échec de la vérification de sécurité."
msgid "Unauthorized."
msgstr "Non autorisé."
msgid "Font name is required."
msgstr "Le nom de la police est requis."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nom de police non valide : seuls les lettres, chiffres, espaces et tirets sont autorisés."
msgid "Font name is too long."
msgstr "Le nom de la police est trop long."
msgid "At least one weight is required."
msgstr "Au moins une graisse est requise."
msgid "Too many weights selected."
msgstr "Trop de graisses sélectionnées."
msgid "At least one style is required."
msgstr "Au moins un style est requis."
msgid "Successfully installed %s."
msgstr "%s a été installé avec succès."
msgid "An unexpected error occurred."
msgstr "Une erreur inattendue s'est produite."
msgid "Invalid font ID."
msgstr "ID de police non valide."
msgid "Font not found."
msgstr "Police introuvable."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Impossible de supprimer les polices non importées par cette extension."
msgid "Font deleted successfully."
msgstr "Police supprimée avec succès."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Police introuvable sur Google Fonts. Veuillez vérifier l'orthographe et réessayer."
msgid "This font is already installed."
msgstr "Cette police est déjà installée."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Impossible de se connecter à Google Fonts. Veuillez vérifier votre connexion Internet et réessayer."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts a renvoyé une erreur. Veuillez réessayer plus tard."
msgid "Could not process the font data. The font may not be available."
msgstr "Impossible de traiter les données de la police. La police n'est peut-être pas disponible."
msgid "Could not download the font files. Please try again."
msgstr "Impossible de télécharger les fichiers de police. Veuillez réessayer."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Impossible d'enregistrer les fichiers de police. Veuillez vérifier que wp-content/fonts est accessible en écriture."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Impossible de créer le répertoire des polices. Veuillez vérifier les droits d'accès aux fichiers."
msgid "Invalid file path."
msgstr "Chemin de fichier non valide."
msgid "Invalid font URL."
msgstr "URL de police non valide."
msgid "Invalid font name."
msgstr "Nom de police non valide."
msgid "No valid weights specified."
msgstr "Aucune graisse valide spécifiée."
msgid "No valid styles specified."
msgstr "Aucun style valide spécifié."
msgid "An unexpected error occurred. Please try again."
msgstr "Une erreur inattendue s'est produite. Veuillez réessayer."
msgid "Thin"
msgstr "Fin"
msgid "Extra Light"
msgstr "Extra léger"
msgid "Light"
msgstr "Léger"
msgid "Regular"
msgstr "Normal"
msgid "Medium"
msgstr "Moyen"
msgid "Semi Bold"
msgstr "Semi-gras"
msgid "Bold"
msgstr "Gras"
msgid "Extra Bold"
msgstr "Extra gras"
msgid "Black"
msgstr "Noir"
msgid "You do not have sufficient permissions to access this page."
msgstr "Vous n'avez pas les droits suffisants pour accéder à cette page."
msgid "Import from Google Fonts"
msgstr "Importer depuis Google Fonts"
msgid "Font Name"
msgstr "Nom de la police"
msgid "e.g., Open Sans"
msgstr "ex. : Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Saisissez le nom exact de la police tel qu'il apparaît sur Google Fonts."
msgid "Weights"
msgstr "Graisses"
msgid "Styles"
msgstr "Styles"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Italique"
msgid "Files to download:"
msgstr "Fichiers à télécharger :"
msgid "Download & Install"
msgstr "Télécharger et installer"
msgid "Installed Fonts"
msgstr "Polices installées"
msgid "No fonts installed yet."
msgstr "Aucune police installée pour le moment."
msgid "Delete"
msgstr "Supprimer"
msgid "Use %s to apply fonts to your site."
msgstr "Utilisez %s pour appliquer les polices à votre site."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Apparence → Éditeur → Styles → Typographie"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Hindi
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Hindi\n"
"Language: hi_IN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Google Fonts को स्थानीय भंडारण में आयात करें और उन्हें GDPR-अनुपालन और गोपनीयता-अनुकूल टाइपोग्राफी के लिए WordPress फ़ॉन्ट लाइब्रेरी में पंजीकृत करें।"
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts को फ़ॉन्ट लाइब्रेरी सहायता के लिए WordPress 6.5 या उच्चतर की आवश्यकता है।"
msgid "Plugin Activation Error"
msgstr "प्लगइन सक्रियण त्रुटि"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts निष्क्रिय कर दिया गया है। इसके लिए WordPress 6.5 या उच्चतर आवश्यक है।"
msgid "Local Fonts"
msgstr "स्थानीय फ़ॉन्ट"
msgid "Downloading..."
msgstr "डाउनलोड हो रहा है..."
msgid "Deleting..."
msgstr "हटाया जा रहा है..."
msgid "Are you sure you want to delete this font?"
msgstr "क्या आप वाकई इस फ़ॉन्ट को हटाना चाहते हैं?"
msgid "An error occurred. Please try again."
msgstr "एक त्रुटि हुई। कृपया पुनः प्रयास करें।"
msgid "Please select at least one weight."
msgstr "कृपया कम से कम एक वज़न चुनें।"
msgid "Please select at least one style."
msgstr "कृपया कम से कम एक शैली चुनें।"
msgid "Please enter a font name."
msgstr "कृपया फ़ॉन्ट का नाम दर्ज करें।"
msgid "Security check failed."
msgstr "सुरक्षा जांच विफल।"
msgid "Unauthorized."
msgstr "अनधिकृत।"
msgid "Font name is required."
msgstr "फ़ॉन्ट नाम आवश्यक है।"
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "अमान्य फ़ॉन्ट नाम: केवल अक्षर, संख्याएं, रिक्त स्थान और हाइफ़न की अनुमति है।"
msgid "Font name is too long."
msgstr "फ़ॉन्ट नाम बहुत लंबा है।"
msgid "At least one weight is required."
msgstr "कम से कम एक वज़न आवश्यक है।"
msgid "Too many weights selected."
msgstr "बहुत अधिक वज़न चुने गए।"
msgid "At least one style is required."
msgstr "कम से कम एक शैली आवश्यक है।"
msgid "Successfully installed %s."
msgstr "%s सफलतापूर्वक स्थापित किया गया।"
msgid "An unexpected error occurred."
msgstr "एक अप्रत्याशित त्रुटि हुई।"
msgid "Invalid font ID."
msgstr "अमान्य फ़ॉन्ट ID।"
msgid "Font not found."
msgstr "फ़ॉन्ट नहीं मिला।"
msgid "Cannot delete fonts not imported by this plugin."
msgstr "इस प्लगइन द्वारा आयात नहीं किए गए फ़ॉन्ट को हटाया नहीं जा सकता।"
msgid "Font deleted successfully."
msgstr "फ़ॉन्ट सफलतापूर्वक हटाया गया।"
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Google Fonts पर फ़ॉन्ट नहीं मिला। कृपया वर्तनी जांचें और पुनः प्रयास करें।"
msgid "This font is already installed."
msgstr "यह फ़ॉन्ट पहले से स्थापित है।"
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Google Fonts से कनेक्ट नहीं हो सका। कृपया अपना इंटरनेट कनेक्शन जांचें और पुनः प्रयास करें।"
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts ने एक त्रुटि लौटाई। कृपया बाद में पुनः प्रयास करें।"
msgid "Could not process the font data. The font may not be available."
msgstr "फ़ॉन्ट डेटा को संसाधित नहीं किया जा सका। फ़ॉन्ट उपलब्ध नहीं हो सकता है।"
msgid "Could not download the font files. Please try again."
msgstr "फ़ॉन्ट फ़ाइलें डाउनलोड नहीं हो सकीं। कृपया पुनः प्रयास करें।"
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "फ़ॉन्ट फ़ाइलें सहेजी नहीं जा सकीं। कृपया जांचें कि wp-content/fonts लिखने योग्य है।"
msgid "Could not create fonts directory. Please check file permissions."
msgstr "फ़ॉन्ट निर्देशिका नहीं बनाई जा सकी। कृपया फ़ाइल अनुमतियां जांचें।"
msgid "Invalid file path."
msgstr "अमान्य फ़ाइल पथ।"
msgid "Invalid font URL."
msgstr "अमान्य फ़ॉन्ट URL।"
msgid "Invalid font name."
msgstr "अमान्य फ़ॉन्ट नाम।"
msgid "No valid weights specified."
msgstr "कोई वैध वज़न निर्दिष्ट नहीं।"
msgid "No valid styles specified."
msgstr "कोई वैध शैली निर्दिष्ट नहीं।"
msgid "An unexpected error occurred. Please try again."
msgstr "एक अप्रत्याशित त्रुटि हुई। कृपया पुनः प्रयास करें।"
msgid "Thin"
msgstr "पतला"
msgid "Extra Light"
msgstr "अति हल्का"
msgid "Light"
msgstr "हल्का"
msgid "Regular"
msgstr "सामान्य"
msgid "Medium"
msgstr "मध्यम"
msgid "Semi Bold"
msgstr "अर्ध-मोटा"
msgid "Bold"
msgstr "मोटा"
msgid "Extra Bold"
msgstr "अति मोटा"
msgid "Black"
msgstr "काला"
msgid "You do not have sufficient permissions to access this page."
msgstr "आपके पास इस पृष्ठ तक पहुंचने की पर्याप्त अनुमति नहीं है।"
msgid "Import from Google Fonts"
msgstr "Google Fonts से आयात करें"
msgid "Font Name"
msgstr "फ़ॉन्ट नाम"
msgid "e.g., Open Sans"
msgstr "उदा., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Google Fonts पर दिखाई देने वाला सटीक फ़ॉन्ट नाम दर्ज करें।"
msgid "Weights"
msgstr "वज़न"
msgid "Styles"
msgstr "शैलियां"
msgid "Normal"
msgstr "सामान्य"
msgid "Italic"
msgstr "इटैलिक"
msgid "Files to download:"
msgstr "डाउनलोड करने के लिए फ़ाइलें:"
msgid "Download & Install"
msgstr "डाउनलोड और इंस्टॉल करें"
msgid "Installed Fonts"
msgstr "स्थापित फ़ॉन्ट"
msgid "No fonts installed yet."
msgstr "अभी तक कोई फ़ॉन्ट स्थापित नहीं।"
msgid "Delete"
msgstr "हटाएं"
msgid "Use %s to apply fonts to your site."
msgstr "अपनी साइट पर फ़ॉन्ट लागू करने के लिए %s का उपयोग करें।"
msgid "Appearance → Editor → Styles → Typography"
msgstr "प्रकटन → संपादक → शैलियां → टाइपोग्राफी"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Indonesian
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Indonesian\n"
"Language: id_ID\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Impor Google Fonts ke penyimpanan lokal dan daftarkan ke Perpustakaan Font WordPress untuk tipografi yang sesuai GDPR dan ramah privasi."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts memerlukan WordPress 6.5 atau lebih tinggi untuk dukungan Perpustakaan Font."
msgid "Plugin Activation Error"
msgstr "Kesalahan Aktivasi Plugin"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts telah dinonaktifkan. Memerlukan WordPress 6.5 atau lebih tinggi."
msgid "Local Fonts"
msgstr "Font Lokal"
msgid "Downloading..."
msgstr "Mengunduh..."
msgid "Deleting..."
msgstr "Menghapus..."
msgid "Are you sure you want to delete this font?"
msgstr "Apakah Anda yakin ingin menghapus font ini?"
msgid "An error occurred. Please try again."
msgstr "Terjadi kesalahan. Silakan coba lagi."
msgid "Please select at least one weight."
msgstr "Silakan pilih setidaknya satu ketebalan."
msgid "Please select at least one style."
msgstr "Silakan pilih setidaknya satu gaya."
msgid "Please enter a font name."
msgstr "Silakan masukkan nama font."
msgid "Security check failed."
msgstr "Pemeriksaan keamanan gagal."
msgid "Unauthorized."
msgstr "Tidak diizinkan."
msgid "Font name is required."
msgstr "Nama font diperlukan."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nama font tidak valid: hanya huruf, angka, spasi, dan tanda hubung yang diperbolehkan."
msgid "Font name is too long."
msgstr "Nama font terlalu panjang."
msgid "At least one weight is required."
msgstr "Setidaknya satu ketebalan diperlukan."
msgid "Too many weights selected."
msgstr "Terlalu banyak ketebalan dipilih."
msgid "At least one style is required."
msgstr "Setidaknya satu gaya diperlukan."
msgid "Successfully installed %s."
msgstr "Berhasil menginstal %s."
msgid "An unexpected error occurred."
msgstr "Terjadi kesalahan yang tidak terduga."
msgid "Invalid font ID."
msgstr "ID font tidak valid."
msgid "Font not found."
msgstr "Font tidak ditemukan."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Tidak dapat menghapus font yang tidak diimpor oleh plugin ini."
msgid "Font deleted successfully."
msgstr "Font berhasil dihapus."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Font tidak ditemukan di Google Fonts. Silakan periksa ejaan dan coba lagi."
msgid "This font is already installed."
msgstr "Font ini sudah terinstal."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Tidak dapat terhubung ke Google Fonts. Silakan periksa koneksi internet Anda dan coba lagi."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts mengembalikan kesalahan. Silakan coba lagi nanti."
msgid "Could not process the font data. The font may not be available."
msgstr "Tidak dapat memproses data font. Font mungkin tidak tersedia."
msgid "Could not download the font files. Please try again."
msgstr "Tidak dapat mengunduh file font. Silakan coba lagi."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Tidak dapat menyimpan file font. Silakan periksa apakah wp-content/fonts dapat ditulis."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Tidak dapat membuat direktori font. Silakan periksa izin file."
msgid "Invalid file path."
msgstr "Jalur file tidak valid."
msgid "Invalid font URL."
msgstr "URL font tidak valid."
msgid "Invalid font name."
msgstr "Nama font tidak valid."
msgid "No valid weights specified."
msgstr "Tidak ada ketebalan valid yang ditentukan."
msgid "No valid styles specified."
msgstr "Tidak ada gaya valid yang ditentukan."
msgid "An unexpected error occurred. Please try again."
msgstr "Terjadi kesalahan yang tidak terduga. Silakan coba lagi."
msgid "Thin"
msgstr "Tipis"
msgid "Extra Light"
msgstr "Ekstra Ringan"
msgid "Light"
msgstr "Ringan"
msgid "Regular"
msgstr "Reguler"
msgid "Medium"
msgstr "Sedang"
msgid "Semi Bold"
msgstr "Semi Tebal"
msgid "Bold"
msgstr "Tebal"
msgid "Extra Bold"
msgstr "Ekstra Tebal"
msgid "Black"
msgstr "Hitam"
msgid "You do not have sufficient permissions to access this page."
msgstr "Anda tidak memiliki izin yang cukup untuk mengakses halaman ini."
msgid "Import from Google Fonts"
msgstr "Impor dari Google Fonts"
msgid "Font Name"
msgstr "Nama Font"
msgid "e.g., Open Sans"
msgstr "contoh: Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Masukkan nama font persis seperti yang muncul di Google Fonts."
msgid "Weights"
msgstr "Ketebalan"
msgid "Styles"
msgstr "Gaya"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Miring"
msgid "Files to download:"
msgstr "File untuk diunduh:"
msgid "Download & Install"
msgstr "Unduh & Instal"
msgid "Installed Fonts"
msgstr "Font Terinstal"
msgid "No fonts installed yet."
msgstr "Belum ada font yang terinstal."
msgid "Delete"
msgstr "Hapus"
msgid "Use %s to apply fonts to your site."
msgstr "Gunakan %s untuk menerapkan font ke situs Anda."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Tampilan → Editor → Gaya → Tipografi"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Italian
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importa i font di Google Fonts nello storage locale e registrali nella libreria font di WordPress per una tipografia conforme al GDPR e rispettosa della privacy."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts richiede WordPress 6.5 o superiore per il supporto della libreria font."
msgid "Plugin Activation Error"
msgstr "Errore di attivazione del plugin"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts è stato disattivato. Richiede WordPress 6.5 o superiore."
msgid "Local Fonts"
msgstr "Font locali"
msgid "Downloading..."
msgstr "Download in corso..."
msgid "Deleting..."
msgstr "Eliminazione in corso..."
msgid "Are you sure you want to delete this font?"
msgstr "Sei sicuro di voler eliminare questo font?"
msgid "An error occurred. Please try again."
msgstr "Si è verificato un errore. Riprova."
msgid "Please select at least one weight."
msgstr "Seleziona almeno un peso."
msgid "Please select at least one style."
msgstr "Seleziona almeno uno stile."
msgid "Please enter a font name."
msgstr "Inserisci un nome del font."
msgid "Security check failed."
msgstr "Controllo di sicurezza fallito."
msgid "Unauthorized."
msgstr "Non autorizzato."
msgid "Font name is required."
msgstr "Il nome del font è obbligatorio."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nome del font non valido: sono consentiti solo lettere, numeri, spazi e trattini."
msgid "Font name is too long."
msgstr "Il nome del font è troppo lungo."
msgid "At least one weight is required."
msgstr "È richiesto almeno un peso."
msgid "Too many weights selected."
msgstr "Troppi pesi selezionati."
msgid "At least one style is required."
msgstr "È richiesto almeno uno stile."
msgid "Successfully installed %s."
msgstr "%s è stato installato con successo."
msgid "An unexpected error occurred."
msgstr "Si è verificato un errore imprevisto."
msgid "Invalid font ID."
msgstr "ID font non valido."
msgid "Font not found."
msgstr "Font non trovato."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Impossibile eliminare i font non importati da questo plugin."
msgid "Font deleted successfully."
msgstr "Font eliminato con successo."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Font non trovato su Google Fonts. Controlla l'ortografia e riprova."
msgid "This font is already installed."
msgstr "Questo font è già installato."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Impossibile connettersi a Google Fonts. Controlla la tua connessione internet e riprova."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts ha restituito un errore. Riprova più tardi."
msgid "Could not process the font data. The font may not be available."
msgstr "Impossibile elaborare i dati del font. Il font potrebbe non essere disponibile."
msgid "Could not download the font files. Please try again."
msgstr "Impossibile scaricare i file del font. Riprova."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Impossibile salvare i file del font. Verifica che wp-content/fonts sia scrivibile."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Impossibile creare la directory dei font. Controlla i permessi dei file."
msgid "Invalid file path."
msgstr "Percorso file non valido."
msgid "Invalid font URL."
msgstr "URL del font non valido."
msgid "Invalid font name."
msgstr "Nome del font non valido."
msgid "No valid weights specified."
msgstr "Nessun peso valido specificato."
msgid "No valid styles specified."
msgstr "Nessuno stile valido specificato."
msgid "An unexpected error occurred. Please try again."
msgstr "Si è verificato un errore imprevisto. Riprova."
msgid "Thin"
msgstr "Sottile"
msgid "Extra Light"
msgstr "Extra leggero"
msgid "Light"
msgstr "Leggero"
msgid "Regular"
msgstr "Normale"
msgid "Medium"
msgstr "Medio"
msgid "Semi Bold"
msgstr "Semi grassetto"
msgid "Bold"
msgstr "Grassetto"
msgid "Extra Bold"
msgstr "Extra grassetto"
msgid "Black"
msgstr "Nero"
msgid "You do not have sufficient permissions to access this page."
msgstr "Non hai i permessi sufficienti per accedere a questa pagina."
msgid "Import from Google Fonts"
msgstr "Importa da Google Fonts"
msgid "Font Name"
msgstr "Nome del font"
msgid "e.g., Open Sans"
msgstr "es. Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Inserisci il nome esatto del font come appare su Google Fonts."
msgid "Weights"
msgstr "Pesi"
msgid "Styles"
msgstr "Stili"
msgid "Normal"
msgstr "Normale"
msgid "Italic"
msgstr "Corsivo"
msgid "Files to download:"
msgstr "File da scaricare:"
msgid "Download & Install"
msgstr "Scarica e installa"
msgid "Installed Fonts"
msgstr "Font installati"
msgid "No fonts installed yet."
msgstr "Nessun font installato."
msgid "Delete"
msgstr "Elimina"
msgid "Use %s to apply fonts to your site."
msgstr "Usa %s per applicare i font al tuo sito."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Aspetto → Editor → Stili → Tipografia"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Japanese
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Japanese\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Google FontsをローカルストレージにインポートしてWordPressフォントライブラリに登録し、GDPR準拠でプライバシーに配慮したタイポグラフィを実現します。"
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local FontsはフォントライブラリのサポートにWordPress 6.5以上が必要です。"
msgid "Plugin Activation Error"
msgstr "プラグイン有効化エラー"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fontsは無効化されました。WordPress 6.5以上が必要です。"
msgid "Local Fonts"
msgstr "ローカルフォント"
msgid "Downloading..."
msgstr "ダウンロード中..."
msgid "Deleting..."
msgstr "削除中..."
msgid "Are you sure you want to delete this font?"
msgstr "このフォントを削除してもよろしいですか?"
msgid "An error occurred. Please try again."
msgstr "エラーが発生しました。もう一度お試しください。"
msgid "Please select at least one weight."
msgstr "少なくとも1つのウェイトを選択してください。"
msgid "Please select at least one style."
msgstr "少なくとも1つのスタイルを選択してください。"
msgid "Please enter a font name."
msgstr "フォント名を入力してください。"
msgid "Security check failed."
msgstr "セキュリティチェックに失敗しました。"
msgid "Unauthorized."
msgstr "権限がありません。"
msgid "Font name is required."
msgstr "フォント名は必須です。"
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "無効なフォント名:文字、数字、スペース、ハイフンのみ使用できます。"
msgid "Font name is too long."
msgstr "フォント名が長すぎます。"
msgid "At least one weight is required."
msgstr "少なくとも1つのウェイトが必要です。"
msgid "Too many weights selected."
msgstr "選択されたウェイトが多すぎます。"
msgid "At least one style is required."
msgstr "少なくとも1つのスタイルが必要です。"
msgid "Successfully installed %s."
msgstr "%sのインストールに成功しました。"
msgid "An unexpected error occurred."
msgstr "予期しないエラーが発生しました。"
msgid "Invalid font ID."
msgstr "無効なフォントID。"
msgid "Font not found."
msgstr "フォントが見つかりません。"
msgid "Cannot delete fonts not imported by this plugin."
msgstr "このプラグインでインポートされていないフォントは削除できません。"
msgid "Font deleted successfully."
msgstr "フォントが正常に削除されました。"
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Google Fontsでフォントが見つかりません。スペルを確認して、もう一度お試しください。"
msgid "This font is already installed."
msgstr "このフォントは既にインストールされています。"
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Google Fontsに接続できませんでした。インターネット接続を確認して、もう一度お試しください。"
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fontsがエラーを返しました。後でもう一度お試しください。"
msgid "Could not process the font data. The font may not be available."
msgstr "フォントデータを処理できませんでした。フォントは利用できない可能性があります。"
msgid "Could not download the font files. Please try again."
msgstr "フォントファイルをダウンロードできませんでした。もう一度お試しください。"
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "フォントファイルを保存できませんでした。wp-content/fontsが書き込み可能か確認してください。"
msgid "Could not create fonts directory. Please check file permissions."
msgstr "フォントディレクトリを作成できませんでした。ファイルのパーミッションを確認してください。"
msgid "Invalid file path."
msgstr "無効なファイルパス。"
msgid "Invalid font URL."
msgstr "無効なフォントURL。"
msgid "Invalid font name."
msgstr "無効なフォント名。"
msgid "No valid weights specified."
msgstr "有効なウェイトが指定されていません。"
msgid "No valid styles specified."
msgstr "有効なスタイルが指定されていません。"
msgid "An unexpected error occurred. Please try again."
msgstr "予期しないエラーが発生しました。もう一度お試しください。"
msgid "Thin"
msgstr "極細"
msgid "Extra Light"
msgstr "極細"
msgid "Light"
msgstr "細字"
msgid "Regular"
msgstr "標準"
msgid "Medium"
msgstr "中字"
msgid "Semi Bold"
msgstr "やや太字"
msgid "Bold"
msgstr "太字"
msgid "Extra Bold"
msgstr "極太"
msgid "Black"
msgstr "極太"
msgid "You do not have sufficient permissions to access this page."
msgstr "このページにアクセスする権限がありません。"
msgid "Import from Google Fonts"
msgstr "Google Fontsからインポート"
msgid "Font Name"
msgstr "フォント名"
msgid "e.g., Open Sans"
msgstr "例Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Google Fontsに表示されている正確なフォント名を入力してください。"
msgid "Weights"
msgstr "ウェイト"
msgid "Styles"
msgstr "スタイル"
msgid "Normal"
msgstr "標準"
msgid "Italic"
msgstr "イタリック"
msgid "Files to download:"
msgstr "ダウンロードするファイル:"
msgid "Download & Install"
msgstr "ダウンロード&インストール"
msgid "Installed Fonts"
msgstr "インストール済みフォント"
msgid "No fonts installed yet."
msgstr "まだフォントがインストールされていません。"
msgid "Delete"
msgstr "削除"
msgid "Use %s to apply fonts to your site."
msgstr "%sを使用してサイトにフォントを適用してください。"
msgid "Appearance → Editor → Styles → Typography"
msgstr "外観 → エディター → スタイル → タイポグラフィ"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Korean
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Google Fonts를 로컬 저장소로 가져와 WordPress 글꼴 라이브러리에 등록하여 GDPR 준수 및 개인정보 보호 친화적인 타이포그래피를 구현합니다."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts는 글꼴 라이브러리 지원을 위해 WordPress 6.5 이상이 필요합니다."
msgid "Plugin Activation Error"
msgstr "플러그인 활성화 오류"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts가 비활성화되었습니다. WordPress 6.5 이상이 필요합니다."
msgid "Local Fonts"
msgstr "로컬 글꼴"
msgid "Downloading..."
msgstr "다운로드 중..."
msgid "Deleting..."
msgstr "삭제 중..."
msgid "Are you sure you want to delete this font?"
msgstr "이 글꼴을 삭제하시겠습니까?"
msgid "An error occurred. Please try again."
msgstr "오류가 발생했습니다. 다시 시도해 주세요."
msgid "Please select at least one weight."
msgstr "최소 하나의 굵기를 선택해 주세요."
msgid "Please select at least one style."
msgstr "최소 하나의 스타일을 선택해 주세요."
msgid "Please enter a font name."
msgstr "글꼴 이름을 입력해 주세요."
msgid "Security check failed."
msgstr "보안 검사 실패."
msgid "Unauthorized."
msgstr "권한이 없습니다."
msgid "Font name is required."
msgstr "글꼴 이름은 필수입니다."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "잘못된 글꼴 이름: 문자, 숫자, 공백 및 하이픈만 허용됩니다."
msgid "Font name is too long."
msgstr "글꼴 이름이 너무 깁니다."
msgid "At least one weight is required."
msgstr "최소 하나의 굵기가 필요합니다."
msgid "Too many weights selected."
msgstr "너무 많은 굵기가 선택되었습니다."
msgid "At least one style is required."
msgstr "최소 하나의 스타일이 필요합니다."
msgid "Successfully installed %s."
msgstr "%s이(가) 성공적으로 설치되었습니다."
msgid "An unexpected error occurred."
msgstr "예기치 않은 오류가 발생했습니다."
msgid "Invalid font ID."
msgstr "잘못된 글꼴 ID."
msgid "Font not found."
msgstr "글꼴을 찾을 수 없습니다."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "이 플러그인으로 가져오지 않은 글꼴은 삭제할 수 없습니다."
msgid "Font deleted successfully."
msgstr "글꼴이 성공적으로 삭제되었습니다."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Google Fonts에서 글꼴을 찾을 수 없습니다. 철자를 확인하고 다시 시도해 주세요."
msgid "This font is already installed."
msgstr "이 글꼴은 이미 설치되어 있습니다."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Google Fonts에 연결할 수 없습니다. 인터넷 연결을 확인하고 다시 시도해 주세요."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts에서 오류가 반환되었습니다. 나중에 다시 시도해 주세요."
msgid "Could not process the font data. The font may not be available."
msgstr "글꼴 데이터를 처리할 수 없습니다. 글꼴을 사용할 수 없을 수 있습니다."
msgid "Could not download the font files. Please try again."
msgstr "글꼴 파일을 다운로드할 수 없습니다. 다시 시도해 주세요."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "글꼴 파일을 저장할 수 없습니다. wp-content/fonts가 쓰기 가능한지 확인해 주세요."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "글꼴 디렉토리를 생성할 수 없습니다. 파일 권한을 확인해 주세요."
msgid "Invalid file path."
msgstr "잘못된 파일 경로."
msgid "Invalid font URL."
msgstr "잘못된 글꼴 URL."
msgid "Invalid font name."
msgstr "잘못된 글꼴 이름."
msgid "No valid weights specified."
msgstr "유효한 굵기가 지정되지 않았습니다."
msgid "No valid styles specified."
msgstr "유효한 스타일이 지정되지 않았습니다."
msgid "An unexpected error occurred. Please try again."
msgstr "예기치 않은 오류가 발생했습니다. 다시 시도해 주세요."
msgid "Thin"
msgstr "가늘게"
msgid "Extra Light"
msgstr "매우 가볍게"
msgid "Light"
msgstr "가볍게"
msgid "Regular"
msgstr "보통"
msgid "Medium"
msgstr "중간"
msgid "Semi Bold"
msgstr "약간 굵게"
msgid "Bold"
msgstr "굵게"
msgid "Extra Bold"
msgstr "매우 굵게"
msgid "Black"
msgstr "검정"
msgid "You do not have sufficient permissions to access this page."
msgstr "이 페이지에 접근할 권한이 없습니다."
msgid "Import from Google Fonts"
msgstr "Google Fonts에서 가져오기"
msgid "Font Name"
msgstr "글꼴 이름"
msgid "e.g., Open Sans"
msgstr "예: Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Google Fonts에 표시된 정확한 글꼴 이름을 입력하세요."
msgid "Weights"
msgstr "굵기"
msgid "Styles"
msgstr "스타일"
msgid "Normal"
msgstr "보통"
msgid "Italic"
msgstr "기울임꼴"
msgid "Files to download:"
msgstr "다운로드할 파일:"
msgid "Download & Install"
msgstr "다운로드 및 설치"
msgid "Installed Fonts"
msgstr "설치된 글꼴"
msgid "No fonts installed yet."
msgstr "아직 설치된 글꼴이 없습니다."
msgid "Delete"
msgstr "삭제"
msgid "Use %s to apply fonts to your site."
msgstr "%s을(를) 사용하여 사이트에 글꼴을 적용하세요."
msgid "Appearance → Editor → Styles → Typography"
msgstr "외모 → 편집기 → 스타일 → 타이포그래피"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Dutch
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importeer Google Fonts naar lokale opslag en registreer ze bij de WordPress Lettertypebibliotheek voor GDPR-conforme, privacyvriendelijke typografie."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts vereist WordPress 6.5 of hoger voor ondersteuning van de Lettertypebibliotheek."
msgid "Plugin Activation Error"
msgstr "Plugin-activeringsfout"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts is gedeactiveerd. Het vereist WordPress 6.5 of hoger."
msgid "Local Fonts"
msgstr "Lokale lettertypen"
msgid "Downloading..."
msgstr "Downloaden..."
msgid "Deleting..."
msgstr "Verwijderen..."
msgid "Are you sure you want to delete this font?"
msgstr "Weet je zeker dat je dit lettertype wilt verwijderen?"
msgid "An error occurred. Please try again."
msgstr "Er is een fout opgetreden. Probeer het opnieuw."
msgid "Please select at least one weight."
msgstr "Selecteer ten minste één gewicht."
msgid "Please select at least one style."
msgstr "Selecteer ten minste één stijl."
msgid "Please enter a font name."
msgstr "Voer een lettertypenaam in."
msgid "Security check failed."
msgstr "Beveiligingscontrole mislukt."
msgid "Unauthorized."
msgstr "Niet geautoriseerd."
msgid "Font name is required."
msgstr "Lettertypenaam is vereist."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Ongeldige lettertypenaam: alleen letters, cijfers, spaties en koppeltekens toegestaan."
msgid "Font name is too long."
msgstr "Lettertypenaam is te lang."
msgid "At least one weight is required."
msgstr "Ten minste één gewicht is vereist."
msgid "Too many weights selected."
msgstr "Te veel gewichten geselecteerd."
msgid "At least one style is required."
msgstr "Ten minste één stijl is vereist."
msgid "Successfully installed %s."
msgstr "%s is succesvol geïnstalleerd."
msgid "An unexpected error occurred."
msgstr "Er is een onverwachte fout opgetreden."
msgid "Invalid font ID."
msgstr "Ongeldige lettertype-ID."
msgid "Font not found."
msgstr "Lettertype niet gevonden."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Kan lettertypen die niet door deze plugin zijn geïmporteerd niet verwijderen."
msgid "Font deleted successfully."
msgstr "Lettertype succesvol verwijderd."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Lettertype niet gevonden op Google Fonts. Controleer de spelling en probeer het opnieuw."
msgid "This font is already installed."
msgstr "Dit lettertype is al geïnstalleerd."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Kan geen verbinding maken met Google Fonts. Controleer je internetverbinding en probeer het opnieuw."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts heeft een fout geretourneerd. Probeer het later opnieuw."
msgid "Could not process the font data. The font may not be available."
msgstr "Kan de lettertypegegevens niet verwerken. Het lettertype is mogelijk niet beschikbaar."
msgid "Could not download the font files. Please try again."
msgstr "Kan de lettertypebestanden niet downloaden. Probeer het opnieuw."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Kan lettertypebestanden niet opslaan. Controleer of wp-content/fonts schrijfbaar is."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Kan lettertypemap niet aanmaken. Controleer de bestandsrechten."
msgid "Invalid file path."
msgstr "Ongeldig bestandspad."
msgid "Invalid font URL."
msgstr "Ongeldige lettertype-URL."
msgid "Invalid font name."
msgstr "Ongeldige lettertypenaam."
msgid "No valid weights specified."
msgstr "Geen geldige gewichten opgegeven."
msgid "No valid styles specified."
msgstr "Geen geldige stijlen opgegeven."
msgid "An unexpected error occurred. Please try again."
msgstr "Er is een onverwachte fout opgetreden. Probeer het opnieuw."
msgid "Thin"
msgstr "Dun"
msgid "Extra Light"
msgstr "Extra licht"
msgid "Light"
msgstr "Licht"
msgid "Regular"
msgstr "Normaal"
msgid "Medium"
msgstr "Medium"
msgid "Semi Bold"
msgstr "Halfvet"
msgid "Bold"
msgstr "Vet"
msgid "Extra Bold"
msgstr "Extra vet"
msgid "Black"
msgstr "Zwart"
msgid "You do not have sufficient permissions to access this page."
msgstr "Je hebt onvoldoende rechten om deze pagina te openen."
msgid "Import from Google Fonts"
msgstr "Importeren van Google Fonts"
msgid "Font Name"
msgstr "Lettertypenaam"
msgid "e.g., Open Sans"
msgstr "bijv. Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Voer de exacte lettertypenaam in zoals deze wordt weergegeven op Google Fonts."
msgid "Weights"
msgstr "Gewichten"
msgid "Styles"
msgstr "Stijlen"
msgid "Normal"
msgstr "Normaal"
msgid "Italic"
msgstr "Cursief"
msgid "Files to download:"
msgstr "Te downloaden bestanden:"
msgid "Download & Install"
msgstr "Downloaden en installeren"
msgid "Installed Fonts"
msgstr "Geïnstalleerde lettertypen"
msgid "No fonts installed yet."
msgstr "Nog geen lettertypen geïnstalleerd."
msgid "Delete"
msgstr "Verwijderen"
msgid "Use %s to apply fonts to your site."
msgstr "Gebruik %s om lettertypen op je site toe te passen."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Weergave → Editor → Stijlen → Typografie"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Polish
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Polish\n"
"Language: pl_PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importuj czcionki Google Fonts do lokalnego magazynu i zarejestruj je w bibliotece czcionek WordPress dla typografii zgodnej z RODO i przyjaznej dla prywatności."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts wymaga WordPress 6.5 lub nowszego do obsługi biblioteki czcionek."
msgid "Plugin Activation Error"
msgstr "Błąd aktywacji wtyczki"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts została dezaktywowana. Wymaga WordPress 6.5 lub nowszego."
msgid "Local Fonts"
msgstr "Lokalne czcionki"
msgid "Downloading..."
msgstr "Pobieranie..."
msgid "Deleting..."
msgstr "Usuwanie..."
msgid "Are you sure you want to delete this font?"
msgstr "Czy na pewno chcesz usunąć tę czcionkę?"
msgid "An error occurred. Please try again."
msgstr "Wystąpił błąd. Spróbuj ponownie."
msgid "Please select at least one weight."
msgstr "Wybierz co najmniej jedną grubość."
msgid "Please select at least one style."
msgstr "Wybierz co najmniej jeden styl."
msgid "Please enter a font name."
msgstr "Wprowadź nazwę czcionki."
msgid "Security check failed."
msgstr "Sprawdzenie zabezpieczeń nie powiodło się."
msgid "Unauthorized."
msgstr "Brak autoryzacji."
msgid "Font name is required."
msgstr "Nazwa czcionki jest wymagana."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nieprawidłowa nazwa czcionki: dozwolone są tylko litery, cyfry, spacje i myślniki."
msgid "Font name is too long."
msgstr "Nazwa czcionki jest za długa."
msgid "At least one weight is required."
msgstr "Wymagana jest co najmniej jedna grubość."
msgid "Too many weights selected."
msgstr "Wybrano za dużo grubości."
msgid "At least one style is required."
msgstr "Wymagany jest co najmniej jeden styl."
msgid "Successfully installed %s."
msgstr "Pomyślnie zainstalowano %s."
msgid "An unexpected error occurred."
msgstr "Wystąpił nieoczekiwany błąd."
msgid "Invalid font ID."
msgstr "Nieprawidłowy identyfikator czcionki."
msgid "Font not found."
msgstr "Czcionka nie została znaleziona."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Nie można usunąć czcionek, które nie zostały zaimportowane przez tę wtyczkę."
msgid "Font deleted successfully."
msgstr "Czcionka została pomyślnie usunięta."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Czcionka nie została znaleziona w Google Fonts. Sprawdź pisownię i spróbuj ponownie."
msgid "This font is already installed."
msgstr "Ta czcionka jest już zainstalowana."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Nie można połączyć się z Google Fonts. Sprawdź połączenie internetowe i spróbuj ponownie."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts zwróciło błąd. Spróbuj ponownie później."
msgid "Could not process the font data. The font may not be available."
msgstr "Nie można przetworzyć danych czcionki. Czcionka może być niedostępna."
msgid "Could not download the font files. Please try again."
msgstr "Nie można pobrać plików czcionki. Spróbuj ponownie."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Nie można zapisać plików czcionki. Sprawdź, czy wp-content/fonts jest zapisywalny."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Nie można utworzyć katalogu czcionek. Sprawdź uprawnienia plików."
msgid "Invalid file path."
msgstr "Nieprawidłowa ścieżka pliku."
msgid "Invalid font URL."
msgstr "Nieprawidłowy adres URL czcionki."
msgid "Invalid font name."
msgstr "Nieprawidłowa nazwa czcionki."
msgid "No valid weights specified."
msgstr "Nie określono prawidłowych grubości."
msgid "No valid styles specified."
msgstr "Nie określono prawidłowych stylów."
msgid "An unexpected error occurred. Please try again."
msgstr "Wystąpił nieoczekiwany błąd. Spróbuj ponownie."
msgid "Thin"
msgstr "Cienka"
msgid "Extra Light"
msgstr "Bardzo lekka"
msgid "Light"
msgstr "Lekka"
msgid "Regular"
msgstr "Normalna"
msgid "Medium"
msgstr "Średnia"
msgid "Semi Bold"
msgstr "Półgruba"
msgid "Bold"
msgstr "Gruba"
msgid "Extra Bold"
msgstr "Bardzo gruba"
msgid "Black"
msgstr "Czarna"
msgid "You do not have sufficient permissions to access this page."
msgstr "Nie masz wystarczających uprawnień, aby uzyskać dostęp do tej strony."
msgid "Import from Google Fonts"
msgstr "Importuj z Google Fonts"
msgid "Font Name"
msgstr "Nazwa czcionki"
msgid "e.g., Open Sans"
msgstr "np. Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Wprowadź dokładną nazwę czcionki, jak jest wyświetlana w Google Fonts."
msgid "Weights"
msgstr "Grubości"
msgid "Styles"
msgstr "Style"
msgid "Normal"
msgstr "Normalna"
msgid "Italic"
msgstr "Kursywa"
msgid "Files to download:"
msgstr "Pliki do pobrania:"
msgid "Download & Install"
msgstr "Pobierz i zainstaluj"
msgid "Installed Fonts"
msgstr "Zainstalowane czcionki"
msgid "No fonts installed yet."
msgstr "Nie zainstalowano jeszcze żadnych czcionek."
msgid "Delete"
msgstr "Usuń"
msgid "Use %s to apply fonts to your site."
msgstr "Użyj %s, aby zastosować czcionki na swojej stronie."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Wygląd → Edytor → Style → Typografia"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Portuguese (Brazil)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Portuguese (Brazil)\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Importe fontes do Google Fonts para o armazenamento local e registre-as na biblioteca de fontes do WordPress para uma tipografia compatível com GDPR e que respeita a privacidade."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "O Maple Local Fonts requer WordPress 6.5 ou superior para suporte à biblioteca de fontes."
msgid "Plugin Activation Error"
msgstr "Erro de ativação do plugin"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "O Maple Local Fonts foi desativado. Requer WordPress 6.5 ou superior."
msgid "Local Fonts"
msgstr "Fontes locais"
msgid "Downloading..."
msgstr "Baixando..."
msgid "Deleting..."
msgstr "Excluindo..."
msgid "Are you sure you want to delete this font?"
msgstr "Tem certeza de que deseja excluir esta fonte?"
msgid "An error occurred. Please try again."
msgstr "Ocorreu um erro. Por favor, tente novamente."
msgid "Please select at least one weight."
msgstr "Por favor, selecione pelo menos um peso."
msgid "Please select at least one style."
msgstr "Por favor, selecione pelo menos um estilo."
msgid "Please enter a font name."
msgstr "Por favor, digite um nome de fonte."
msgid "Security check failed."
msgstr "Falha na verificação de segurança."
msgid "Unauthorized."
msgstr "Não autorizado."
msgid "Font name is required."
msgstr "O nome da fonte é obrigatório."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Nome de fonte inválido: apenas letras, números, espaços e hífens são permitidos."
msgid "Font name is too long."
msgstr "O nome da fonte é muito longo."
msgid "At least one weight is required."
msgstr "Pelo menos um peso é necessário."
msgid "Too many weights selected."
msgstr "Muitos pesos selecionados."
msgid "At least one style is required."
msgstr "Pelo menos um estilo é necessário."
msgid "Successfully installed %s."
msgstr "%s foi instalado com sucesso."
msgid "An unexpected error occurred."
msgstr "Ocorreu um erro inesperado."
msgid "Invalid font ID."
msgstr "ID de fonte inválido."
msgid "Font not found."
msgstr "Fonte não encontrada."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Não é possível excluir fontes que não foram importadas por este plugin."
msgid "Font deleted successfully."
msgstr "Fonte excluída com sucesso."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Fonte não encontrada no Google Fonts. Verifique a ortografia e tente novamente."
msgid "This font is already installed."
msgstr "Esta fonte já está instalada."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Não foi possível conectar ao Google Fonts. Verifique sua conexão com a internet e tente novamente."
msgid "Google Fonts returned an error. Please try again later."
msgstr "O Google Fonts retornou um erro. Por favor, tente novamente mais tarde."
msgid "Could not process the font data. The font may not be available."
msgstr "Não foi possível processar os dados da fonte. A fonte pode não estar disponível."
msgid "Could not download the font files. Please try again."
msgstr "Não foi possível baixar os arquivos da fonte. Por favor, tente novamente."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Não foi possível salvar os arquivos da fonte. Verifique se wp-content/fonts tem permissão de escrita."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Não foi possível criar o diretório de fontes. Verifique as permissões de arquivo."
msgid "Invalid file path."
msgstr "Caminho de arquivo inválido."
msgid "Invalid font URL."
msgstr "URL de fonte inválida."
msgid "Invalid font name."
msgstr "Nome de fonte inválido."
msgid "No valid weights specified."
msgstr "Nenhum peso válido especificado."
msgid "No valid styles specified."
msgstr "Nenhum estilo válido especificado."
msgid "An unexpected error occurred. Please try again."
msgstr "Ocorreu um erro inesperado. Por favor, tente novamente."
msgid "Thin"
msgstr "Fina"
msgid "Extra Light"
msgstr "Extra leve"
msgid "Light"
msgstr "Leve"
msgid "Regular"
msgstr "Normal"
msgid "Medium"
msgstr "Média"
msgid "Semi Bold"
msgstr "Semi negrito"
msgid "Bold"
msgstr "Negrito"
msgid "Extra Bold"
msgstr "Extra negrito"
msgid "Black"
msgstr "Preta"
msgid "You do not have sufficient permissions to access this page."
msgstr "Você não tem permissões suficientes para acessar esta página."
msgid "Import from Google Fonts"
msgstr "Importar do Google Fonts"
msgid "Font Name"
msgstr "Nome da fonte"
msgid "e.g., Open Sans"
msgstr "ex.: Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Digite o nome exato da fonte como aparece no Google Fonts."
msgid "Weights"
msgstr "Pesos"
msgid "Styles"
msgstr "Estilos"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "Itálico"
msgid "Files to download:"
msgstr "Arquivos para baixar:"
msgid "Download & Install"
msgstr "Baixar e instalar"
msgid "Installed Fonts"
msgstr "Fontes instaladas"
msgid "No fonts installed yet."
msgstr "Nenhuma fonte instalada ainda."
msgid "Delete"
msgstr "Excluir"
msgid "Use %s to apply fonts to your site."
msgstr "Use %s para aplicar fontes ao seu site."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Aparência → Editor → Estilos → Tipografia"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Russian
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Импортируйте шрифты Google Fonts в локальное хранилище и зарегистрируйте их в библиотеке шрифтов WordPress для типографики, соответствующей GDPR и защищающей конфиденциальность."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts требует WordPress 6.5 или выше для поддержки библиотеки шрифтов."
msgid "Plugin Activation Error"
msgstr "Ошибка активации плагина"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts был деактивирован. Требуется WordPress 6.5 или выше."
msgid "Local Fonts"
msgstr "Локальные шрифты"
msgid "Downloading..."
msgstr "Загрузка..."
msgid "Deleting..."
msgstr "Удаление..."
msgid "Are you sure you want to delete this font?"
msgstr "Вы уверены, что хотите удалить этот шрифт?"
msgid "An error occurred. Please try again."
msgstr "Произошла ошибка. Пожалуйста, попробуйте снова."
msgid "Please select at least one weight."
msgstr "Пожалуйста, выберите хотя бы одну толщину."
msgid "Please select at least one style."
msgstr "Пожалуйста, выберите хотя бы один стиль."
msgid "Please enter a font name."
msgstr "Пожалуйста, введите название шрифта."
msgid "Security check failed."
msgstr "Проверка безопасности не пройдена."
msgid "Unauthorized."
msgstr "Не авторизован."
msgid "Font name is required."
msgstr "Название шрифта обязательно."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Недопустимое название шрифта: разрешены только буквы, цифры, пробелы и дефисы."
msgid "Font name is too long."
msgstr "Название шрифта слишком длинное."
msgid "At least one weight is required."
msgstr "Требуется хотя бы одна толщина."
msgid "Too many weights selected."
msgstr "Выбрано слишком много толщин."
msgid "At least one style is required."
msgstr "Требуется хотя бы один стиль."
msgid "Successfully installed %s."
msgstr "%s успешно установлен."
msgid "An unexpected error occurred."
msgstr "Произошла непредвиденная ошибка."
msgid "Invalid font ID."
msgstr "Недопустимый ID шрифта."
msgid "Font not found."
msgstr "Шрифт не найден."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Невозможно удалить шрифты, не импортированные этим плагином."
msgid "Font deleted successfully."
msgstr "Шрифт успешно удалён."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Шрифт не найден в Google Fonts. Проверьте правописание и попробуйте снова."
msgid "This font is already installed."
msgstr "Этот шрифт уже установлен."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Не удалось подключиться к Google Fonts. Проверьте подключение к интернету и попробуйте снова."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts вернул ошибку. Пожалуйста, попробуйте позже."
msgid "Could not process the font data. The font may not be available."
msgstr "Не удалось обработать данные шрифта. Шрифт может быть недоступен."
msgid "Could not download the font files. Please try again."
msgstr "Не удалось загрузить файлы шрифта. Пожалуйста, попробуйте снова."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Не удалось сохранить файлы шрифта. Проверьте, что wp-content/fonts доступен для записи."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Не удалось создать каталог шрифтов. Проверьте права доступа к файлам."
msgid "Invalid file path."
msgstr "Недопустимый путь к файлу."
msgid "Invalid font URL."
msgstr "Недопустимый URL шрифта."
msgid "Invalid font name."
msgstr "Недопустимое название шрифта."
msgid "No valid weights specified."
msgstr "Не указаны допустимые толщины."
msgid "No valid styles specified."
msgstr "Не указаны допустимые стили."
msgid "An unexpected error occurred. Please try again."
msgstr "Произошла непредвиденная ошибка. Пожалуйста, попробуйте снова."
msgid "Thin"
msgstr "Тонкий"
msgid "Extra Light"
msgstr "Сверхлёгкий"
msgid "Light"
msgstr "Лёгкий"
msgid "Regular"
msgstr "Обычный"
msgid "Medium"
msgstr "Средний"
msgid "Semi Bold"
msgstr "Полужирный"
msgid "Bold"
msgstr "Жирный"
msgid "Extra Bold"
msgstr "Сверхжирный"
msgid "Black"
msgstr "Чёрный"
msgid "You do not have sufficient permissions to access this page."
msgstr "У вас недостаточно прав для доступа к этой странице."
msgid "Import from Google Fonts"
msgstr "Импорт из Google Fonts"
msgid "Font Name"
msgstr "Название шрифта"
msgid "e.g., Open Sans"
msgstr "напр., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Введите точное название шрифта, как оно отображается в Google Fonts."
msgid "Weights"
msgstr "Толщины"
msgid "Styles"
msgstr "Стили"
msgid "Normal"
msgstr "Обычный"
msgid "Italic"
msgstr "Курсив"
msgid "Files to download:"
msgstr "Файлов для загрузки:"
msgid "Download & Install"
msgstr "Скачать и установить"
msgid "Installed Fonts"
msgstr "Установленные шрифты"
msgid "No fonts installed yet."
msgstr "Шрифты ещё не установлены."
msgid "Delete"
msgstr "Удалить"
msgid "Use %s to apply fonts to your site."
msgstr "Используйте %s для применения шрифтов к вашему сайту."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Внешний вид → Редактор → Стили → Типографика"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Turkish
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "Google Fonts'u yerel depolamaya aktarın ve GDPR uyumlu, gizlilik dostu tipografi için WordPress Yazı Tipi Kitaplığı'na kaydedin."
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts, Yazı Tipi Kitaplığı desteği için WordPress 6.5 veya üstünü gerektirir."
msgid "Plugin Activation Error"
msgstr "Eklenti Etkinleştirme Hatası"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts devre dışı bırakıldı. WordPress 6.5 veya üstünü gerektirir."
msgid "Local Fonts"
msgstr "Yerel Yazı Tipleri"
msgid "Downloading..."
msgstr "İndiriliyor..."
msgid "Deleting..."
msgstr "Siliniyor..."
msgid "Are you sure you want to delete this font?"
msgstr "Bu yazı tipini silmek istediğinizden emin misiniz?"
msgid "An error occurred. Please try again."
msgstr "Bir hata oluştu. Lütfen tekrar deneyin."
msgid "Please select at least one weight."
msgstr "Lütfen en az bir kalınlık seçin."
msgid "Please select at least one style."
msgstr "Lütfen en az bir stil seçin."
msgid "Please enter a font name."
msgstr "Lütfen bir yazı tipi adı girin."
msgid "Security check failed."
msgstr "Güvenlik kontrolü başarısız."
msgid "Unauthorized."
msgstr "Yetkisiz."
msgid "Font name is required."
msgstr "Yazı tipi adı gereklidir."
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "Geçersiz yazı tipi adı: yalnızca harfler, sayılar, boşluklar ve tireler kullanılabilir."
msgid "Font name is too long."
msgstr "Yazı tipi adı çok uzun."
msgid "At least one weight is required."
msgstr "En az bir kalınlık gereklidir."
msgid "Too many weights selected."
msgstr "Çok fazla kalınlık seçildi."
msgid "At least one style is required."
msgstr "En az bir stil gereklidir."
msgid "Successfully installed %s."
msgstr "%s başarıyla yüklendi."
msgid "An unexpected error occurred."
msgstr "Beklenmeyen bir hata oluştu."
msgid "Invalid font ID."
msgstr "Geçersiz yazı tipi kimliği."
msgid "Font not found."
msgstr "Yazı tipi bulunamadı."
msgid "Cannot delete fonts not imported by this plugin."
msgstr "Bu eklenti tarafından içe aktarılmayan yazı tipleri silinemez."
msgid "Font deleted successfully."
msgstr "Yazı tipi başarıyla silindi."
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "Yazı tipi Google Fonts'ta bulunamadı. Lütfen yazımı kontrol edin ve tekrar deneyin."
msgid "This font is already installed."
msgstr "Bu yazı tipi zaten yüklü."
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "Google Fonts'a bağlanılamadı. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin."
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts bir hata döndürdü. Lütfen daha sonra tekrar deneyin."
msgid "Could not process the font data. The font may not be available."
msgstr "Yazı tipi verileri işlenemedi. Yazı tipi kullanılamıyor olabilir."
msgid "Could not download the font files. Please try again."
msgstr "Yazı tipi dosyaları indirilemedi. Lütfen tekrar deneyin."
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "Yazı tipi dosyaları kaydedilemedi. Lütfen wp-content/fonts klasörünün yazılabilir olduğunu kontrol edin."
msgid "Could not create fonts directory. Please check file permissions."
msgstr "Yazı tipi dizini oluşturulamadı. Lütfen dosya izinlerini kontrol edin."
msgid "Invalid file path."
msgstr "Geçersiz dosya yolu."
msgid "Invalid font URL."
msgstr "Geçersiz yazı tipi URL'si."
msgid "Invalid font name."
msgstr "Geçersiz yazı tipi adı."
msgid "No valid weights specified."
msgstr "Geçerli kalınlık belirtilmedi."
msgid "No valid styles specified."
msgstr "Geçerli stil belirtilmedi."
msgid "An unexpected error occurred. Please try again."
msgstr "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin."
msgid "Thin"
msgstr "İnce"
msgid "Extra Light"
msgstr "Ekstra Hafif"
msgid "Light"
msgstr "Hafif"
msgid "Regular"
msgstr "Normal"
msgid "Medium"
msgstr "Orta"
msgid "Semi Bold"
msgstr "Yarı Kalın"
msgid "Bold"
msgstr "Kalın"
msgid "Extra Bold"
msgstr "Ekstra Kalın"
msgid "Black"
msgstr "Siyah"
msgid "You do not have sufficient permissions to access this page."
msgstr "Bu sayfaya erişmek için yeterli izniniz yok."
msgid "Import from Google Fonts"
msgstr "Google Fonts'tan İçe Aktar"
msgid "Font Name"
msgstr "Yazı Tipi Adı"
msgid "e.g., Open Sans"
msgstr "örn., Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "Google Fonts'ta göründüğü şekliyle tam yazı tipi adını girin."
msgid "Weights"
msgstr "Kalınlıklar"
msgid "Styles"
msgstr "Stiller"
msgid "Normal"
msgstr "Normal"
msgid "Italic"
msgstr "İtalik"
msgid "Files to download:"
msgstr "İndirilecek dosyalar:"
msgid "Download & Install"
msgstr "İndir ve Yükle"
msgid "Installed Fonts"
msgstr "Yüklü Yazı Tipleri"
msgid "No fonts installed yet."
msgstr "Henüz yazı tipi yüklenmedi."
msgid "Delete"
msgstr "Sil"
msgid "Use %s to apply fonts to your site."
msgstr "Sitenize yazı tipleri uygulamak için %s kullanın."
msgid "Appearance → Editor → Styles → Typography"
msgstr "Görünüm → Düzenleyici → Stiller → Tipografi"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Chinese (Simplified)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Chinese (Simplified)\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "将 Google Fonts 导入本地存储并在 WordPress 字体库中注册,实现符合 GDPR 且注重隐私的排版。"
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts 需要 WordPress 6.5 或更高版本以支持字体库。"
msgid "Plugin Activation Error"
msgstr "插件激活错误"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts 已停用。需要 WordPress 6.5 或更高版本。"
msgid "Local Fonts"
msgstr "本地字体"
msgid "Downloading..."
msgstr "下载中..."
msgid "Deleting..."
msgstr "删除中..."
msgid "Are you sure you want to delete this font?"
msgstr "您确定要删除此字体吗?"
msgid "An error occurred. Please try again."
msgstr "发生错误。请重试。"
msgid "Please select at least one weight."
msgstr "请至少选择一个字重。"
msgid "Please select at least one style."
msgstr "请至少选择一个样式。"
msgid "Please enter a font name."
msgstr "请输入字体名称。"
msgid "Security check failed."
msgstr "安全检查失败。"
msgid "Unauthorized."
msgstr "未授权。"
msgid "Font name is required."
msgstr "字体名称为必填项。"
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "字体名称无效:只允许使用字母、数字、空格和连字符。"
msgid "Font name is too long."
msgstr "字体名称过长。"
msgid "At least one weight is required."
msgstr "至少需要一个字重。"
msgid "Too many weights selected."
msgstr "选择的字重过多。"
msgid "At least one style is required."
msgstr "至少需要一个样式。"
msgid "Successfully installed %s."
msgstr "成功安装 %s。"
msgid "An unexpected error occurred."
msgstr "发生意外错误。"
msgid "Invalid font ID."
msgstr "无效的字体 ID。"
msgid "Font not found."
msgstr "未找到字体。"
msgid "Cannot delete fonts not imported by this plugin."
msgstr "无法删除非此插件导入的字体。"
msgid "Font deleted successfully."
msgstr "字体删除成功。"
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "在 Google Fonts 上未找到字体。请检查拼写并重试。"
msgid "This font is already installed."
msgstr "此字体已安装。"
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "无法连接到 Google Fonts。请检查您的网络连接并重试。"
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts 返回错误。请稍后重试。"
msgid "Could not process the font data. The font may not be available."
msgstr "无法处理字体数据。该字体可能不可用。"
msgid "Could not download the font files. Please try again."
msgstr "无法下载字体文件。请重试。"
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "无法保存字体文件。请检查 wp-content/fonts 是否可写。"
msgid "Could not create fonts directory. Please check file permissions."
msgstr "无法创建字体目录。请检查文件权限。"
msgid "Invalid file path."
msgstr "无效的文件路径。"
msgid "Invalid font URL."
msgstr "无效的字体 URL。"
msgid "Invalid font name."
msgstr "无效的字体名称。"
msgid "No valid weights specified."
msgstr "未指定有效的字重。"
msgid "No valid styles specified."
msgstr "未指定有效的样式。"
msgid "An unexpected error occurred. Please try again."
msgstr "发生意外错误。请重试。"
msgid "Thin"
msgstr "极细"
msgid "Extra Light"
msgstr "特细"
msgid "Light"
msgstr "细体"
msgid "Regular"
msgstr "常规"
msgid "Medium"
msgstr "中等"
msgid "Semi Bold"
msgstr "半粗"
msgid "Bold"
msgstr "粗体"
msgid "Extra Bold"
msgstr "特粗"
msgid "Black"
msgstr "黑体"
msgid "You do not have sufficient permissions to access this page."
msgstr "您没有足够的权限访问此页面。"
msgid "Import from Google Fonts"
msgstr "从 Google Fonts 导入"
msgid "Font Name"
msgstr "字体名称"
msgid "e.g., Open Sans"
msgstr "例如Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "输入 Google Fonts 上显示的确切字体名称。"
msgid "Weights"
msgstr "字重"
msgid "Styles"
msgstr "样式"
msgid "Normal"
msgstr "正常"
msgid "Italic"
msgstr "斜体"
msgid "Files to download:"
msgstr "要下载的文件:"
msgid "Download & Install"
msgstr "下载并安装"
msgid "Installed Fonts"
msgstr "已安装的字体"
msgid "No fonts installed yet."
msgstr "尚未安装任何字体。"
msgid "Delete"
msgstr "删除"
msgid "Use %s to apply fonts to your site."
msgstr "使用 %s 将字体应用到您的网站。"
msgid "Appearance → Editor → Styles → Typography"
msgstr "外观 → 编辑器 → 样式 → 排版"

View file

@ -1,222 +0,0 @@
# Translation of Maple Local Fonts in Chinese (Traditional)
# Copyright (C) 2024 Maple Open Technologies
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Maple Local Fonts 1.0.0\n"
"Report-Msgid-Bugs-To: https://mapleopentech.org/\n"
"Last-Translator: Maple Open Technologies\n"
"Language-Team: Chinese (Traditional)\n"
"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
"X-Generator: Manual\n"
"X-Domain: maple-local-fonts\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. Plugin Name of the plugin
msgid "Maple Local Fonts"
msgstr "Maple Local Fonts"
#. Description of the plugin
msgid "Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography."
msgstr "將 Google Fonts 匯入本機儲存空間並在 WordPress 字型庫中註冊,實現符合 GDPR 且注重隱私的排版。"
#. Author of the plugin
msgid "Maple Open Technologies"
msgstr "Maple Open Technologies"
msgid "Maple Local Fonts requires WordPress 6.5 or higher for Font Library support."
msgstr "Maple Local Fonts 需要 WordPress 6.5 或更高版本以支援字型庫。"
msgid "Plugin Activation Error"
msgstr "外掛啟用錯誤"
msgid "Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher."
msgstr "Maple Local Fonts 已停用。需要 WordPress 6.5 或更高版本。"
msgid "Local Fonts"
msgstr "本機字型"
msgid "Downloading..."
msgstr "下載中..."
msgid "Deleting..."
msgstr "刪除中..."
msgid "Are you sure you want to delete this font?"
msgstr "您確定要刪除此字型嗎?"
msgid "An error occurred. Please try again."
msgstr "發生錯誤。請重試。"
msgid "Please select at least one weight."
msgstr "請至少選擇一個字重。"
msgid "Please select at least one style."
msgstr "請至少選擇一個樣式。"
msgid "Please enter a font name."
msgstr "請輸入字型名稱。"
msgid "Security check failed."
msgstr "安全性檢查失敗。"
msgid "Unauthorized."
msgstr "未授權。"
msgid "Font name is required."
msgstr "字型名稱為必填項。"
msgid "Invalid font name: only letters, numbers, spaces, and hyphens allowed."
msgstr "字型名稱無效:只允許使用字母、數字、空格和連字號。"
msgid "Font name is too long."
msgstr "字型名稱過長。"
msgid "At least one weight is required."
msgstr "至少需要一個字重。"
msgid "Too many weights selected."
msgstr "選擇的字重過多。"
msgid "At least one style is required."
msgstr "至少需要一個樣式。"
msgid "Successfully installed %s."
msgstr "成功安裝 %s。"
msgid "An unexpected error occurred."
msgstr "發生意外錯誤。"
msgid "Invalid font ID."
msgstr "無效的字型 ID。"
msgid "Font not found."
msgstr "找不到字型。"
msgid "Cannot delete fonts not imported by this plugin."
msgstr "無法刪除非此外掛匯入的字型。"
msgid "Font deleted successfully."
msgstr "字型刪除成功。"
msgid "Font not found on Google Fonts. Please check the spelling and try again."
msgstr "在 Google Fonts 上找不到字型。請檢查拼寫並重試。"
msgid "This font is already installed."
msgstr "此字型已安裝。"
msgid "Could not connect to Google Fonts. Please check your internet connection and try again."
msgstr "無法連線到 Google Fonts。請檢查您的網路連線並重試。"
msgid "Google Fonts returned an error. Please try again later."
msgstr "Google Fonts 傳回錯誤。請稍後重試。"
msgid "Could not process the font data. The font may not be available."
msgstr "無法處理字型資料。該字型可能不可用。"
msgid "Could not download the font files. Please try again."
msgstr "無法下載字型檔案。請重試。"
msgid "Could not save font files. Please check that wp-content/fonts is writable."
msgstr "無法儲存字型檔案。請檢查 wp-content/fonts 是否可寫入。"
msgid "Could not create fonts directory. Please check file permissions."
msgstr "無法建立字型目錄。請檢查檔案權限。"
msgid "Invalid file path."
msgstr "無效的檔案路徑。"
msgid "Invalid font URL."
msgstr "無效的字型 URL。"
msgid "Invalid font name."
msgstr "無效的字型名稱。"
msgid "No valid weights specified."
msgstr "未指定有效的字重。"
msgid "No valid styles specified."
msgstr "未指定有效的樣式。"
msgid "An unexpected error occurred. Please try again."
msgstr "發生意外錯誤。請重試。"
msgid "Thin"
msgstr "極細"
msgid "Extra Light"
msgstr "特細"
msgid "Light"
msgstr "細體"
msgid "Regular"
msgstr "標準"
msgid "Medium"
msgstr "中等"
msgid "Semi Bold"
msgstr "半粗"
msgid "Bold"
msgstr "粗體"
msgid "Extra Bold"
msgstr "特粗"
msgid "Black"
msgstr "黑體"
msgid "You do not have sufficient permissions to access this page."
msgstr "您沒有足夠的權限存取此頁面。"
msgid "Import from Google Fonts"
msgstr "從 Google Fonts 匯入"
msgid "Font Name"
msgstr "字型名稱"
msgid "e.g., Open Sans"
msgstr "例如Open Sans"
msgid "Enter the exact font name as it appears on Google Fonts."
msgstr "輸入 Google Fonts 上顯示的確切字型名稱。"
msgid "Weights"
msgstr "字重"
msgid "Styles"
msgstr "樣式"
msgid "Normal"
msgstr "正常"
msgid "Italic"
msgstr "斜體"
msgid "Files to download:"
msgstr "要下載的檔案:"
msgid "Download & Install"
msgstr "下載並安裝"
msgid "Installed Fonts"
msgstr "已安裝的字型"
msgid "No fonts installed yet."
msgstr "尚未安裝任何字型。"
msgid "Delete"
msgstr "刪除"
msgid "Use %s to apply fonts to your site."
msgstr "使用 %s 將字型套用到您的網站。"
msgid "Appearance → Editor → Styles → Typography"
msgstr "外觀 → 編輯器 → 樣式 → 排版"

View file

@ -1,627 +0,0 @@
<?php
/**
* Plugin Name: Maple Local Fonts
* Plugin URI: https://mapleopentech.org/plugins/maple-local-fonts
* Description: Import Google Fonts to local storage and register them with WordPress Font Library for GDPR-compliant, privacy-friendly typography.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: Maple Open Technologies
* Author URI: https://mapleopentech.org
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: maple-local-fonts
* Domain Path: /languages
*/
if (!defined('ABSPATH')) {
exit;
}
// Plugin constants
define('MLF_VERSION', '1.0.0');
define('MLF_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MLF_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MLF_PLUGIN_BASENAME', plugin_basename(__FILE__));
// Limits
define('MLF_MAX_FONTS_PER_REQUEST', 10);
define('MLF_MAX_WEIGHTS_PER_FONT', 9);
define('MLF_REQUEST_TIMEOUT', 30);
define('MLF_MAX_CSS_SIZE', 512 * 1024); // 512KB max CSS response
define('MLF_MAX_FONT_FILE_SIZE', 5 * 1024 * 1024); // 5MB max font file
define('MLF_MAX_FONT_FACES', 20); // Max font faces per import (9 weights × 2 styles + buffer)
/**
* Ensure WOFF2 MIME type is registered (fixes Safari font loading).
*
* @param array $mimes Existing MIME types.
* @return array Modified MIME types.
*/
function mlf_add_woff2_mime_type($mimes) {
$mimes['woff2'] = 'font/woff2';
$mimes['woff'] = 'font/woff';
return $mimes;
}
add_filter('mime_types', 'mlf_add_woff2_mime_type');
/**
* Check WordPress version on activation.
*/
function mlf_activate() {
if (version_compare(get_bloginfo('version'), '6.5', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die(
esc_html__('Maple Local Fonts requires WordPress 6.5 or higher for Font Library support.', 'maple-local-fonts'),
esc_html__('Plugin Activation Error', 'maple-local-fonts'),
['back_link' => true]
);
}
// Ensure fonts directory exists
$font_dir = wp_get_font_dir();
if (!file_exists($font_dir['path'])) {
wp_mkdir_p($font_dir['path']);
}
// Create .htaccess for proper MIME types and CORS (Apache servers)
$htaccess_path = trailingslashit($font_dir['path']) . '.htaccess';
if (!file_exists($htaccess_path)) {
$htaccess_content = "# Maple Local Fonts - Font MIME types and CORS\n";
$htaccess_content .= "<IfModule mod_mime.c>\n";
$htaccess_content .= " AddType font/woff2 .woff2\n";
$htaccess_content .= " AddType font/woff .woff\n";
$htaccess_content .= "</IfModule>\n\n";
$htaccess_content .= "<IfModule mod_headers.c>\n";
$htaccess_content .= " <FilesMatch \"\\.(woff2?|ttf|otf|eot)$\">\n";
$htaccess_content .= " Header set Access-Control-Allow-Origin \"*\"\n";
$htaccess_content .= " </FilesMatch>\n";
$htaccess_content .= "</IfModule>\n";
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if ($wp_filesystem) {
$wp_filesystem->put_contents($htaccess_path, $htaccess_content, FS_CHMOD_FILE);
}
}
// Flush rewrite rules for fonts CSS endpoint
mlf_add_rewrite_rules();
flush_rewrite_rules();
}
register_activation_hook(__FILE__, 'mlf_activate');
/**
* Flush rewrite rules on deactivation.
*/
function mlf_deactivate() {
flush_rewrite_rules();
}
register_deactivation_hook(__FILE__, 'mlf_deactivate');
/**
* Check WordPress version on admin init (in case WP was downgraded).
*/
function mlf_check_version() {
if (version_compare(get_bloginfo('version'), '6.5', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
add_action('admin_notices', 'mlf_version_notice');
}
}
add_action('admin_init', 'mlf_check_version');
/**
* Ensure fonts directory htaccess exists (for MIME types and CORS).
*/
function mlf_ensure_htaccess() {
$font_dir = wp_get_font_dir();
$htaccess_path = trailingslashit($font_dir['path']) . '.htaccess';
// Always update htaccess to ensure latest headers
$htaccess_content = "# Maple Local Fonts - Font MIME types and CORS\n";
$htaccess_content .= "# Required for cross-browser font loading including iOS Safari\n\n";
$htaccess_content .= "<IfModule mod_mime.c>\n";
$htaccess_content .= " AddType font/woff2 .woff2\n";
$htaccess_content .= " AddType font/woff .woff\n";
$htaccess_content .= "</IfModule>\n\n";
$htaccess_content .= "<IfModule mod_headers.c>\n";
$htaccess_content .= " <FilesMatch \"\\.(woff2?|ttf|otf|eot)$\">\n";
$htaccess_content .= " Header set Access-Control-Allow-Origin \"*\"\n";
$htaccess_content .= " Header set Access-Control-Allow-Methods \"GET, OPTIONS\"\n";
$htaccess_content .= " Header set Access-Control-Allow-Headers \"Origin, Content-Type\"\n";
$htaccess_content .= " Header set Cross-Origin-Resource-Policy \"cross-origin\"\n";
$htaccess_content .= " Header set Timing-Allow-Origin \"*\"\n";
$htaccess_content .= " </FilesMatch>\n";
$htaccess_content .= "</IfModule>\n";
if (file_exists($font_dir['path'])) {
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if ($wp_filesystem) {
$wp_filesystem->put_contents($htaccess_path, $htaccess_content, FS_CHMOD_FILE);
}
}
}
add_action('admin_init', 'mlf_ensure_htaccess');
/**
* Display version notice.
*/
function mlf_version_notice() {
echo '<div class="error"><p>';
esc_html_e('Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.', 'maple-local-fonts');
echo '</p></div>';
}
/**
* Declare WooCommerce HPOS compatibility.
*/
function mlf_declare_hpos_compatibility() {
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
}
add_action('before_woocommerce_init', 'mlf_declare_hpos_compatibility');
/**
* Load plugin text domain.
*/
function mlf_load_textdomain() {
load_plugin_textdomain('maple-local-fonts', false, dirname(MLF_PLUGIN_BASENAME) . '/languages');
}
add_action('plugins_loaded', 'mlf_load_textdomain');
/**
* Autoload plugin classes.
*/
function mlf_autoload($class) {
$prefix = 'MLF_';
if (strpos($class, $prefix) !== 0) {
return;
}
$class_name = str_replace($prefix, '', $class);
$class_name = str_replace('_', '-', strtolower($class_name));
$file = MLF_PLUGIN_DIR . 'includes/class-mlf-' . $class_name . '.php';
if (file_exists($file)) {
require_once $file;
}
}
spl_autoload_register('mlf_autoload');
/**
* Initialize the plugin.
*/
function mlf_init() {
// Only load admin functionality
if (is_admin()) {
new MLF_Admin_Page();
new MLF_Ajax_Handler();
new MLF_Font_Search();
}
}
add_action('plugins_loaded', 'mlf_init', 20);
/**
* Register REST API routes.
*/
function mlf_register_rest_routes() {
$controller = new MLF_Rest_Controller();
$controller->register_routes();
// Register font server routes for cross-browser compatibility
$font_server = new MLF_Font_Server();
$font_server->register_routes();
}
add_action('rest_api_init', 'mlf_register_rest_routes');
/**
* Check if we should use REST API for font serving (compatibility mode).
*
* @return bool True if using REST API font serving.
*/
function mlf_use_rest_font_serving() {
return get_option('mlf_compatibility_mode', true); // Default to true for maximum compatibility
}
/**
* Get the URL for a font file.
*
* Uses REST API endpoint for compatibility, or direct file URL for performance.
*
* @param string $filename The font filename.
* @return string The font URL.
*/
function mlf_get_font_url($filename) {
if (mlf_use_rest_font_serving()) {
// Use REST API endpoint (guaranteed proper headers)
return rest_url('mlf/v1/font/' . $filename);
} else {
// Use direct file URL (faster but depends on server config)
$font_dir = wp_get_font_dir();
return trailingslashit($font_dir['url']) . $filename;
}
}
/**
* Get the required capability for managing fonts.
*
* @return string The capability required to manage fonts.
*/
function mlf_get_capability() {
/**
* Filter the capability required to manage local fonts.
*
* @since 1.0.0
* @param string $capability Default capability is 'edit_theme_options'.
*/
return apply_filters('mlf_manage_fonts_capability', 'edit_theme_options');
}
/**
* Add imported fonts to the theme.json typography settings.
*
* This makes fonts appear in the Site Editor typography dropdown
* and generates the @font-face CSS for the frontend.
*
* @param WP_Theme_JSON_Data $theme_json The theme.json data.
* @return WP_Theme_JSON_Data Modified theme.json data.
*/
function mlf_add_fonts_to_theme_json($theme_json) {
// Wrap in try-catch to prevent breaking Site Editor if something goes wrong
try {
$registry = new MLF_Font_Registry();
$fonts = $registry->get_imported_fonts_with_src();
if (empty($fonts) || !is_array($fonts)) {
return $theme_json;
}
// Build our font families
$font_families = [];
$font_dir = wp_get_font_dir();
if (empty($font_dir['url'])) {
return $theme_json;
}
$font_base_url = trailingslashit($font_dir['url']);
foreach ($fonts as $font) {
// Validate required font properties
if (empty($font['name']) || empty($font['slug']) || empty($font['variants'])) {
continue;
}
$font_faces = [];
foreach ($font['variants'] as $variant) {
// Validate variant data
if (empty($variant['filename'])) {
continue;
}
$weight = !empty($variant['weight']) ? $variant['weight'] : '400';
$style = !empty($variant['style']) ? $variant['style'] : 'normal';
$filename = $variant['filename'];
// Use direct file URL
$font_url = $font_base_url . $filename;
$font_faces[] = [
'fontFamily' => $font['name'],
'fontWeight' => $weight,
'fontStyle' => $style,
'fontDisplay' => 'swap',
'src' => [$font_url],
];
}
// Only add font if it has valid faces
if (!empty($font_faces)) {
$font_families[] = [
'name' => $font['name'],
'slug' => $font['slug'],
'fontFamily' => "'{$font['name']}', sans-serif",
'fontFace' => $font_faces,
];
}
}
// Only update if we have valid fonts
if (empty($font_families)) {
return $theme_json;
}
// Use update_with to merge - WordPress handles the merging logic
$new_data = [
'version' => 2,
'settings' => [
'typography' => [
'fontFamilies' => $font_families,
],
],
];
return $theme_json->update_with($new_data);
} catch (Exception $e) {
// Log error but don't break the Site Editor
error_log('MLF theme.json filter error: ' . $e->getMessage());
return $theme_json;
}
}
add_filter('wp_theme_json_data_user', 'mlf_add_fonts_to_theme_json', 10);
/**
* Generate @font-face CSS for imported fonts.
*
* @return string CSS content.
*/
function mlf_get_font_face_css() {
$registry = new MLF_Font_Registry();
$fonts = $registry->get_imported_fonts();
if (empty($fonts)) {
return '';
}
$css = '';
foreach ($fonts as $font) {
$font_slug = sanitize_title($font['name']);
// Escape font name for CSS (handle quotes and special chars)
$css_font_name = addcslashes($font['name'], "'\\");
foreach ($font['variants'] as $variant) {
$weight = $variant['weight'];
$style = $variant['style'];
// Build filename based on our naming convention
if (strpos($weight, ' ') !== false) {
// Variable font
$filename = sprintf('%s_%s_variable.woff2', $font_slug, $style);
} else {
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight);
}
// Get font URL (uses REST API or direct based on setting)
$font_url = mlf_get_font_url($filename);
// Ensure HTTPS if site uses HTTPS (important for reverse proxy setups)
if (is_ssl() && strpos($font_url, 'http://') === 0) {
$font_url = str_replace('http://', 'https://', $font_url);
}
$css .= "@font-face {";
$css .= "font-family: '{$css_font_name}';";
$css .= "font-style: {$style};";
$css .= "font-weight: {$weight};";
$css .= "font-display: swap;";
$css .= "src: url('" . esc_url($font_url) . "') format('woff2');";
$css .= "}\n";
}
}
return $css;
}
/**
* Enqueue font face CSS on frontend.
*
* Uses external CSS file just like Google Fonts does.
*/
function mlf_enqueue_font_faces() {
$registry = new MLF_Font_Registry();
$fonts = $registry->get_imported_fonts();
if (empty($fonts)) {
return;
}
// Load CSS from custom endpoint (like Google Fonts serves CSS)
wp_enqueue_style(
'mlf-local-fonts',
home_url('/mlf-fonts.css'),
[],
null // No version string, like Google Fonts
);
}
add_action('wp_enqueue_scripts', 'mlf_enqueue_font_faces');
/**
* Enqueue font face CSS in block editor.
*/
function mlf_enqueue_editor_font_faces() {
$registry = new MLF_Font_Registry();
$fonts = $registry->get_imported_fonts();
if (empty($fonts)) {
return;
}
// Load CSS from custom endpoint (like Google Fonts serves CSS)
wp_enqueue_style(
'mlf-local-fonts-editor',
home_url('/mlf-fonts.css'),
[],
null
);
}
add_action('enqueue_block_editor_assets', 'mlf_enqueue_editor_font_faces');
/**
* Add rewrite rule for fonts CSS endpoint.
*/
function mlf_add_rewrite_rules() {
add_rewrite_rule('^mlf-fonts\.css$', 'index.php?mlf_fonts_css=1', 'top');
}
add_action('init', 'mlf_add_rewrite_rules');
/**
* Add query var for fonts CSS.
*/
function mlf_add_query_vars($vars) {
$vars[] = 'mlf_fonts_css';
return $vars;
}
add_filter('query_vars', 'mlf_add_query_vars');
/**
* Handle fonts CSS request.
*/
function mlf_handle_fonts_css_request() {
if (!get_query_var('mlf_fonts_css')) {
return;
}
// Send CSS headers
header('Content-Type: text/css; charset=UTF-8');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: public, max-age=86400');
header('X-Content-Type-Options: nosniff');
$registry = new MLF_Font_Registry();
$fonts = $registry->get_imported_fonts_with_src();
if (empty($fonts)) {
echo "/* No fonts installed */\n";
exit;
}
echo "/* Maple Local Fonts */\n\n";
// Use direct font file URLs
$font_dir = wp_get_font_dir();
$font_base_url = trailingslashit($font_dir['url']);
foreach ($fonts as $font) {
foreach ($font['variants'] as $variant) {
$weight = $variant['weight'];
$style = $variant['style'];
// Use the actual filename from database (stored in src)
$filename = $variant['filename'];
$file_url = $font_base_url . $filename;
echo "@font-face {\n";
echo " font-family: '{$font['name']}';\n";
echo " font-style: {$style};\n";
echo " font-weight: {$weight};\n";
echo " font-display: swap;\n";
echo " src: url({$file_url}) format('woff2');\n";
echo "}\n\n";
}
}
exit;
}
add_action('template_redirect', 'mlf_handle_fonts_css_request');
/**
* Register admin menu.
*/
function mlf_register_menu() {
add_submenu_page(
'options-general.php',
__('Maple Fonts', 'maple-local-fonts'),
__('Maple Fonts', 'maple-local-fonts'),
mlf_get_capability(),
'maple-local-fonts',
'mlf_render_admin_page'
);
}
add_action('admin_menu', 'mlf_register_menu');
/**
* Add settings link to plugin action links.
*
* @param array $links Existing plugin action links.
* @return array Modified plugin action links.
*/
function mlf_plugin_action_links($links) {
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url(admin_url('options-general.php?page=maple-local-fonts')),
esc_html__('Settings', 'maple-local-fonts')
);
array_unshift($links, $settings_link);
return $links;
}
add_filter('plugin_action_links_' . MLF_PLUGIN_BASENAME, 'mlf_plugin_action_links');
/**
* Render admin page (delegates to MLF_Admin_Page).
*/
function mlf_render_admin_page() {
$admin_page = new MLF_Admin_Page();
$admin_page->render();
}
/**
* Enqueue admin assets.
*
* @param string $hook The current admin page hook.
*/
function mlf_enqueue_admin_assets($hook) {
if ($hook !== 'settings_page_maple-local-fonts') {
return;
}
// Use filemtime for cache-busting during development
$css_version = MLF_VERSION . '.' . filemtime(MLF_PLUGIN_DIR . 'assets/admin.css');
$js_version = MLF_VERSION . '.' . filemtime(MLF_PLUGIN_DIR . 'assets/admin.js');
wp_enqueue_style(
'mlf-admin',
MLF_PLUGIN_URL . 'assets/admin.css',
[],
$css_version
);
wp_enqueue_script(
'mlf-admin',
MLF_PLUGIN_URL . 'assets/admin.js',
['jquery'],
$js_version,
true
);
wp_localize_script('mlf-admin', 'mapleLocalFontsData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'downloadNonce' => wp_create_nonce('mlf_download_font'),
'deleteNonce' => wp_create_nonce('mlf_delete_font'),
'searchNonce' => wp_create_nonce('mlf_search_fonts'),
'checkUpdatesNonce' => wp_create_nonce('mlf_check_updates'),
'updateFontNonce' => wp_create_nonce('mlf_update_font'),
'strings' => [
'downloading' => __('Downloading...', 'maple-local-fonts'),
'deleting' => __('Deleting...', 'maple-local-fonts'),
'updating' => __('Updating...', 'maple-local-fonts'),
'checking' => __('Checking...', 'maple-local-fonts'),
'confirmDelete' => __('Are you sure you want to delete this font?', 'maple-local-fonts'),
'error' => __('An error occurred. Please try again.', 'maple-local-fonts'),
'searching' => __('Searching...', 'maple-local-fonts'),
'noResults' => __('No fonts found. Try a different search term.', 'maple-local-fonts'),
'selectFont' => __('Please select a font first.', 'maple-local-fonts'),
'previewText' => __('Maple Fonts Preview', 'maple-local-fonts'),
'minChars' => __('Please enter at least 2 characters.', 'maple-local-fonts'),
'noUpdates' => __('All fonts are up to date.', 'maple-local-fonts'),
'updatesFound' => __('Updates available for %d font(s).', 'maple-local-fonts'),
],
]);
}
add_action('admin_enqueue_scripts', 'mlf_enqueue_admin_assets');

View file

@ -1,61 +0,0 @@
<?xml version="1.0"?>
<ruleset name="Maple Local Fonts">
<description>PHP Coding Standards for Maple Local Fonts WordPress Plugin</description>
<!-- Scan these files -->
<file>.</file>
<!-- Exclude these files/directories -->
<exclude-pattern>/vendor/*</exclude-pattern>
<exclude-pattern>/tests/*</exclude-pattern>
<exclude-pattern>/bin/*</exclude-pattern>
<exclude-pattern>/build/*</exclude-pattern>
<exclude-pattern>/node_modules/*</exclude-pattern>
<exclude-pattern>*.min.js</exclude-pattern>
<exclude-pattern>*.min.css</exclude-pattern>
<!-- Show progress and sniff codes -->
<arg value="ps"/>
<arg name="colors"/>
<!-- Use WordPress Coding Standards -->
<rule ref="WordPress">
<!-- Allow short array syntax -->
<exclude name="Generic.Arrays.DisallowShortArraySyntax"/>
<!-- Allow file names that don't match class names (for index.php files) -->
<exclude name="WordPress.Files.FileName.InvalidClassFileName"/>
<exclude name="WordPress.Files.FileName.NotHyphenatedLowercase"/>
</rule>
<!-- Use WordPress Extra Standards -->
<rule ref="WordPress-Extra">
<exclude name="WordPress.Files.FileName"/>
</rule>
<!-- Check for PHP cross-version compatibility -->
<config name="testVersion" value="7.4-"/>
<rule ref="PHPCompatibilityWP"/>
<!-- WordPress-specific settings -->
<config name="minimum_supported_wp_version" value="6.5"/>
<!-- Text domain for i18n -->
<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array">
<element value="maple-local-fonts"/>
</property>
</properties>
</rule>
<!-- Prefix all globals -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties>
<property name="prefixes" type="array">
<element value="mlf"/>
<element value="MLF"/>
<element value="maple_local_fonts"/>
</property>
</properties>
</rule>
</ruleset>

View file

@ -1,27 +0,0 @@
<?xml version="1.0"?>
<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="Maple Local Fonts Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./includes/</directory>
<file>./maple-local-fonts.php</file>
</whitelist>
</filter>
<php>
<const name="WP_TESTS_DOMAIN" value="example.org"/>
<const name="WP_TESTS_EMAIL" value="admin@example.org"/>
<const name="WP_TESTS_TITLE" value="Test Blog"/>
<const name="WP_PHP_BINARY" value="php"/>
</php>
</phpunit>

View file

@ -1,155 +0,0 @@
=== Maple Local Fonts ===
Contributors: mapleopentech
Tags: fonts, google fonts, local fonts, gdpr, typography, privacy
Requires at least: 6.5
Tested up to: 6.7
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Import Google Fonts to local storage for GDPR-compliant, privacy-friendly typography. Fonts are served from your server, not Google.
== Description ==
**Maple Local Fonts** allows you to import Google Fonts to your WordPress site's local storage. Once imported, fonts are served directly from your server — Google is never contacted again.
= Why Local Fonts? =
* **GDPR Compliance**: Serving fonts from Google's servers can transfer visitor IP addresses to Google, which may violate GDPR. Local fonts eliminate this concern.
* **Performance**: Local fonts can load faster since they're served from your own server without additional DNS lookups.
* **Privacy**: Your visitors' data stays private — no third-party requests for fonts.
* **Reliability**: Fonts are always available, even if Google's services are blocked or unavailable.
= Features =
* **Simple Import**: Enter a font name, select weights and styles, and click to import.
* **WordPress Integration**: Fonts automatically appear in the Full Site Editor typography settings.
* **Clean Removal**: Uninstalling the plugin removes all imported fonts and files.
* **No Frontend Impact**: Zero JavaScript or CSS added to your frontend — only font files are served.
= How It Works =
1. Enter the Google Fonts name (e.g., "Open Sans")
2. Select the weights (400, 700, etc.) and styles (normal, italic)
3. Click "Download & Install"
4. The plugin downloads the font files and registers them with WordPress
5. Use the fonts in Appearance → Editor → Styles → Typography
= Requirements =
* WordPress 6.5 or higher (required for Font Library API)
* PHP 7.4 or higher
== Installation ==
1. Upload the `maple-local-fonts` folder to `/wp-content/plugins/`
2. Activate the plugin through the Plugins menu
3. Go to Appearance → Local Fonts
4. Import your desired fonts
== Frequently Asked Questions ==
= Does this plugin contact Google after importing a font? =
No. The plugin only contacts Google Fonts once, during the import process. After that, fonts are served entirely from your server.
= Where are the font files stored? =
Font files are stored in `wp-content/fonts/`, which is WordPress's default font directory.
= Can I use these fonts with any theme? =
**Block Themes (FSE):** Imported fonts automatically appear in Appearance → Editor → Styles → Typography.
**Classic Themes:** Fonts are registered but must be applied via custom CSS. After importing, add CSS like this to your theme or Customizer:
`body { font-family: "Open Sans", sans-serif; }`
The plugin will detect your theme type and show appropriate instructions.
= What happens to the fonts if I uninstall the plugin? =
When you delete the plugin, all imported fonts and their files are removed. This ensures a clean uninstall.
= Is this plugin GDPR compliant? =
Yes. After importing, fonts are served from your own server. No visitor data is sent to Google or any third party.
= What font formats are downloaded? =
The plugin downloads WOFF2 format, which is the most efficient and widely supported web font format.
= Can I import fonts that aren't on Google Fonts? =
No, this plugin only supports importing from Google Fonts. For other fonts, you would need to manually upload them.
= I'm using Wordfence/a firewall and imports are failing =
If font imports fail, your firewall may be blocking outbound requests. Whitelist these domains:
* `fonts.googleapis.com` (for font CSS)
* `fonts.gstatic.com` (for font files)
**Wordfence:** Go to Wordfence → Firewall → Blocking and ensure these domains aren't blocked. If using Learning Mode, imports should work and the domains will be automatically learned.
**Other firewalls:** Add the above domains to your outbound request allowlist.
= The plugin says it requires WordPress 6.5 =
WordPress 6.5 introduced the Font Library API that this plugin uses. Earlier versions don't support local font registration. Please update WordPress to use this plugin.
== Compatibility ==
= Tested With =
* **WordPress:** 6.5, 6.6, 6.7
* **WooCommerce:** 8.0+ (HPOS compatible)
* **Wordfence:** 7.10+ (see FAQ for firewall settings)
* **LearnDash:** 4.10+
* **Popular Block Themes:** Twenty Twenty-Four, Twenty Twenty-Five
= Theme Compatibility =
* **Block Themes (FSE):** Full support via Site Editor typography settings
* **Classic Themes:** Supported via custom CSS (instructions shown in admin)
== Privacy ==
= Data Collection =
This plugin does not collect any personal data from your website visitors.
= External Services =
This plugin connects to Google Fonts (fonts.googleapis.com and fonts.gstatic.com) **only during the font import process** in the WordPress admin area. This connection is:
* Initiated only by administrators
* Used only to download font files
* Not repeated after initial download
After import, no external connections are made. Fonts are served entirely from your server.
= Cookies =
This plugin does not set any cookies.
== Screenshots ==
1. The font import interface
2. Installed fonts list with delete option
3. Fonts appearing in the Full Site Editor typography settings
== Changelog ==
= 1.0.0 =
* Initial release
* Import Google Fonts to local storage
* WordPress Font Library integration
* Admin interface for font management
* Clean uninstall functionality
== Upgrade Notice ==
= 1.0.0 =
Initial release of Maple Local Fonts.

View file

@ -1,40 +0,0 @@
<?php
/**
* PHPUnit bootstrap file for Maple Local Fonts.
*
* @package Maple_Local_Fonts
*/
// Composer autoloader.
$autoload = dirname(__DIR__) . '/vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
// Get the tests directory.
$_tests_dir = getenv('WP_TESTS_DIR');
// Try to find the WP test suite.
if (!$_tests_dir) {
$_tests_dir = rtrim(sys_get_temp_dir(), '/\\') . '/wordpress-tests-lib';
}
// Check if the test suite exists.
if (!file_exists($_tests_dir . '/includes/functions.php')) {
echo "Could not find $_tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh?" . PHP_EOL;
exit(1);
}
// Give access to tests_add_filter() function.
require_once $_tests_dir . '/includes/functions.php';
/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
require dirname(__DIR__) . '/maple-local-fonts.php';
}
tests_add_filter('muplugins_loaded', '_manually_load_plugin');
// Start up the WP testing environment.
require $_tests_dir . '/includes/bootstrap.php';

View file

@ -1,2 +0,0 @@
<?php
// Silence is golden.

View file

@ -1,426 +0,0 @@
<?php
/**
* Tests for MLF_Ajax_Handler class.
*
* @package Maple_Local_Fonts
*/
/**
* Class Test_MLF_Ajax_Handler
*
* @covers MLF_Ajax_Handler
*/
class Test_MLF_Ajax_Handler extends WP_Ajax_UnitTestCase {
/**
* Admin user ID.
*
* @var int
*/
private $admin_id;
/**
* Subscriber user ID.
*
* @var int
*/
private $subscriber_id;
/**
* Set up test fixtures.
*/
public function set_up() {
parent::set_up();
// Create users
$this->admin_id = $this->factory->user->create(['role' => 'administrator']);
$this->subscriber_id = $this->factory->user->create(['role' => 'subscriber']);
// Initialize the handler
new MLF_Ajax_Handler();
}
/**
* Clean up after tests.
*/
public function tear_down() {
// Clean up test fonts
$fonts = get_posts([
'post_type' => 'wp_font_family',
'posts_per_page' => -1,
'meta_key' => '_mlf_imported',
'meta_value' => '1',
'fields' => 'ids',
]);
foreach ($fonts as $font_id) {
wp_delete_post($font_id, true);
}
wp_set_current_user(0);
delete_transient('mlf_imported_fonts_list');
parent::tear_down();
}
/**
* Test download without nonce.
*/
public function test_download_without_nonce() {
wp_set_current_user($this->admin_id);
$_POST['font_name'] = 'Open Sans';
$_POST['weights'] = [400];
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('Security', $response['data']['message']);
throw $e;
}
}
/**
* Test download with invalid nonce.
*/
public function test_download_with_invalid_nonce() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = 'invalid_nonce';
$_POST['font_name'] = 'Open Sans';
$_POST['weights'] = [400];
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
throw $e;
}
}
/**
* Test download without proper capability.
*/
public function test_download_without_capability() {
wp_set_current_user($this->subscriber_id);
$_POST['nonce'] = wp_create_nonce('mlf_download_font');
$_POST['font_name'] = 'Open Sans';
$_POST['weights'] = [400];
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('Unauthorized', $response['data']['message']);
throw $e;
}
}
/**
* Test download with empty font name.
*/
public function test_download_empty_font_name() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_download_font');
$_POST['font_name'] = '';
$_POST['weights'] = [400];
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('required', $response['data']['message']);
throw $e;
}
}
/**
* Test download with invalid font name (XSS attempt).
*/
public function test_download_invalid_font_name_xss() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_download_font');
$_POST['font_name'] = '<script>alert("xss")</script>';
$_POST['weights'] = [400];
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('Invalid font name', $response['data']['message']);
throw $e;
}
}
/**
* Test download with path traversal in font name.
*/
public function test_download_path_traversal() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_download_font');
$_POST['font_name'] = '../../../etc/passwd';
$_POST['weights'] = [400];
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('Invalid font name', $response['data']['message']);
throw $e;
}
}
/**
* Test download with font name too long.
*/
public function test_download_font_name_too_long() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_download_font');
$_POST['font_name'] = str_repeat('a', 101);
$_POST['weights'] = [400];
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('too long', $response['data']['message']);
throw $e;
}
}
/**
* Test download with no weights.
*/
public function test_download_no_weights() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_download_font');
$_POST['font_name'] = 'Open Sans';
$_POST['weights'] = [];
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('weight', $response['data']['message']);
throw $e;
}
}
/**
* Test download with invalid weights.
*/
public function test_download_invalid_weights() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_download_font');
$_POST['font_name'] = 'Open Sans';
$_POST['weights'] = [999, 1000]; // Invalid weights
$_POST['styles'] = ['normal'];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('weight', $response['data']['message']);
throw $e;
}
}
/**
* Test download with no styles.
*/
public function test_download_no_styles() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_download_font');
$_POST['font_name'] = 'Open Sans';
$_POST['weights'] = [400];
$_POST['styles'] = [];
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_download_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('style', $response['data']['message']);
throw $e;
}
}
/**
* Test delete without nonce.
*/
public function test_delete_without_nonce() {
wp_set_current_user($this->admin_id);
$font_id = $this->create_test_font();
$_POST['font_id'] = $font_id;
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_delete_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
throw $e;
}
}
/**
* Test delete without capability.
*/
public function test_delete_without_capability() {
wp_set_current_user($this->subscriber_id);
$font_id = $this->create_test_font();
$_POST['nonce'] = wp_create_nonce('mlf_delete_font');
$_POST['font_id'] = $font_id;
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_delete_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('Unauthorized', $response['data']['message']);
throw $e;
}
}
/**
* Test delete with invalid font ID.
*/
public function test_delete_invalid_font_id() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_delete_font');
$_POST['font_id'] = 0;
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_delete_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('Invalid', $response['data']['message']);
throw $e;
}
}
/**
* Test delete font not found.
*/
public function test_delete_font_not_found() {
wp_set_current_user($this->admin_id);
$_POST['nonce'] = wp_create_nonce('mlf_delete_font');
$_POST['font_id'] = 99999;
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_delete_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('not found', $response['data']['message']);
throw $e;
}
}
/**
* Test delete font not imported by plugin.
*/
public function test_delete_font_not_ours() {
wp_set_current_user($this->admin_id);
// Create font without our meta
$font_id = wp_insert_post([
'post_type' => 'wp_font_family',
'post_title' => 'Theme Font',
'post_status' => 'publish',
]);
$_POST['nonce'] = wp_create_nonce('mlf_delete_font');
$_POST['font_id'] = $font_id;
$this->expectException('WPAjaxDieStopException');
try {
$this->_handleAjax('mlf_delete_font');
} catch (WPAjaxDieStopException $e) {
$response = json_decode($this->_last_response, true);
$this->assertFalse($response['success']);
$this->assertStringContainsString('not imported', $response['data']['message']);
throw $e;
}
// Clean up
wp_delete_post($font_id, true);
}
/**
* Helper to create a test font.
*
* @return int Font ID.
*/
private function create_test_font() {
$font_id = wp_insert_post([
'post_type' => 'wp_font_family',
'post_title' => 'Test Font',
'post_name' => 'test-font',
'post_status' => 'publish',
]);
update_post_meta($font_id, '_mlf_imported', '1');
return $font_id;
}
}

View file

@ -1,303 +0,0 @@
<?php
/**
* Tests for MLF_Font_Registry class.
*
* @package Maple_Local_Fonts
*/
/**
* Class Test_MLF_Font_Registry
*
* @covers MLF_Font_Registry
*/
class Test_MLF_Font_Registry extends WP_UnitTestCase {
/**
* Font registry instance.
*
* @var MLF_Font_Registry
*/
private $registry;
/**
* Set up test fixtures.
*/
public function set_up() {
parent::set_up();
$this->registry = new MLF_Font_Registry();
// Clear font cache
delete_transient('mlf_imported_fonts_list');
}
/**
* Clean up after tests.
*/
public function tear_down() {
// Clean up any test fonts
$fonts = get_posts([
'post_type' => 'wp_font_family',
'posts_per_page' => -1,
'meta_key' => '_mlf_imported',
'meta_value' => '1',
'fields' => 'ids',
]);
foreach ($fonts as $font_id) {
wp_delete_post($font_id, true);
}
delete_transient('mlf_imported_fonts_list');
parent::tear_down();
}
/**
* Test that get_imported_fonts returns empty array when no fonts installed.
*/
public function test_get_imported_fonts_empty() {
$fonts = $this->registry->get_imported_fonts();
$this->assertIsArray($fonts);
$this->assertEmpty($fonts);
}
/**
* Test font registration.
*/
public function test_register_font() {
$font_name = 'Test Font';
$font_slug = 'test-font';
$files = [
[
'path' => '/tmp/test-font_normal_400.woff2',
'weight' => '400',
'style' => 'normal',
],
];
// Create a dummy file for the test
file_put_contents($files[0]['path'], 'wOF2dummy');
$result = $this->registry->register_font($font_name, $font_slug, $files);
$this->assertIsInt($result);
$this->assertGreaterThan(0, $result);
// Verify font was registered
$font = get_post($result);
$this->assertEquals('wp_font_family', $font->post_type);
$this->assertEquals($font_name, $font->post_title);
// Clean up
unlink($files[0]['path']);
}
/**
* Test that duplicate fonts are rejected.
*/
public function test_register_font_duplicate() {
$font_name = 'Duplicate Font';
$font_slug = 'duplicate-font';
$files = [
[
'path' => '/tmp/duplicate-font_normal_400.woff2',
'weight' => '400',
'style' => 'normal',
],
];
file_put_contents($files[0]['path'], 'wOF2dummy');
// Register first time
$result1 = $this->registry->register_font($font_name, $font_slug, $files);
$this->assertIsInt($result1);
// Try to register again
$result2 = $this->registry->register_font($font_name, $font_slug, $files);
$this->assertWPError($result2);
$this->assertEquals('font_exists', $result2->get_error_code());
// Clean up
unlink($files[0]['path']);
}
/**
* Test get_imported_fonts returns registered fonts.
*/
public function test_get_imported_fonts_returns_fonts() {
$font_name = 'Listed Font';
$font_slug = 'listed-font';
$files = [
[
'path' => '/tmp/listed-font_normal_400.woff2',
'weight' => '400',
'style' => 'normal',
],
[
'path' => '/tmp/listed-font_normal_700.woff2',
'weight' => '700',
'style' => 'normal',
],
];
foreach ($files as $file) {
file_put_contents($file['path'], 'wOF2dummy');
}
$font_id = $this->registry->register_font($font_name, $font_slug, $files);
// Clear cache to test fresh retrieval
delete_transient('mlf_imported_fonts_list');
$fonts = $this->registry->get_imported_fonts();
$this->assertCount(1, $fonts);
$this->assertEquals($font_id, $fonts[0]['id']);
$this->assertEquals($font_name, $fonts[0]['name']);
$this->assertEquals($font_slug, $fonts[0]['slug']);
$this->assertCount(2, $fonts[0]['variants']);
// Clean up
foreach ($files as $file) {
unlink($file['path']);
}
}
/**
* Test font deletion.
*/
public function test_delete_font() {
$font_name = 'Delete Font';
$font_slug = 'delete-font';
$files = [
[
'path' => '/tmp/delete-font_normal_400.woff2',
'weight' => '400',
'style' => 'normal',
],
];
file_put_contents($files[0]['path'], 'wOF2dummy');
$font_id = $this->registry->register_font($font_name, $font_slug, $files);
// Delete the font
$result = $this->registry->delete_font($font_id);
$this->assertTrue($result);
// Verify font is gone
$font = get_post($font_id);
$this->assertNull($font);
}
/**
* Test delete_font rejects non-existent fonts.
*/
public function test_delete_font_not_found() {
$result = $this->registry->delete_font(99999);
$this->assertWPError($result);
$this->assertEquals('not_found', $result->get_error_code());
}
/**
* Test delete_font rejects fonts not imported by plugin.
*/
public function test_delete_font_not_ours() {
// Create a font family post without our meta
$font_id = wp_insert_post([
'post_type' => 'wp_font_family',
'post_title' => 'Theme Font',
'post_status' => 'publish',
]);
$result = $this->registry->delete_font($font_id);
$this->assertWPError($result);
$this->assertEquals('not_ours', $result->get_error_code());
// Clean up
wp_delete_post($font_id, true);
}
/**
* Test that cache is cleared on register.
*/
public function test_cache_cleared_on_register() {
// Set a dummy cache
set_transient('mlf_imported_fonts_list', ['cached' => true], 300);
$font_name = 'Cache Test Font';
$font_slug = 'cache-test-font';
$files = [
[
'path' => '/tmp/cache-test-font_normal_400.woff2',
'weight' => '400',
'style' => 'normal',
],
];
file_put_contents($files[0]['path'], 'wOF2dummy');
$this->registry->register_font($font_name, $font_slug, $files);
// Cache should be cleared
$cached = get_transient('mlf_imported_fonts_list');
$this->assertFalse($cached);
// Clean up
unlink($files[0]['path']);
}
/**
* Test that cache is cleared on delete.
*/
public function test_cache_cleared_on_delete() {
$font_name = 'Cache Delete Font';
$font_slug = 'cache-delete-font';
$files = [
[
'path' => '/tmp/cache-delete-font_normal_400.woff2',
'weight' => '400',
'style' => 'normal',
],
];
file_put_contents($files[0]['path'], 'wOF2dummy');
$font_id = $this->registry->register_font($font_name, $font_slug, $files);
// Set a dummy cache
set_transient('mlf_imported_fonts_list', ['cached' => true], 300);
$this->registry->delete_font($font_id);
// Cache should be cleared
$cached = get_transient('mlf_imported_fonts_list');
$this->assertFalse($cached);
}
/**
* Test font_exists method.
*/
public function test_font_exists() {
$font_name = 'Exists Font';
$font_slug = 'exists-font';
$files = [
[
'path' => '/tmp/exists-font_normal_400.woff2',
'weight' => '400',
'style' => 'normal',
],
];
file_put_contents($files[0]['path'], 'wOF2dummy');
// Before registration
$this->assertFalse($this->registry->font_exists($font_slug));
// After registration
$this->registry->register_font($font_name, $font_slug, $files);
$this->assertTrue($this->registry->font_exists($font_slug));
// Clean up
unlink($files[0]['path']);
}
}

View file

@ -1,177 +0,0 @@
<?php
/**
* Tests for MLF_Rate_Limiter class.
*
* @package Maple_Local_Fonts
*/
/**
* Class Test_MLF_Rate_Limiter
*
* @covers MLF_Rate_Limiter
*/
class Test_MLF_Rate_Limiter extends WP_UnitTestCase {
/**
* Rate limiter instance.
*
* @var MLF_Rate_Limiter
*/
private $rate_limiter;
/**
* Set up test fixtures.
*/
public function set_up() {
parent::set_up();
$this->rate_limiter = new MLF_Rate_Limiter(3, 60); // 3 requests per 60 seconds
}
/**
* Clean up after tests.
*/
public function tear_down() {
// Clear any transients
$this->rate_limiter->clear('test_action');
parent::tear_down();
}
/**
* Test that first request is not limited.
*/
public function test_first_request_not_limited() {
$this->assertFalse($this->rate_limiter->is_limited('test_action'));
}
/**
* Test that remaining count starts at limit.
*/
public function test_remaining_starts_at_limit() {
$this->assertEquals(3, $this->rate_limiter->get_remaining('test_action'));
}
/**
* Test that check_and_record allows requests within limit.
*/
public function test_check_and_record_allows_within_limit() {
$this->assertTrue($this->rate_limiter->check_and_record('test_action'));
$this->assertTrue($this->rate_limiter->check_and_record('test_action'));
$this->assertTrue($this->rate_limiter->check_and_record('test_action'));
}
/**
* Test that check_and_record blocks after limit exceeded.
*/
public function test_check_and_record_blocks_after_limit() {
// Use up all allowed requests
$this->rate_limiter->check_and_record('test_action');
$this->rate_limiter->check_and_record('test_action');
$this->rate_limiter->check_and_record('test_action');
// Next request should be blocked
$this->assertFalse($this->rate_limiter->check_and_record('test_action'));
}
/**
* Test that remaining count decreases correctly.
*/
public function test_remaining_decreases() {
$this->rate_limiter->record_request('test_action');
$this->assertEquals(2, $this->rate_limiter->get_remaining('test_action'));
$this->rate_limiter->record_request('test_action');
$this->assertEquals(1, $this->rate_limiter->get_remaining('test_action'));
$this->rate_limiter->record_request('test_action');
$this->assertEquals(0, $this->rate_limiter->get_remaining('test_action'));
}
/**
* Test that is_limited returns true after limit exceeded.
*/
public function test_is_limited_after_exceeding() {
$this->rate_limiter->record_request('test_action');
$this->rate_limiter->record_request('test_action');
$this->rate_limiter->record_request('test_action');
$this->assertTrue($this->rate_limiter->is_limited('test_action'));
}
/**
* Test that clear resets the rate limit.
*/
public function test_clear_resets_limit() {
// Use up all requests
$this->rate_limiter->record_request('test_action');
$this->rate_limiter->record_request('test_action');
$this->rate_limiter->record_request('test_action');
$this->assertTrue($this->rate_limiter->is_limited('test_action'));
// Clear and verify reset
$this->rate_limiter->clear('test_action');
$this->assertFalse($this->rate_limiter->is_limited('test_action'));
$this->assertEquals(3, $this->rate_limiter->get_remaining('test_action'));
}
/**
* Test that different actions are tracked separately.
*/
public function test_different_actions_tracked_separately() {
// Use up all requests for action1
$this->rate_limiter->record_request('action1');
$this->rate_limiter->record_request('action1');
$this->rate_limiter->record_request('action1');
// action1 should be limited
$this->assertTrue($this->rate_limiter->is_limited('action1'));
// action2 should not be limited
$this->assertFalse($this->rate_limiter->is_limited('action2'));
$this->assertEquals(3, $this->rate_limiter->get_remaining('action2'));
// Clean up
$this->rate_limiter->clear('action1');
$this->rate_limiter->clear('action2');
}
/**
* Test that logged in user uses user ID for tracking.
*/
public function test_logged_in_user_tracking() {
// Create and log in a user
$user_id = $this->factory->user->create(['role' => 'administrator']);
wp_set_current_user($user_id);
// Use up requests
$this->rate_limiter->record_request('test_action');
$this->rate_limiter->record_request('test_action');
$this->rate_limiter->record_request('test_action');
$this->assertTrue($this->rate_limiter->is_limited('test_action'));
// Clean up
wp_set_current_user(0);
$this->rate_limiter->clear('test_action');
}
/**
* Test constructor with custom limits.
*/
public function test_custom_limits() {
$custom_limiter = new MLF_Rate_Limiter(5, 120);
$this->assertEquals(5, $custom_limiter->get_remaining('custom_action'));
// Use 5 requests
for ($i = 0; $i < 5; $i++) {
$this->assertTrue($custom_limiter->check_and_record('custom_action'));
}
// 6th should be blocked
$this->assertFalse($custom_limiter->check_and_record('custom_action'));
// Clean up
$custom_limiter->clear('custom_action');
}
}

View file

@ -1,387 +0,0 @@
<?php
/**
* Tests for MLF_Rest_Controller class.
*
* @package Maple_Local_Fonts
*/
/**
* Class Test_MLF_Rest_Controller
*
* @covers MLF_Rest_Controller
*/
class Test_MLF_Rest_Controller extends WP_Test_REST_Controller_Testcase {
/**
* REST controller instance.
*
* @var MLF_Rest_Controller
*/
private $controller;
/**
* Admin user ID.
*
* @var int
*/
private $admin_id;
/**
* Subscriber user ID.
*
* @var int
*/
private $subscriber_id;
/**
* Set up test fixtures.
*/
public function set_up() {
parent::set_up();
// Register REST routes
$this->controller = new MLF_Rest_Controller();
$this->controller->register_routes();
// Create users
$this->admin_id = $this->factory->user->create(['role' => 'administrator']);
$this->subscriber_id = $this->factory->user->create(['role' => 'subscriber']);
// Clear any existing fonts and cache
delete_transient('mlf_imported_fonts_list');
}
/**
* Clean up after tests.
*/
public function tear_down() {
// Clean up test fonts
$fonts = get_posts([
'post_type' => 'wp_font_family',
'posts_per_page' => -1,
'meta_key' => '_mlf_imported',
'meta_value' => '1',
'fields' => 'ids',
]);
foreach ($fonts as $font_id) {
wp_delete_post($font_id, true);
}
wp_set_current_user(0);
delete_transient('mlf_imported_fonts_list');
parent::tear_down();
}
/**
* Test route registration.
*/
public function test_register_routes() {
$routes = rest_get_server()->get_routes();
$this->assertArrayHasKey('/mlf/v1/fonts', $routes);
$this->assertArrayHasKey('/mlf/v1/fonts/(?P<id>[\d]+)', $routes);
}
/**
* Test get_items without authentication.
*/
public function test_get_items_unauthenticated() {
$request = new WP_REST_Request('GET', '/mlf/v1/fonts');
$response = rest_get_server()->dispatch($request);
$this->assertEquals(401, $response->get_status());
}
/**
* Test get_items with subscriber (insufficient permissions).
*/
public function test_get_items_insufficient_permissions() {
wp_set_current_user($this->subscriber_id);
$request = new WP_REST_Request('GET', '/mlf/v1/fonts');
$response = rest_get_server()->dispatch($request);
$this->assertEquals(403, $response->get_status());
}
/**
* Test get_items with admin.
*/
public function test_get_items_as_admin() {
wp_set_current_user($this->admin_id);
$request = new WP_REST_Request('GET', '/mlf/v1/fonts');
$response = rest_get_server()->dispatch($request);
$this->assertEquals(200, $response->get_status());
$this->assertIsArray($response->get_data());
}
/**
* Test get_items returns installed fonts.
*/
public function test_get_items_returns_fonts() {
wp_set_current_user($this->admin_id);
// Create a test font
$font_id = $this->create_test_font('Test API Font', 'test-api-font');
$request = new WP_REST_Request('GET', '/mlf/v1/fonts');
$response = rest_get_server()->dispatch($request);
$this->assertEquals(200, $response->get_status());
$data = $response->get_data();
$this->assertCount(1, $data);
$this->assertEquals($font_id, $data[0]['id']);
$this->assertEquals('Test API Font', $data[0]['name']);
}
/**
* Test get_item.
*/
public function test_get_item() {
wp_set_current_user($this->admin_id);
$font_id = $this->create_test_font('Single Font', 'single-font');
$request = new WP_REST_Request('GET', '/mlf/v1/fonts/' . $font_id);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(200, $response->get_status());
$data = $response->get_data();
$this->assertEquals($font_id, $data['id']);
$this->assertEquals('Single Font', $data['name']);
}
/**
* Test get_item not found.
*/
public function test_get_item_not_found() {
wp_set_current_user($this->admin_id);
$request = new WP_REST_Request('GET', '/mlf/v1/fonts/99999');
$response = rest_get_server()->dispatch($request);
$this->assertEquals(404, $response->get_status());
}
/**
* Test create_item without authentication.
*/
public function test_create_item_unauthenticated() {
$request = new WP_REST_Request('POST', '/mlf/v1/fonts');
$request->set_param('font_name', 'Open Sans');
$request->set_param('weights', [400, 700]);
$request->set_param('styles', ['normal']);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(401, $response->get_status());
}
/**
* Test create_item with subscriber.
*/
public function test_create_item_insufficient_permissions() {
wp_set_current_user($this->subscriber_id);
$request = new WP_REST_Request('POST', '/mlf/v1/fonts');
$request->set_param('font_name', 'Open Sans');
$request->set_param('weights', [400, 700]);
$request->set_param('styles', ['normal']);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(403, $response->get_status());
}
/**
* Test create_item with invalid font name.
*/
public function test_create_item_invalid_font_name() {
wp_set_current_user($this->admin_id);
$request = new WP_REST_Request('POST', '/mlf/v1/fonts');
$request->set_param('font_name', '<script>alert("xss")</script>');
$request->set_param('weights', [400]);
$request->set_param('styles', ['normal']);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(400, $response->get_status());
}
/**
* Test create_item with no weights.
*/
public function test_create_item_no_weights() {
wp_set_current_user($this->admin_id);
$request = new WP_REST_Request('POST', '/mlf/v1/fonts');
$request->set_param('font_name', 'Test Font');
$request->set_param('weights', []);
$request->set_param('styles', ['normal']);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(400, $response->get_status());
}
/**
* Test create_item with no styles.
*/
public function test_create_item_no_styles() {
wp_set_current_user($this->admin_id);
$request = new WP_REST_Request('POST', '/mlf/v1/fonts');
$request->set_param('font_name', 'Test Font');
$request->set_param('weights', [400]);
$request->set_param('styles', []);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(400, $response->get_status());
}
/**
* Test delete_item without authentication.
*/
public function test_delete_item_unauthenticated() {
$font_id = $this->create_test_font('Delete Font', 'delete-font');
$request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/' . $font_id);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(401, $response->get_status());
}
/**
* Test delete_item with subscriber.
*/
public function test_delete_item_insufficient_permissions() {
$font_id = $this->create_test_font('Delete Font 2', 'delete-font-2');
wp_set_current_user($this->subscriber_id);
$request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/' . $font_id);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(403, $response->get_status());
}
/**
* Test delete_item as admin.
*/
public function test_delete_item_as_admin() {
$font_id = $this->create_test_font('Delete Font 3', 'delete-font-3');
wp_set_current_user($this->admin_id);
$request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/' . $font_id);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(200, $response->get_status());
$data = $response->get_data();
$this->assertTrue($data['deleted']);
}
/**
* Test delete_item not found.
*/
public function test_delete_item_not_found() {
wp_set_current_user($this->admin_id);
$request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/99999');
$response = rest_get_server()->dispatch($request);
$this->assertEquals(404, $response->get_status());
}
/**
* Test delete_item for font not imported by plugin.
*/
public function test_delete_item_not_ours() {
wp_set_current_user($this->admin_id);
// Create a font without our meta
$font_id = wp_insert_post([
'post_type' => 'wp_font_family',
'post_title' => 'Theme Font',
'post_status' => 'publish',
]);
$request = new WP_REST_Request('DELETE', '/mlf/v1/fonts/' . $font_id);
$response = rest_get_server()->dispatch($request);
$this->assertEquals(403, $response->get_status());
// Clean up
wp_delete_post($font_id, true);
}
/**
* Test item schema.
*/
public function test_get_item_schema() {
$request = new WP_REST_Request('OPTIONS', '/mlf/v1/fonts');
$response = rest_get_server()->dispatch($request);
$data = $response->get_data();
$this->assertArrayHasKey('schema', $data);
$this->assertEquals('font', $data['schema']['title']);
}
/**
* Test context parameter.
*/
public function test_context_param() {
// This test is inherited from WP_Test_REST_Controller_Testcase
// We just need to implement it to satisfy the abstract requirement
$this->assertTrue(true);
}
/**
* Helper to create a test font.
*
* @param string $name Font name.
* @param string $slug Font slug.
* @return int Font ID.
*/
private function create_test_font($name, $slug) {
// Create font family post
$font_id = wp_insert_post([
'post_type' => 'wp_font_family',
'post_title' => $name,
'post_name' => $slug,
'post_status' => 'publish',
'post_content' => wp_json_encode([
'fontFamily' => $name,
'slug' => $slug,
]),
]);
// Add our meta marker
update_post_meta($font_id, '_mlf_imported', '1');
// Create a font face
$face_id = wp_insert_post([
'post_type' => 'wp_font_face',
'post_title' => $name . ' 400 normal',
'post_status' => 'publish',
'post_parent' => $font_id,
'post_content' => wp_json_encode([
'fontFamily' => $name,
'fontWeight' => '400',
'fontStyle' => 'normal',
'src' => ['file:./fonts/' . $slug . '_normal_400.woff2'],
]),
]);
// Clear cache
delete_transient('mlf_imported_fonts_list');
return $font_id;
}
}

View file

@ -1,124 +0,0 @@
<?php
/**
* Uninstall script for Maple Local Fonts.
*
* This file is executed when the plugin is deleted from WordPress.
* It removes all fonts imported by this plugin and cleans up any data.
*
* @package Maple_Local_Fonts
*/
// If uninstall not called from WordPress, exit.
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
/**
* Clean up all plugin data on uninstall.
*
* Uses batched processing to prevent memory exhaustion on sites with many fonts.
*/
function mlf_uninstall() {
$font_dir = wp_get_font_dir();
$batch_size = 50; // Process 50 fonts at a time to prevent memory issues
$processed = 0;
$max_iterations = 100; // Safety limit: max 5000 fonts (100 × 50)
// Process fonts in batches
for ($i = 0; $i < $max_iterations; $i++) {
$fonts = get_posts([
'post_type' => 'wp_font_family',
'posts_per_page' => $batch_size,
'post_status' => 'any',
'meta_key' => '_mlf_imported',
'meta_value' => '1',
'fields' => 'ids', // Only get IDs for memory efficiency
]);
// No more fonts to process
if (empty($fonts)) {
break;
}
// Get all font faces for this batch in a single query
$all_faces = get_posts([
'post_type' => 'wp_font_face',
'posts_per_page' => $batch_size * 20, // Max ~20 faces per font
'post_status' => 'any',
'post_parent__in' => $fonts,
]);
// Delete font face files and posts
foreach ($all_faces as $face) {
$settings = json_decode($face->post_content, true);
if (isset($settings['src'])) {
// Convert file:. URL to path
$src = $settings['src'];
$src = str_replace('file:./', '', $src);
$file_path = trailingslashit($font_dir['path']) . basename($src);
// Validate path and extension before deletion
if (mlf_uninstall_validate_font_path($file_path, $font_dir)
&& pathinfo($file_path, PATHINFO_EXTENSION) === 'woff2'
&& file_exists($file_path)) {
wp_delete_file($file_path);
}
}
wp_delete_post($face->ID, true);
}
// Delete family posts
foreach ($fonts as $font_id) {
wp_delete_post($font_id, true);
}
$fonts_count = count($fonts);
$processed += $fonts_count;
// Free memory
unset($fonts, $all_faces);
// If we got fewer than batch_size, we're done
if ($fonts_count < $batch_size) {
break;
}
}
// Clear caches
delete_transient('wp_font_library_fonts');
delete_transient('mlf_imported_fonts_list');
}
/**
* Validate that a path is within the WordPress fonts directory.
*
* @param string $path Full path to validate.
* @param array $font_dir Font directory info.
* @return bool True if path is safe, false otherwise.
*/
function mlf_uninstall_validate_font_path($path, $font_dir) {
$fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));
// Resolve to real path (handles ../ etc)
$real_path = realpath($path);
// If realpath fails, file doesn't exist yet - validate the directory
if ($real_path === false) {
$dir = dirname($path);
$real_dir = realpath($dir);
if ($real_dir === false) {
return false;
}
$real_path = wp_normalize_path($real_dir . '/' . basename($path));
} else {
$real_path = wp_normalize_path($real_path);
}
// Must be within fonts directory
return strpos($real_path, $fonts_path) === 0;
}
// Run uninstall
mlf_uninstall();

View file

@ -1,827 +0,0 @@
# CLAUDE.md — Maple Icons WordPress Plugin
## Quick Reference
| Document | When to Reference |
|----------|-------------------|
| **CLAUDE.md** (this file) | Architecture, file structure, build order |
| **SECURITY.md** | Writing ANY PHP code (same patterns as Maple Local Fonts) |
---
## Project Overview
Build a WordPress plugin called **Maple Icons** that:
1. Fetches open-source icon sets from CDN and stores them locally
2. Provides preset icon sets: Heroicons, Lucide, Feather, Phosphor, Material
3. Only ONE icon set can be active at a time (though multiple can be downloaded)
4. Offers a Gutenberg block "Maple Icons" for inserting icons
5. Icons use `currentColor` to inherit text color from Global Styles
**Key principle:** Download once, serve locally. Zero runtime external requests after initial fetch.
---
## Requirements
- **Minimum PHP:** 7.4
- **Minimum WordPress:** 6.5 (for block.json apiVersion 3 and Global Styles)
- **License:** GPL-2.0-or-later
---
## Preset Icon Sets
All icons fetched from jsdelivr CDN with pinned versions:
| Set | Package | Version | Styles | ~Icons | viewBox |
|-----|---------|---------|--------|--------|---------|
| Heroicons | `heroicons` | 2.1.1 | outline, solid, mini | 290 | 24×24 |
| Lucide | `lucide-static` | 0.303.0 | icons | 1,400 | 24×24 |
| Feather | `feather-icons` | 4.29.1 | icons | 280 | 24×24 |
| Phosphor | `@phosphor-icons/core` | 2.1.1 | regular, bold, light, thin, fill, duotone | 1,200× styles | 256×256 (normalize) |
| Material | `@material-design-icons/svg` | 0.14.13 | filled, outlined, round, sharp, two-tone | 2,500 | 24×24 |
### CDN URL Patterns
```
Heroicons: https://cdn.jsdelivr.net/npm/heroicons@2.1.1/24/outline/{name}.svg
Lucide: https://cdn.jsdelivr.net/npm/lucide-static@0.303.0/icons/{name}.svg
Feather: https://cdn.jsdelivr.net/npm/feather-icons@4.29.1/dist/icons/{name}.svg
Phosphor: https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.1.1/assets/{style}/{name}.svg
Material: https://cdn.jsdelivr.net/npm/@material-design-icons/svg@0.14.13/{style}/{name}.svg
```
### SVG Normalization Required
| Set | Issue | Fix |
|-----|-------|-----|
| Phosphor | viewBox="0 0 256 256" | Normalize to 24×24 |
| Material | `fill="#000000"` hardcoded | Replace with `currentColor` |
| All | May have XML declarations, comments | Strip during save |
---
## User Flow
```
Admin visits Settings → Maple Icons (or plugin action link)
Sees list of preset icon sets with Download/Delete buttons
Clicks "Download" on Heroicons → progress bar → icons saved locally
Clicks "Set Active" to make Heroicons the active set
User inserts "Maple Icons" block in Gutenberg editor
Block shows + button → click → icon picker modal with search
User searches/filters → selects icon → SVG inserted inline
SVG uses currentColor → inherits from Global Styles
All icons served from local storage (wp-content/maple-icons/)
```
---
## File Structure
```
maple-icons-wp/
├── maple-icons.php # Main plugin file
├── index.php # Silence is golden
├── uninstall.php # Clean removal
├── readme.txt # WordPress.org readme
├── package.json # Block build config
├── includes/
│ ├── index.php
│ ├── class-mi-icon-sets.php # Preset icon set definitions
│ ├── class-mi-icon-registry.php # Icon management & search
│ ├── class-mi-downloader.php # CDN fetch & local storage
│ ├── class-mi-admin-page.php # Settings page
│ └── class-mi-ajax-handler.php # AJAX handlers
├── presets/ # Bundled manifests (icon names, tags)
│ ├── index.php
│ ├── heroicons.json
│ ├── lucide.json
│ ├── feather.json
│ ├── phosphor.json
│ └── material.json
├── build/ # Compiled block assets (generated)
│ ├── index.php
│ ├── index.js
│ ├── index.asset.php
│ └── style-index.css
├── src/ # Block source (for development)
│ ├── index.js # Block registration
│ ├── edit.js # Editor component
│ ├── save.js # Save component (inline SVG)
│ ├── icon-picker.js # Icon selection modal
│ ├── editor.scss # Editor-only styles
│ └── block.json # Block metadata
├── assets/
│ ├── index.php
│ ├── admin.css # Settings page styles
│ └── admin.js # Settings page JS (download progress, etc.)
└── languages/
├── index.php
└── maple-icons.pot
```
### Local Icon Storage
Icons are stored in:
```
wp-content/maple-icons/{set-slug}/{style}/{icon-name}.svg
```
Example:
```
wp-content/maple-icons/heroicons/outline/academic-cap.svg
wp-content/maple-icons/heroicons/solid/academic-cap.svg
wp-content/maple-icons/lucide/icons/activity.svg
wp-content/maple-icons/phosphor/regular/airplane.svg
```
Using `wp-content/maple-icons/` (not uploads) to avoid cleanup plugin interference.
---
## Class Responsibilities
### MI_Icon_Sets
Static definitions of all preset icon sets.
```php
class MI_Icon_Sets {
public static function get_all(): array;
public static function get(string $slug): ?array;
public static function get_cdn_url(string $slug, string $style, string $name): string;
public static function get_manifest_path(string $slug): string;
}
```
Returns structure:
```php
[
'slug' => 'heroicons',
'name' => 'Heroicons',
'version' => '2.1.1',
'license' => 'MIT',
'url' => 'https://heroicons.com',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/heroicons@2.1.1/',
'styles' => [
'outline' => ['path' => '24/outline', 'label' => 'Outline'],
'solid' => ['path' => '24/solid', 'label' => 'Solid'],
'mini' => ['path' => '20/solid', 'label' => 'Mini'],
],
'default_style' => 'outline',
'viewbox' => '0 0 24 24',
'normalize' => false, // true for Phosphor (256→24)
'color_fix' => false, // true for Material (replace hardcoded colors)
]
```
### MI_Icon_Registry
Manages downloaded sets and provides icon search/retrieval.
```php
class MI_Icon_Registry {
public function get_downloaded_sets(): array;
public function get_active_set(): ?string;
public function set_active(string $slug): bool;
public function is_downloaded(string $slug): bool;
public function get_icons_for_set(string $slug, string $style): array;
public function search_icons(string $query, int $limit = 50): array;
public function get_icon_svg(string $slug, string $style, string $name): string|WP_Error;
public function delete_set(string $slug): bool;
}
```
### MI_Downloader
Handles fetching icons from CDN and storing locally.
```php
class MI_Downloader {
public function download_set(string $slug, callable $progress_callback = null): array|WP_Error;
public function download_icon(string $slug, string $style, string $name): string|WP_Error;
private function normalize_svg(string $svg, array $set_config): string;
private function get_local_path(string $slug, string $style, string $name): string;
}
```
### MI_Admin_Page
Settings page under Settings → Maple Icons.
- List all preset icon sets
- Show download status (downloaded/not downloaded)
- Download button with progress indicator
- Delete button for downloaded sets
- Radio buttons to select active set
- Preview sample icons from each downloaded set
### MI_Ajax_Handler
AJAX endpoints:
- `mi_download_set` — Download an icon set from CDN
- `mi_delete_set` — Delete a downloaded icon set
- `mi_set_active` — Set the active icon set
- `mi_search_icons` — Search icons in active set (for block picker)
- `mi_get_icon_svg` — Get specific icon SVG (for block)
---
## Settings Storage
```php
// Option name: maple_icons_settings
[
'active_set' => 'heroicons', // Slug of active set, or empty string
'downloaded_sets' => [
'heroicons' => [
'version' => '2.1.1',
'downloaded_at' => '2024-01-15 10:30:00',
'icon_count' => 876, // Total across all styles
],
'lucide' => [
'version' => '0.303.0',
'downloaded_at' => '2024-01-15 11:00:00',
'icon_count' => 1400,
],
],
]
```
---
## Manifest Format (presets/*.json)
Each preset ships with a manifest listing all icons:
```json
{
"slug": "heroicons",
"name": "Heroicons",
"version": "2.1.1",
"icons": [
{
"name": "academic-cap",
"tags": ["education", "graduation", "school", "hat"],
"category": "objects",
"styles": ["outline", "solid", "mini"]
},
{
"name": "adjustments-horizontal",
"tags": ["settings", "controls", "sliders"],
"category": "ui",
"styles": ["outline", "solid", "mini"]
}
]
}
```
Manifests are bundled with the plugin to avoid runtime dependency on jsdelivr API.
---
## Gutenberg Block Architecture
### Block Registration (block.json)
```json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "jetrails/maple-icons",
"version": "1.0.0",
"title": "Maple Icons",
"category": "design",
"icon": "star-filled",
"description": "Insert an icon from your downloaded icon sets.",
"keywords": ["icon", "svg", "symbol", "maple"],
"textdomain": "maple-icons",
"attributes": {
"iconSet": {
"type": "string",
"default": ""
},
"iconStyle": {
"type": "string",
"default": ""
},
"iconName": {
"type": "string",
"default": ""
},
"iconSVG": {
"type": "string",
"default": ""
},
"size": {
"type": "number",
"default": 24
},
"label": {
"type": "string",
"default": ""
},
"strokeWidth": {
"type": "number",
"default": 0
},
"strokeColor": {
"type": "string",
"default": ""
}
},
"supports": {
"html": false,
"align": ["left", "center", "right"],
"color": {
"text": true,
"background": true,
"gradients": true
},
"spacing": {
"margin": true,
"padding": true
},
"shadow": true
},
"editorScript": "file:./index.js",
"editorStyle": "file:./style-index.css"
}
```
### Block Style Controls (Inspector Panel)
The block sidebar will include these controls:
**Icon Settings:**
- Icon picker button (change icon)
- Size slider (8-256px, default 24)
- Accessibility label text input
**Color Settings (via WordPress supports):**
- Icon color (text color) — inherited by SVG via `currentColor`
- Background color
- Gradient support
**Stroke Settings (custom panel):**
- Stroke width slider (0-10px)
- Stroke color picker
**Spacing (via WordPress supports):**
- Padding controls
- Margin controls
**Effects:**
- Drop shadow presets (via WordPress `shadow` support)
### Style Application
```jsx
// In edit.js and save.js, compute styles:
const iconStyles = {
width: `${size}px`,
height: `${size}px`,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
};
// Stroke is applied via CSS filter or SVG manipulation
const svgStyles = strokeWidth > 0 ? {
filter: `drop-shadow(0 0 0 ${strokeColor})`,
WebkitTextStroke: `${strokeWidth}px ${strokeColor}`,
} : {};
// Note: For true SVG stroke, we modify the SVG's stroke attribute
// This is handled in the SVG wrapper, not CSS
```
### SVG Stroke Handling
For stroke on SVG icons, we wrap the SVG output:
```jsx
// The SVG itself uses currentColor for fill/stroke
// Additional stroke effect applied via CSS or inline style
<span
className="wp-block-maple-icon__svg"
style={{
// Apply stroke via paint-order and stroke properties
'--mi-stroke-width': strokeWidth ? `${strokeWidth}px` : undefined,
'--mi-stroke-color': strokeColor || undefined,
}}
dangerouslySetInnerHTML={{ __html: iconSVG }}
/>
```
With CSS:
```css
.wp-block-maple-icon__svg svg {
stroke: var(--mi-stroke-color, currentColor);
stroke-width: var(--mi-stroke-width, 0);
paint-order: stroke fill;
}
```
### Block Behavior
**Initial State (no icon selected):**
- Shows + button placeholder
- Click opens icon picker modal
**With icon selected:**
- Displays inline SVG
- Sidebar shows: size control, accessibility label, change icon button
**Icon Picker Modal:**
- Search input (filters as you type)
- Style selector dropdown (if set has multiple styles)
- Grid of icon thumbnails
- Click to select → SVG fetched and stored in attributes
### Save Output (save.js)
```jsx
export default function save({ attributes }) {
const { iconSVG, size, label } = attributes;
if (!iconSVG) {
return null;
}
const blockProps = useBlockProps.save({
className: 'wp-block-maple-icon',
style: {
width: `${size}px`,
height: `${size}px`,
display: 'inline-flex',
},
});
return (
<span {...blockProps}>
<span
dangerouslySetInnerHTML={{ __html: iconSVG }}
role={label ? 'img' : 'presentation'}
aria-label={label || undefined}
aria-hidden={!label ? 'true' : undefined}
/>
</span>
);
}
```
---
## Download Process
### Batch Download Flow
1. User clicks "Download" for an icon set
2. Frontend disables button, shows progress bar
3. AJAX request to `mi_download_set`
4. Backend:
a. Load manifest from `presets/{slug}.json`
b. Create local directory structure
c. For each icon in manifest:
- Fetch SVG from CDN
- Normalize (viewBox, currentColor)
- Save to local filesystem
d. Update settings with download info
5. Return success with icon count
6. Frontend updates UI to show "Downloaded" state
### Batching Strategy
To avoid timeouts and memory issues:
- Process icons in batches of 50
- Use streaming/chunked approach
- Allow resume on failure (track progress in transient)
```php
// Download in batches
$batch_size = 50;
$icons = $manifest['icons'];
$total = count($icons);
for ($i = 0; $i < $total; $i += $batch_size) {
$batch = array_slice($icons, $i, $batch_size);
foreach ($batch as $icon) {
// Download each icon in batch
}
// Update progress transient
set_transient('mi_download_progress_' . $slug, [
'completed' => min($i + $batch_size, $total),
'total' => $total,
], HOUR_IN_SECONDS);
}
```
---
## SVG Sanitization
Same approach as original CLAUDE.md — strict allowlist of SVG elements and attributes.
```php
function mi_sanitize_svg($svg) {
$allowed_tags = [
'svg' => ['xmlns', 'viewbox', 'width', 'height', 'fill', 'stroke', ...],
'path' => ['d', 'fill', 'stroke', 'stroke-width', ...],
'circle' => ['cx', 'cy', 'r', 'fill', 'stroke', ...],
'rect' => ['x', 'y', 'width', 'height', 'rx', 'ry', ...],
'line' => ['x1', 'y1', 'x2', 'y2', ...],
'polyline' => ['points', ...],
'polygon' => ['points', ...],
'ellipse' => ['cx', 'cy', 'rx', 'ry', ...],
'g' => ['fill', 'stroke', 'transform', ...],
];
// Remove dangerous content
$svg = preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $svg);
$svg = preg_replace('/\son\w+\s*=/i', ' data-removed=', $svg);
return wp_kses($svg, $allowed_tags);
}
```
### SVG Normalization
```php
function mi_normalize_svg($svg, $set_config) {
// 1. Strip XML declaration
$svg = preg_replace('/<\?xml[^>]*\?>/i', '', $svg);
// 2. Strip comments
$svg = preg_replace('/<!--.*?-->/s', '', $svg);
// 3. Normalize viewBox for Phosphor (256 → 24)
if ($set_config['normalize']) {
$svg = preg_replace('/viewBox=["\']0 0 256 256["\']/', 'viewBox="0 0 24 24"', $svg);
}
// 4. Fix hardcoded colors for Material
if ($set_config['color_fix']) {
$svg = preg_replace('/fill=["\']#[0-9a-fA-F]{3,6}["\']/', 'fill="currentColor"', $svg);
$svg = preg_replace('/fill=["\']black["\']/', 'fill="currentColor"', $svg);
}
// 5. Ensure currentColor is used
// Most sets already use currentColor, but double-check
// 6. Remove width/height attributes (let CSS control)
$svg = preg_replace('/\s(width|height)=["\'][^"\']*["\']/', '', $svg);
return trim($svg);
}
```
---
## Admin Settings Page UI
```
┌─────────────────────────────────────────────────────────────────┐
│ Maple Icons │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ICON SETS │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ◉ Heroicons ✓ Downloaded │ │
│ │ 290 icons · Outline, Solid, Mini · MIT │ │
│ │ [Preview: ⚙ 🏠 👤 ✉ 🔔 ⭐] │ │
│ │ [Delete] │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ ○ Lucide ✓ Downloaded │ │
│ │ 1,400 icons · Icons · ISC │ │
│ │ [Preview: ⚙ 📁 📄 💾 🖨 ⬇] │ │
│ │ [Delete] │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ ○ Feather Not downloaded │ │
│ │ 280 icons · Icons · MIT │ │
│ │ [Download] │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ ○ Phosphor Not downloaded │ │
│ │ 7,200 icons · 6 styles · MIT │ │
│ │ [Download] │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ ○ Material Not downloaded │ │
│ │ 2,500 icons · 5 styles · Apache 2.0 │ │
│ │ [Download] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ◉ = Active icon set (used in Maple Icons block) │
│ │
│ ─────────────────────────────────────────────────────────────── │
│ │
│ USAGE │
│ 1. Download one or more icon sets above │
│ 2. Select which set should be active (radio button) │
│ 3. Insert icons using the "Maple Icons" block in the editor │
│ 4. Icons inherit your theme's text color automatically │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## Icon Picker Modal (Block Editor)
```
┌─────────────────────────────────────────────────────────────────┐
│ Select Icon [X] │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 🔍 Search icons... │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Style: [Outline ▼] │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │ ⚙ │ │ 🏠 │ │ 👤 │ │ ✉ │ │ 🔔 │ │ ⭐ │ │ ❤ │ │ 🔍 │ │ │
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
│ │ cog home user mail bell star heart search │ │
│ │ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │ + │ │ ✓ │ │ ✕ │ │ ⬆ │ │ ⬇ │ │ ◀ │ │ ▶ │ │ ↻ │ │ │
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
│ │ plus check x up down left right refresh │ │
│ │ │ │
│ │ [Load More...] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Selected: arrow-right │
│ │
│ [Cancel] [Insert Icon] │
└─────────────────────────────────────────────────────────────────┘
```
---
## Build Order
### Phase 1: Foundation
1. `maple-icons.php` — Plugin header, constants, autoloader, activation hook
2. `index.php` files in ALL directories
3. `includes/class-mi-icon-sets.php` — Static preset definitions
### Phase 2: Download Infrastructure
4. `includes/class-mi-downloader.php` — CDN fetch, normalize, store
5. `includes/class-mi-icon-registry.php` — Downloaded set management
6. `includes/class-mi-ajax-handler.php` — Download/delete/activate endpoints
7. Create manifest files: `presets/*.json`
### Phase 3: Admin Interface
8. `includes/class-mi-admin-page.php` — Settings page
9. `assets/admin.css` — Settings page styles
10. `assets/admin.js` — Download progress, AJAX handlers
### Phase 4: Gutenberg Block
11. `src/block.json` — Block metadata
12. `src/index.js` — Block registration
13. `src/edit.js` — Editor component
14. `src/save.js` — Save component
15. `src/icon-picker.js` — Modal component
16. `src/editor.scss` — Editor styles
17. `package.json` + build
### Phase 5: Cleanup & Polish
18. `uninstall.php` — Clean removal
19. `readme.txt` — WordPress.org readme
20. Testing
---
## Constants
```php
define('MI_VERSION', '1.0.0');
define('MI_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MI_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MI_PLUGIN_BASENAME', plugin_basename(__FILE__));
define('MI_ICONS_DIR', WP_CONTENT_DIR . '/maple-icons/');
define('MI_ICONS_URL', content_url('/maple-icons/'));
define('MI_PRESETS_DIR', MI_PLUGIN_DIR . 'presets/');
// Limits
define('MI_DOWNLOAD_BATCH_SIZE', 50);
define('MI_SEARCH_LIMIT', 50);
define('MI_DOWNLOAD_TIMEOUT', 10); // Per-icon timeout in seconds
```
---
## Security Checklist
Reference SECURITY.md for full details. Key points:
- [ ] ABSPATH check on every PHP file
- [ ] index.php in every directory
- [ ] Nonce verification FIRST in every AJAX handler
- [ ] Capability check SECOND (`manage_options` for settings, `edit_posts` for block)
- [ ] Validate icon set slugs against preset allowlist
- [ ] Validate style slugs against set's defined styles
- [ ] Sanitize all SVG content before storage
- [ ] Validate file paths (prevent path traversal)
- [ ] Use `wp_remote_get()` for CDN requests
- [ ] Escape all output
---
## WordPress Hooks Used
```php
// Activation/Deactivation
register_activation_hook(__FILE__, 'mi_activate');
register_deactivation_hook(__FILE__, 'mi_deactivate');
// Block registration
add_action('init', 'mi_register_block');
// Admin menu
add_action('admin_menu', 'mi_register_settings_page');
// Admin assets (settings page only)
add_action('admin_enqueue_scripts', 'mi_enqueue_admin_assets');
// Plugin action links (Settings link in plugin list)
add_filter('plugin_action_links_' . MI_PLUGIN_BASENAME, 'mi_add_action_links');
// AJAX handlers (admin only)
add_action('wp_ajax_mi_download_set', 'mi_ajax_download_set');
add_action('wp_ajax_mi_delete_set', 'mi_ajax_delete_set');
add_action('wp_ajax_mi_set_active', 'mi_ajax_set_active');
add_action('wp_ajax_mi_search_icons', 'mi_ajax_search_icons');
add_action('wp_ajax_mi_get_icon_svg', 'mi_ajax_get_icon_svg');
// WooCommerce HPOS compatibility
add_action('before_woocommerce_init', 'mi_declare_hpos_compatibility');
```
---
## Compatibility Notes
### WooCommerce HPOS
Declare compatibility (we don't touch orders):
```php
add_action('before_woocommerce_init', function() {
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
});
```
### Caching Plugins
Icons stored locally means no cache issues. SVG is inline in post_content.
### Wordfence
CDN requests only happen during admin download action, using standard `wp_remote_get()`.
### LearnDash / WPForms
No interference — we only add a block, no frontend hooks.
---
## Testing Checklist
### Download Flow
- [ ] Download Heroicons → all icons saved locally
- [ ] Download progress shows correctly
- [ ] Download Lucide (large set) → doesn't timeout
- [ ] Delete downloaded set → files removed
- [ ] Switch active set → setting persists
### Block Editor
- [ ] Insert Maple Icons block → shows placeholder
- [ ] Click + → opens icon picker
- [ ] Search filters icons correctly
- [ ] Style dropdown works (for sets with multiple styles)
- [ ] Select icon → SVG appears in editor
- [ ] Save post → icon persists
- [ ] Frontend → icon displays correctly
- [ ] Change text color in Global Styles → icon color changes
### Security
- [ ] Download without nonce → 403
- [ ] Download as non-admin → 403
- [ ] Invalid set slug → rejected
- [ ] Path traversal attempt → rejected
### Edge Cases
- [ ] No sets downloaded → block shows helpful message
- [ ] Active set deleted → block handles gracefully
- [ ] CDN unreachable during download → appropriate error
- [ ] Partial download failure → can retry/resume

View file

@ -1,621 +0,0 @@
# SECURITY.md — Maple Local Fonts Security Requirements
## Overview
This document covers all security requirements for the Maple Local Fonts plugin. Reference this when writing ANY PHP code.
---
## ABSPATH Check (Every PHP File)
Every PHP file MUST start with this check. No exceptions.
```php
<?php
if (!defined('ABSPATH')) {
exit;
}
```
---
## Silence is Golden Files
Create `index.php` in EVERY directory:
```php
<?php
// Silence is golden.
if (!defined('ABSPATH')) {
exit;
}
```
**Required locations:**
- `/maple-local-fonts/index.php`
- `/maple-local-fonts/includes/index.php`
- `/maple-local-fonts/assets/index.php`
- `/maple-local-fonts/languages/index.php`
---
## OWASP Compliance
### A1 - Injection Prevention
**SQL Injection:**
```php
// NEVER do this
$wpdb->query("SELECT * FROM table WHERE id = " . $_POST['id']);
// ALWAYS do this
$wpdb->get_results($wpdb->prepare(
"SELECT * FROM %i WHERE id = %d",
$table_name,
absint($_POST['id'])
));
```
**Note:** This plugin should rarely need direct SQL. Use WordPress APIs (`get_posts`, `wp_insert_post`, etc.) which handle escaping internally.
### A2 - Authentication
All admin actions require capability check:
```php
if (!current_user_can('edit_theme_options')) {
wp_die('Unauthorized', 'Error', ['response' => 403]);
}
```
### A3 - Sensitive Data
- No API keys (Google Fonts CSS2 API is public)
- No user credentials stored
- No PII collected
### A5 - Broken Access Control
**Order of checks for ALL AJAX handlers:**
```php
public function handle_ajax_action() {
// 1. Nonce verification FIRST
if (!check_ajax_referer('mlf_action_name', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
// 2. Capability check SECOND
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => 'Unauthorized'], 403);
}
// 3. Input validation THIRD
// ... validate all inputs ...
// 4. Process request
// ... actual logic ...
}
```
### A7 - Cross-Site Scripting (XSS)
**Escape ALL output:**
```php
// HTML content
echo esc_html($font_name);
// HTML attributes
echo '<input value="' . esc_attr($font_name) . '">';
// URLs
echo '<a href="' . esc_url($url) . '">';
// JavaScript data
wp_localize_script('mlf-admin', 'mlfData', [
'fontName' => esc_js($font_name), // Or let wp_localize_script handle it
]);
// Translatable strings with variables
printf(
esc_html__('Installed: %s', 'maple-local-fonts'),
esc_html($font_name)
);
```
**Never trust input for output:**
```php
// WRONG - XSS vulnerability
echo '<div>' . $_POST['font_name'] . '</div>';
// RIGHT - sanitize input, escape output
$font_name = sanitize_text_field($_POST['font_name']);
echo '<div>' . esc_html($font_name) . '</div>';
```
### A8 - Insecure Deserialization
```php
// NEVER use unserialize() on external data
$data = unserialize($_POST['data']); // DANGEROUS
// Use JSON instead
$data = json_decode(sanitize_text_field($_POST['data']), true);
if (json_last_error() !== JSON_ERROR_NONE) {
wp_send_json_error(['message' => 'Invalid data format']);
}
```
### A9 - Vulnerable Components
- No external PHP libraries
- Use only WordPress core functions
- Keep dependencies to zero
---
## Nonce Implementation
### Creating Nonces
**In admin page form:**
```php
wp_nonce_field('mlf_download_font', 'mlf_nonce');
```
**For AJAX (via wp_localize_script):**
```php
wp_localize_script('mlf-admin', 'mlfData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('mlf_download_font'),
]);
```
### Verifying Nonces
**AJAX handler:**
```php
// Returns false on failure, doesn't die (we handle response ourselves)
if (!check_ajax_referer('mlf_download_font', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
```
**Form submission:**
```php
if (!wp_verify_nonce($_POST['mlf_nonce'], 'mlf_download_font')) {
wp_die('Security check failed');
}
```
### Nonce Names
Use consistent, descriptive nonce action names:
| Action | Nonce Name |
|--------|------------|
| Download font | `mlf_download_font` |
| Delete font | `mlf_delete_font` |
| Update settings | `mlf_update_settings` |
---
## Input Validation
### Font Name Validation
```php
$font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : '';
// Strict allowlist pattern - alphanumeric, spaces, hyphens only
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
wp_send_json_error(['message' => 'Invalid font name: only letters, numbers, spaces, and hyphens allowed']);
}
// Length limit
if (strlen($font_name) > 100) {
wp_send_json_error(['message' => 'Font name too long']);
}
// Not empty
if (empty($font_name)) {
wp_send_json_error(['message' => 'Font name required']);
}
```
### Weight Validation
```php
$weights = isset($_POST['weights']) ? (array) $_POST['weights'] : [];
// Convert to integers
$weights = array_map('absint', $weights);
// Strict allowlist
$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
$weights = array_intersect($weights, $allowed_weights);
// Must have at least one
if (empty($weights)) {
wp_send_json_error(['message' => 'At least one weight required']);
}
```
### Style Validation
```php
$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : [];
// Strict allowlist - only these two values ever
$allowed_styles = ['normal', 'italic'];
$styles = array_filter($styles, function($style) use ($allowed_styles) {
return in_array($style, $allowed_styles, true);
});
// Must have at least one
if (empty($styles)) {
wp_send_json_error(['message' => 'At least one style required']);
}
```
### Font Family ID Validation (for delete)
```php
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
if ($font_id < 1) {
wp_send_json_error(['message' => 'Invalid font ID']);
}
// Verify it exists and is a font family
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => 'Font not found']);
}
// Verify it's one we imported (not a theme font)
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => 'Cannot delete theme fonts']);
}
```
---
## File Operation Security
### Path Traversal Prevention
```php
/**
* Validate that a path is within the WordPress fonts directory.
* Prevents path traversal attacks.
*
* @param string $path Full path to validate
* @return bool True if path is safe, false otherwise
*/
function mlf_validate_font_path($path) {
$font_dir = wp_get_font_dir();
$fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));
// Resolve to real path (handles ../ etc)
$real_path = realpath($path);
// If realpath fails, file doesn't exist yet - validate the directory
if ($real_path === false) {
$dir = dirname($path);
$real_dir = realpath($dir);
if ($real_dir === false) {
return false;
}
$real_path = wp_normalize_path($real_dir . '/' . basename($path));
} else {
$real_path = wp_normalize_path($real_path);
}
// Must be within fonts directory
return strpos($real_path, $fonts_path) === 0;
}
```
### Filename Sanitization
```php
/**
* Sanitize and validate a font filename.
*
* @param string $filename The filename to validate
* @return string|false Sanitized filename or false if invalid
*/
function mlf_sanitize_font_filename($filename) {
// WordPress sanitization first
$filename = sanitize_file_name($filename);
// Must have .woff2 extension
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') {
return false;
}
// No path components
if ($filename !== basename($filename)) {
return false;
}
// Reasonable length
if (strlen($filename) > 200) {
return false;
}
return $filename;
}
```
### Safe File Writing
```php
/**
* Safely write a font file to the fonts directory.
*
* @param string $filename Sanitized filename
* @param string $content File content
* @return string|WP_Error File path on success, WP_Error on failure
*/
function mlf_write_font_file($filename, $content) {
// Validate filename
$safe_filename = mlf_sanitize_font_filename($filename);
if ($safe_filename === false) {
return new WP_Error('invalid_filename', 'Invalid filename');
}
// Get fonts directory
$font_dir = wp_get_font_dir();
$destination = trailingslashit($font_dir['path']) . $safe_filename;
// Validate path
if (!mlf_validate_font_path($destination)) {
return new WP_Error('invalid_path', 'Invalid file path');
}
// Ensure directory exists
if (!wp_mkdir_p($font_dir['path'])) {
return new WP_Error('mkdir_failed', 'Could not create fonts directory');
}
// Write file
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) {
return new WP_Error('write_failed', 'Could not write font file');
}
return $destination;
}
```
### Safe File Deletion
```php
/**
* Safely delete a font file.
*
* @param string $path Full path to the file
* @return bool True on success, false on failure
*/
function mlf_delete_font_file($path) {
// Validate path is within fonts directory
if (!mlf_validate_font_path($path)) {
return false;
}
// Must be a .woff2 file
if (pathinfo($path, PATHINFO_EXTENSION) !== 'woff2') {
return false;
}
// File must exist
if (!file_exists($path)) {
return true; // Already gone, that's fine
}
return wp_delete_file($path);
}
```
---
## HTTP Request Security
### Outbound Requests (Google Fonts)
```php
$response = wp_remote_get($url, [
'timeout' => 15,
'sslverify' => true, // Always verify SSL
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
]);
// Check for errors
if (is_wp_error($response)) {
// Log error, return gracefully
error_log('MLF: Google Fonts request failed - ' . $response->get_error_message());
return new WP_Error('request_failed', 'Could not connect to Google Fonts');
}
// Check HTTP status
$status = wp_remote_retrieve_response_code($response);
if ($status !== 200) {
return new WP_Error('http_error', 'Google Fonts returned status ' . $status);
}
// Get body
$body = wp_remote_retrieve_body($response);
if (empty($body)) {
return new WP_Error('empty_response', 'Empty response from Google Fonts');
}
```
### URL Validation (Google Fonts only)
```php
/**
* Validate that a URL is a legitimate Google Fonts URL.
*
* @param string $url URL to validate
* @return bool True if valid Google Fonts URL
*/
function mlf_is_valid_google_fonts_url($url) {
$parsed = wp_parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
// Only allow Google Fonts domains
$allowed_hosts = [
'fonts.googleapis.com',
'fonts.gstatic.com',
];
return in_array($parsed['host'], $allowed_hosts, true);
}
```
---
## AJAX Handler Complete Template
```php
<?php
if (!defined('ABSPATH')) {
exit;
}
class MLF_Ajax_Handler {
public function __construct() {
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
// NEVER add wp_ajax_nopriv_ - admin only functionality
}
/**
* Handle font download AJAX request.
*/
public function handle_download() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_download_font', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
// 2. CAPABILITY CHECK
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => 'Unauthorized'], 403);
}
// 3. INPUT VALIDATION
$font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : '';
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name) || strlen($font_name) > 100) {
wp_send_json_error(['message' => 'Invalid font name']);
}
$weights = isset($_POST['weights']) ? array_map('absint', (array) $_POST['weights']) : [];
$weights = array_intersect($weights, [100, 200, 300, 400, 500, 600, 700, 800, 900]);
if (empty($weights)) {
wp_send_json_error(['message' => 'At least one weight required']);
}
$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : [];
$styles = array_intersect($styles, ['normal', 'italic']);
if (empty($styles)) {
wp_send_json_error(['message' => 'At least one style required']);
}
// 4. PROCESS REQUEST
try {
$downloader = new MLF_Font_Downloader();
$result = $downloader->download($font_name, $weights, $styles);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success([
'message' => sprintf('Successfully installed %s', esc_html($font_name)),
'font_id' => $result,
]);
} catch (Exception $e) {
error_log('MLF Download Error: ' . $e->getMessage());
wp_send_json_error(['message' => 'An unexpected error occurred']);
}
}
/**
* Handle font deletion AJAX request.
*/
public function handle_delete() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_delete_font', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
// 2. CAPABILITY CHECK
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => 'Unauthorized'], 403);
}
// 3. INPUT VALIDATION
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
if ($font_id < 1) {
wp_send_json_error(['message' => 'Invalid font ID']);
}
// Verify font exists and is ours
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => 'Font not found']);
}
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => 'Cannot delete theme fonts']);
}
// 4. PROCESS REQUEST
try {
$registry = new MLF_Font_Registry();
$result = $registry->delete_font($font_id);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success(['message' => 'Font deleted successfully']);
} catch (Exception $e) {
error_log('MLF Delete Error: ' . $e->getMessage());
wp_send_json_error(['message' => 'An unexpected error occurred']);
}
}
}
```
---
## Security Checklist
Before committing any code:
- [ ] ABSPATH check at top of every PHP file
- [ ] index.php exists in every directory
- [ ] All AJAX handlers verify nonce first
- [ ] All AJAX handlers check capability second
- [ ] All user input sanitized with appropriate function
- [ ] All user input validated against allowlists where applicable
- [ ] All output escaped with appropriate function
- [ ] File paths validated to prevent traversal
- [ ] No direct SQL queries (use WordPress APIs)
- [ ] No `unserialize()` on user input
- [ ] No `eval()` or similar dynamic execution
- [ ] External URLs validated before use
- [ ] Error messages don't expose sensitive info

View file

@ -1,560 +0,0 @@
# WORDPRESS_COMPATIBILITY.md — WordPress & Plugin Compatibility
## Overview
This document covers compatibility requirements for WordPress core systems and popular plugins. Reference this when building the font registry class and integration points.
---
## WordPress Version Requirements
**Minimum: WordPress 6.5**
WordPress 6.5 introduced the Font Library API which this plugin depends on. Earlier versions will not work.
```php
// Check on activation
register_activation_hook(__FILE__, function() {
if (version_compare(get_bloginfo('version'), '6.5', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die(
'Maple Local Fonts requires WordPress 6.5 or higher for Font Library support.',
'Plugin Activation Error',
['back_link' => true]
);
}
});
// Also check on admin init (in case WP was downgraded)
add_action('admin_init', function() {
if (version_compare(get_bloginfo('version'), '6.5', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
add_action('admin_notices', function() {
echo '<div class="error"><p>Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.</p></div>';
});
}
});
```
---
## WordPress Font Library API
### How It Works
WordPress 6.5+ stores fonts using custom post types:
| Post Type | Purpose |
|-----------|---------|
| `wp_font_family` | Font family (e.g., "Open Sans") |
| `wp_font_face` | Individual weight/style variant (child of family) |
Fonts are stored in `wp-content/fonts/` by default.
### Getting the Fonts Directory
```php
// ALWAYS use this function, never hardcode paths
$font_dir = wp_get_font_dir();
// Returns:
[
'path' => '/var/www/html/wp-content/fonts',
'url' => 'https://example.com/wp-content/fonts',
'subdir' => '',
'basedir' => '/var/www/html/wp-content/fonts',
'baseurl' => 'https://example.com/wp-content/fonts',
]
```
### Registering a Font Family
```php
/**
* Register a font family with WordPress Font Library.
*
* @param string $font_name Display name (e.g., "Open Sans")
* @param string $font_slug Slug (e.g., "open-sans")
* @param array $files Array of downloaded file data
* @return int|WP_Error Font family post ID or error
*/
function mlf_register_font_family($font_name, $font_slug, $files) {
// Check if font already exists
$existing = get_posts([
'post_type' => 'wp_font_family',
'name' => $font_slug,
'posts_per_page' => 1,
'post_status' => 'publish',
]);
if (!empty($existing)) {
return new WP_Error('font_exists', 'Font family already installed');
}
// Build font family settings
$font_family_settings = [
'name' => $font_name,
'slug' => $font_slug,
'fontFamily' => sprintf('"%s", sans-serif', $font_name),
'fontFace' => [],
];
// Add each font face
foreach ($files as $file) {
$font_dir = wp_get_font_dir();
$relative_path = str_replace($font_dir['path'], '', $file['path']);
$font_family_settings['fontFace'][] = [
'fontFamily' => $font_name,
'fontWeight' => $file['weight'],
'fontStyle' => $file['style'],
'src' => 'file:.' . $font_dir['basedir'] . $relative_path,
];
}
// Create font family post
$family_id = wp_insert_post([
'post_type' => 'wp_font_family',
'post_title' => $font_name,
'post_name' => $font_slug,
'post_status' => 'publish',
'post_content' => wp_json_encode($font_family_settings),
]);
if (is_wp_error($family_id)) {
return $family_id;
}
// Mark as imported by our plugin (for identification)
update_post_meta($family_id, '_mlf_imported', '1');
update_post_meta($family_id, '_mlf_import_date', current_time('mysql'));
// Create font face posts (children)
foreach ($files as $file) {
$font_dir = wp_get_font_dir();
$face_settings = [
'fontFamily' => $font_name,
'fontWeight' => $file['weight'],
'fontStyle' => $file['style'],
'src' => 'file:.' . $font_dir['baseurl'] . '/' . basename($file['path']),
];
wp_insert_post([
'post_type' => 'wp_font_face',
'post_parent' => $family_id,
'post_status' => 'publish',
'post_content' => wp_json_encode($face_settings),
]);
}
// Clear font caches
delete_transient('wp_font_library_fonts');
return $family_id;
}
```
### Deleting a Font Family
```php
/**
* Delete a font family and its files.
*
* @param int $family_id Font family post ID
* @return bool|WP_Error True on success, error on failure
*/
function mlf_delete_font_family($family_id) {
$family = get_post($family_id);
if (!$family || $family->post_type !== 'wp_font_family') {
return new WP_Error('not_found', 'Font family not found');
}
// Verify it's one we imported
if (get_post_meta($family_id, '_mlf_imported', true) !== '1') {
return new WP_Error('not_ours', 'Cannot delete fonts not imported by this plugin');
}
// Get font faces
$faces = get_children([
'post_parent' => $family_id,
'post_type' => 'wp_font_face',
]);
$font_dir = wp_get_font_dir();
// Delete font face files and posts
foreach ($faces as $face) {
$settings = json_decode($face->post_content, true);
if (isset($settings['src'])) {
// Convert file:. URL to path
$src = $settings['src'];
$src = str_replace('file:.', '', $src);
// Handle both URL and path formats
if (strpos($src, $font_dir['baseurl']) !== false) {
$file_path = str_replace($font_dir['baseurl'], $font_dir['path'], $src);
} else {
$file_path = $font_dir['path'] . '/' . basename($src);
}
// Validate path before deletion
if (mlf_validate_font_path($file_path) && file_exists($file_path)) {
wp_delete_file($file_path);
}
}
wp_delete_post($face->ID, true);
}
// Delete family post
wp_delete_post($family_id, true);
// Clear caches
delete_transient('wp_font_library_fonts');
return true;
}
```
### Listing Installed Fonts
```php
/**
* Get all fonts imported by this plugin.
*
* @return array Array of font data
*/
function mlf_get_imported_fonts() {
$fonts = get_posts([
'post_type' => 'wp_font_family',
'posts_per_page' => 100,
'post_status' => 'publish',
'meta_key' => '_mlf_imported',
'meta_value' => '1',
]);
$result = [];
foreach ($fonts as $font) {
$settings = json_decode($font->post_content, true);
// Get variants
$faces = get_children([
'post_parent' => $font->ID,
'post_type' => 'wp_font_face',
]);
$variants = [];
foreach ($faces as $face) {
$face_settings = json_decode($face->post_content, true);
$variants[] = [
'weight' => $face_settings['fontWeight'] ?? '400',
'style' => $face_settings['fontStyle'] ?? 'normal',
];
}
$result[] = [
'id' => $font->ID,
'name' => $settings['name'] ?? $font->post_title,
'slug' => $settings['slug'] ?? $font->post_name,
'variants' => $variants,
'import_date' => get_post_meta($font->ID, '_mlf_import_date', true),
];
}
return $result;
}
```
---
## Gutenberg FSE Integration
### How Fonts Appear in the Editor
Once registered via the Font Library API, fonts automatically appear in:
1. **Global Styles** → Typography → Font dropdown
2. **Block settings** → Typography → Font dropdown (when per-block typography is enabled)
No additional integration code is needed — WordPress handles this automatically.
### Theme.json Compatibility
**DO NOT:**
- Directly modify theme.json
- Filter `wp_theme_json_data_theme` to inject fonts (let Font Library handle it)
- Override global styles CSS directly
**DO:**
- Use the Font Library API (post types)
- Let WordPress generate CSS custom properties
- Trust the system
### CSS Custom Properties
When a font is applied in Global Styles, WordPress generates:
```css
body {
--wp--preset--font-family--open-sans: "Open Sans", sans-serif;
}
```
And applies it:
```css
body {
font-family: var(--wp--preset--font-family--open-sans);
}
```
Our plugin doesn't need to touch this — it's automatic.
---
## WooCommerce Compatibility
### HPOS (High-Performance Order Storage)
WooCommerce's HPOS moves order data from post meta to custom tables. We must declare compatibility.
```php
// Declare HPOS compatibility
add_action('before_woocommerce_init', function() {
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
});
```
### Why We're Compatible
Our plugin:
- Does NOT interact with orders at all
- Does NOT query wp_posts for order data
- Does NOT use wp_postmeta for order data
- Only uses wp_font_family and wp_font_face post types
We're inherently compatible because we don't touch WooCommerce data.
### Frontend Considerations
**DO NOT:**
- Override `.woocommerce` class styles
- Override `.wc-block-*` styles
- Target cart/checkout elements specifically
**DO:**
- Let WooCommerce elements inherit from body/heading fonts
- Let global styles cascade naturally
WooCommerce product titles, descriptions, and other text will naturally inherit the fonts set via Global Styles. No special handling needed.
---
## Wordfence Compatibility
### Potential Concerns
1. **Outbound requests** to Google Fonts during import
2. **AJAX endpoints** for admin actions
3. **File operations** in wp-content
### Why We're Compatible
**Outbound Requests:**
- Only occur during admin import (user-initiated action)
- Target well-known domains (fonts.googleapis.com, fonts.gstatic.com)
- Use standard `wp_remote_get()` which Wordfence allows
- No runtime external requests on frontend
**AJAX Endpoints:**
- Use standard `admin-ajax.php` (not custom endpoints)
- Include proper nonces
- Follow WordPress patterns that Wordfence expects
**File Operations:**
- Write only to `wp-content/fonts/` (WordPress default directory)
- Use WordPress Filesystem API
- Don't create executable files
### Testing with Wordfence
Test these scenarios with Wordfence active:
- [ ] Learning Mode: Import should succeed
- [ ] Enabled Mode: Import should succeed
- [ ] Rate Limiting: Admin AJAX not blocked
- [ ] Firewall: No false positives on font download
---
## LearnDash Compatibility
### Overview
LearnDash is a WordPress LMS that uses:
- Custom post types (courses, lessons, topics, quizzes)
- Custom templates
- Focus Mode (distraction-free learning)
### Why We're Compatible
Our plugin:
- Doesn't touch LearnDash post types
- Doesn't modify LearnDash templates
- Doesn't inject CSS on frontend
- Lets Global Styles cascade to LearnDash content
LearnDash course content, lesson text, and quiz questions will inherit the fonts set in Global Styles automatically.
### Focus Mode Consideration
LearnDash Focus Mode uses its own template. Fonts set via Global Styles will apply because:
- Focus Mode still loads theme.json styles
- CSS custom properties cascade to all content
- No special handling needed
**DO NOT:**
- Target `.learndash-*` classes specifically
- Override Focus Mode styles
- Inject custom CSS for LearnDash
---
## WPForms Compatibility
### Overview
WPForms renders forms via shortcodes and blocks. Form styling is handled by WPForms.
### Why We're Compatible
- Form labels and text inherit from body font
- We don't override `.wpforms-*` classes
- No JavaScript conflicts (we have no frontend JS)
### Consideration
If a user wants form text in a different font, they should use WPForms' built-in styling options or custom CSS — not expect our plugin to handle it.
---
## General Best Practices
### What We Hook Into
```php
// Admin menu
add_action('admin_menu', [$this, 'register_menu']);
// Admin assets (only on our page)
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
// AJAX handlers
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
// WooCommerce HPOS compatibility
add_action('before_woocommerce_init', [$this, 'declare_hpos_compatibility']);
```
### What We DON'T Hook Into
```php
// NO frontend hooks
// add_action('wp_enqueue_scripts', ...); // DON'T DO THIS
// add_action('wp_head', ...); // DON'T DO THIS
// add_action('wp_footer', ...); // DON'T DO THIS
// NO theme modification hooks
// add_filter('wp_theme_json_data_theme', ...); // Let Font Library handle it
// NO WooCommerce hooks
// add_action('woocommerce_*', ...); // DON'T DO THIS
// NO content filters
// add_filter('the_content', ...); // DON'T DO THIS
```
---
## Conflict Debugging
If a user reports a conflict, check:
### 1. Plugin Load Order
Our plugin should load with default priority. Check if another plugin is:
- Modifying the Font Library
- Overriding font CSS
- Filtering theme.json
### 2. CSS Specificity
If fonts aren't applying:
- Check browser DevTools for CSS cascade
- Look for more specific selectors overriding global styles
- Check for `!important` declarations
### 3. Cache Issues
Font changes not appearing:
- Clear browser cache
- Clear any caching plugins (WP Rocket, W3TC, etc.)
- Clear CDN cache if applicable
- WordPress transients: `delete_transient('wp_font_library_fonts')`
### 4. JavaScript Errors
If admin page isn't working:
- Check browser console for JS errors
- Look for conflicts with other admin scripts
- Verify jQuery isn't being dequeued
---
## Compatibility Checklist
Before releasing:
### WordPress Core
- [ ] Works on WordPress 6.5
- [ ] Works on WordPress 6.6+
- [ ] Font Library API integration works
- [ ] Fonts appear in Global Styles
- [ ] Fonts apply correctly on frontend
### WooCommerce
- [ ] HPOS compatibility declared
- [ ] No errors in WooCommerce status page
- [ ] Product pages render correctly with custom fonts
- [ ] Cart/Checkout not affected
### Wordfence
- [ ] Import works with firewall enabled
- [ ] No blocked requests
- [ ] No false positive security alerts
### LearnDash
- [ ] Course content inherits fonts
- [ ] Focus Mode renders correctly
- [ ] No JavaScript conflicts
### WPForms
- [ ] Forms render correctly
- [ ] No styling conflicts
### Other
- [ ] No PHP errors in debug.log
- [ ] No JavaScript errors in console
- [ ] Admin page loads correctly
- [ ] No memory issues during import

View file

@ -1,294 +0,0 @@
/**
* Maple Icons - Admin Styles
*
* @package MapleIcons
*/
/* Admin page wrapper */
.mi-admin-wrap {
max-width: 1200px;
}
.mi-admin-intro {
background: #fff;
border: 1px solid #c3c4c7;
border-left: 4px solid #2271b1;
padding: 12px 16px;
margin: 20px 0;
}
.mi-admin-intro p {
margin: 0;
font-size: 14px;
}
/* Icon sets grid */
.mi-sets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin: 20px 0;
}
/* Individual set card */
.mi-set-card {
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
padding: 20px;
position: relative;
transition: border-color 0.2s, box-shadow 0.2s;
}
.mi-set-card:hover {
border-color: #2271b1;
}
.mi-set-card.mi-set-active {
border-color: #00a32a;
box-shadow: 0 0 0 1px #00a32a;
}
.mi-set-card.mi-set-downloading {
opacity: 0.8;
pointer-events: none;
}
/* Card header */
.mi-set-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.mi-set-name {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1d2327;
}
/* Badges */
.mi-badge {
display: inline-block;
padding: 2px 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
border-radius: 3px;
}
.mi-badge-active {
background: #00a32a;
color: #fff;
}
.mi-badge-downloaded {
background: #dcdcde;
color: #50575e;
}
/* Set metadata */
.mi-set-meta {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
font-size: 13px;
color: #50575e;
}
.mi-set-count {
font-weight: 600;
color: #1d2327;
}
/* External link */
.mi-set-link {
display: inline-block;
margin-bottom: 16px;
font-size: 13px;
text-decoration: none;
}
.mi-set-link:hover {
text-decoration: underline;
}
/* Action buttons */
.mi-set-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.mi-set-actions .button {
flex: 1;
text-align: center;
}
.mi-delete-btn {
color: #d63638 !important;
border-color: #d63638 !important;
}
.mi-delete-btn:hover {
background: #d63638 !important;
color: #fff !important;
}
/* Progress bar */
.mi-set-progress {
margin-top: 16px;
}
.mi-progress-bar {
height: 8px;
background: #dcdcde;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.mi-progress-fill {
height: 100%;
background: #2271b1;
border-radius: 4px;
width: 0;
transition: width 0.3s ease;
}
.mi-progress-text {
font-size: 12px;
color: #50575e;
}
/* Messages */
.mi-set-message {
margin-top: 12px;
padding: 8px 12px;
border-radius: 4px;
font-size: 13px;
}
.mi-set-message.mi-message-success {
background: #edfaef;
border: 1px solid #00a32a;
color: #00450c;
}
.mi-set-message.mi-message-error {
background: #fcf0f1;
border: 1px solid #d63638;
color: #8a1f22;
}
.mi-set-message.mi-message-info {
background: #f0f6fc;
border: 1px solid #2271b1;
color: #135e96;
}
/* Usage section */
.mi-admin-usage,
.mi-admin-info {
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
padding: 20px;
margin: 20px 0;
}
.mi-admin-usage h2,
.mi-admin-info h2 {
margin-top: 0;
font-size: 16px;
border-bottom: 1px solid #c3c4c7;
padding-bottom: 10px;
}
.mi-admin-usage ol {
margin: 16px 0 0;
padding-left: 24px;
}
.mi-admin-usage li {
margin-bottom: 8px;
font-size: 14px;
}
.mi-admin-info p {
font-size: 14px;
margin-bottom: 12px;
}
.mi-admin-info p:last-child {
margin-bottom: 0;
}
.mi-admin-info code {
background: #f0f0f1;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
/* Loading spinner */
.mi-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #c3c4c7;
border-top-color: #2271b1;
border-radius: 50%;
animation: mi-spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes mi-spin {
to {
transform: rotate(360deg);
}
}
/* Button loading state */
.button.mi-loading {
position: relative;
color: transparent !important;
}
.button.mi-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 14px;
height: 14px;
margin: -7px 0 0 -7px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: mi-spin 0.8s linear infinite;
}
.button-primary.mi-loading::after {
border-color: rgba(255, 255, 255, 0.3);
border-top-color: #fff;
}
/* Responsive adjustments */
@media screen and (max-width: 782px) {
.mi-sets-grid {
grid-template-columns: 1fr;
}
.mi-set-actions {
flex-direction: column;
}
.mi-set-actions .button {
width: 100%;
}
}

View file

@ -1,255 +0,0 @@
/**
* Maple Icons - Admin JavaScript
*
* @package MapleIcons
*/
(function($) {
'use strict';
/**
* Show message on a card.
*
* @param {jQuery} $card The card element.
* @param {string} message The message text.
* @param {string} type Message type: success, error, info.
*/
function showMessage($card, message, type) {
var $message = $card.find('.mi-set-message');
$message
.removeClass('mi-message-success mi-message-error mi-message-info')
.addClass('mi-message-' + type)
.text(message)
.show();
// Auto-hide success messages after 5 seconds.
if (type === 'success') {
setTimeout(function() {
$message.fadeOut();
}, 5000);
}
}
/**
* Hide message on a card.
*
* @param {jQuery} $card The card element.
*/
function hideMessage($card) {
$card.find('.mi-set-message').hide();
}
/**
* Show progress bar.
*
* @param {jQuery} $card The card element.
*/
function showProgress($card) {
$card.find('.mi-set-progress').show();
$card.find('.mi-set-actions').hide();
}
/**
* Hide progress bar.
*
* @param {jQuery} $card The card element.
*/
function hideProgress($card) {
$card.find('.mi-set-progress').hide();
$card.find('.mi-set-actions').show();
}
/**
* Update progress bar.
*
* @param {jQuery} $card The card element.
* @param {number} percentage Progress percentage (0-100).
* @param {string} text Progress text.
*/
function updateProgress($card, percentage, text) {
$card.find('.mi-progress-fill').css('width', percentage + '%');
$card.find('.mi-progress-text').text(text);
}
/**
* Poll download progress.
*
* @param {string} slug Icon set slug.
* @param {jQuery} $card The card element.
*/
function pollProgress(slug, $card) {
$.ajax({
url: miAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'mi_get_progress',
nonce: miAdmin.nonce,
slug: slug
},
success: function(response) {
if (response.success && response.data.downloading) {
var percentage = response.data.percentage || 0;
var text = response.data.completed + ' / ' + response.data.total + ' icons';
updateProgress($card, percentage, text);
// Continue polling if still downloading.
setTimeout(function() {
pollProgress(slug, $card);
}, 1000);
}
}
});
}
/**
* Handle download button click.
*/
$(document).on('click', '.mi-download-btn', function(e) {
e.preventDefault();
var $btn = $(this);
var $card = $btn.closest('.mi-set-card');
var slug = $btn.data('slug');
if ($btn.hasClass('mi-loading')) {
return;
}
$btn.addClass('mi-loading');
$card.addClass('mi-set-downloading');
hideMessage($card);
showProgress($card);
updateProgress($card, 0, miAdmin.strings.downloading);
// Start polling for progress.
setTimeout(function() {
pollProgress(slug, $card);
}, 500);
$.ajax({
url: miAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'mi_download_set',
nonce: miAdmin.nonce,
slug: slug
},
success: function(response) {
hideProgress($card);
$btn.removeClass('mi-loading');
$card.removeClass('mi-set-downloading');
if (response.success) {
showMessage($card, response.data.message || miAdmin.strings.downloadSuccess, 'success');
// Reload page to update UI state.
setTimeout(function() {
location.reload();
}, 1500);
} else {
showMessage($card, response.data.message || miAdmin.strings.downloadError, 'error');
}
},
error: function() {
hideProgress($card);
$btn.removeClass('mi-loading');
$card.removeClass('mi-set-downloading');
showMessage($card, miAdmin.strings.downloadError, 'error');
}
});
});
/**
* Handle delete button click.
*/
$(document).on('click', '.mi-delete-btn', function(e) {
e.preventDefault();
var $btn = $(this);
var $card = $btn.closest('.mi-set-card');
var slug = $btn.data('slug');
if ($btn.hasClass('mi-loading')) {
return;
}
if (!confirm(miAdmin.strings.confirmDelete)) {
return;
}
$btn.addClass('mi-loading');
hideMessage($card);
$.ajax({
url: miAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'mi_delete_set',
nonce: miAdmin.nonce,
slug: slug
},
success: function(response) {
$btn.removeClass('mi-loading');
if (response.success) {
showMessage($card, response.data.message || miAdmin.strings.deleteSuccess, 'success');
// Reload page to update UI state.
setTimeout(function() {
location.reload();
}, 1500);
} else {
showMessage($card, response.data.message || miAdmin.strings.deleteError, 'error');
}
},
error: function() {
$btn.removeClass('mi-loading');
showMessage($card, miAdmin.strings.deleteError, 'error');
}
});
});
/**
* Handle activate button click.
*/
$(document).on('click', '.mi-activate-btn, .mi-deactivate-btn', function(e) {
e.preventDefault();
var $btn = $(this);
var $card = $btn.closest('.mi-set-card');
var slug = $btn.data('slug');
if ($btn.hasClass('mi-loading')) {
return;
}
$btn.addClass('mi-loading');
hideMessage($card);
$.ajax({
url: miAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'mi_set_active',
nonce: miAdmin.nonce,
slug: slug
},
success: function(response) {
$btn.removeClass('mi-loading');
if (response.success) {
showMessage($card, response.data.message || miAdmin.strings.activateSuccess, 'success');
// Reload page to update UI state.
setTimeout(function() {
location.reload();
}, 1000);
} else {
showMessage($card, response.data.message || miAdmin.strings.activateError, 'error');
}
},
error: function() {
$btn.removeClass('mi-loading');
showMessage($card, miAdmin.strings.activateError, 'error');
}
});
});
})(jQuery);

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

View file

@ -1,224 +0,0 @@
<?php
/**
* Admin Page class - Handles the settings page.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Admin_Page
*
* Handles the plugin settings page in WordPress admin.
*/
class MI_Admin_Page {
/**
* Constructor - Register admin hooks.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
/**
* Register the admin menu item.
*/
public function register_menu() {
add_options_page(
__( 'Maple Icons', 'maple-icons' ),
__( 'Maple Icons', 'maple-icons' ),
'manage_options',
'maple-icons',
array( $this, 'render_page' )
);
}
/**
* Enqueue admin assets.
*
* @param string $hook_suffix The current admin page.
*/
public function enqueue_assets( $hook_suffix ) {
if ( 'settings_page_maple-icons' !== $hook_suffix ) {
return;
}
wp_enqueue_style(
'mi-admin',
MI_PLUGIN_URL . 'assets/admin.css',
array(),
MI_VERSION
);
wp_enqueue_script(
'mi-admin',
MI_PLUGIN_URL . 'assets/admin.js',
array( 'jquery' ),
MI_VERSION,
true
);
wp_localize_script(
'mi-admin',
'miAdmin',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mi_admin_nonce' ),
'strings' => array(
'downloading' => __( 'Downloading...', 'maple-icons' ),
'deleting' => __( 'Deleting...', 'maple-icons' ),
'activating' => __( 'Activating...', 'maple-icons' ),
'confirmDelete' => __( 'Are you sure you want to delete this icon set?', 'maple-icons' ),
'downloadError' => __( 'Download failed. Please try again.', 'maple-icons' ),
'deleteError' => __( 'Delete failed. Please try again.', 'maple-icons' ),
'activateError' => __( 'Activation failed. Please try again.', 'maple-icons' ),
'downloadSuccess'=> __( 'Icon set downloaded successfully!', 'maple-icons' ),
'deleteSuccess' => __( 'Icon set deleted successfully!', 'maple-icons' ),
'activateSuccess'=> __( 'Icon set activated!', 'maple-icons' ),
),
)
);
}
/**
* Render the settings page.
*/
public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$registry = MI_Icon_Registry::get_instance();
$all_sets = MI_Icon_Sets::get_all();
$downloaded = $registry->get_downloaded_sets();
$active_set = $registry->get_active_set();
?>
<div class="wrap mi-admin-wrap">
<h1><?php esc_html_e( 'Maple Icons', 'maple-icons' ); ?></h1>
<div class="mi-admin-intro">
<p><?php esc_html_e( 'Download icon sets from CDN and use them in the Gutenberg block editor. Only one icon set can be active at a time.', 'maple-icons' ); ?></p>
</div>
<div class="mi-icon-sets">
<h2><?php esc_html_e( 'Available Icon Sets', 'maple-icons' ); ?></h2>
<div class="mi-sets-grid">
<?php foreach ( $all_sets as $slug => $set ) : ?>
<?php
$is_downloaded = isset( $downloaded[ $slug ] );
$is_active = $active_set === $slug;
$icon_count = $is_downloaded ? $downloaded[ $slug ]['icon_count'] : 0;
$download_date = $is_downloaded ? $downloaded[ $slug ]['downloaded_at'] : '';
?>
<div class="mi-set-card <?php echo $is_active ? 'mi-set-active' : ''; ?> <?php echo $is_downloaded ? 'mi-set-downloaded' : ''; ?>" data-slug="<?php echo esc_attr( $slug ); ?>">
<div class="mi-set-header">
<h3 class="mi-set-name"><?php echo esc_html( $set['name'] ); ?></h3>
<?php if ( $is_active ) : ?>
<span class="mi-badge mi-badge-active"><?php esc_html_e( 'Active', 'maple-icons' ); ?></span>
<?php elseif ( $is_downloaded ) : ?>
<span class="mi-badge mi-badge-downloaded"><?php esc_html_e( 'Downloaded', 'maple-icons' ); ?></span>
<?php endif; ?>
</div>
<div class="mi-set-meta">
<span class="mi-set-license">
<?php
/* translators: %s: License name */
printf( esc_html__( 'License: %s', 'maple-icons' ), esc_html( $set['license'] ) );
?>
</span>
<span class="mi-set-styles">
<?php
/* translators: %s: Style names */
printf( esc_html__( 'Styles: %s', 'maple-icons' ), esc_html( implode( ', ', $set['styles'] ) ) );
?>
</span>
<?php if ( $is_downloaded && $icon_count > 0 ) : ?>
<span class="mi-set-count">
<?php
/* translators: %d: Number of icons */
printf( esc_html( _n( '%d icon', '%d icons', $icon_count, 'maple-icons' ) ), intval( $icon_count ) );
?>
</span>
<?php endif; ?>
</div>
<?php if ( ! empty( $set['url'] ) ) : ?>
<a href="<?php echo esc_url( $set['url'] ); ?>" class="mi-set-link" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'View on website', 'maple-icons' ); ?> &rarr;
</a>
<?php endif; ?>
<div class="mi-set-actions">
<?php if ( ! $is_downloaded ) : ?>
<button type="button" class="button button-primary mi-download-btn" data-slug="<?php echo esc_attr( $slug ); ?>">
<?php esc_html_e( 'Download', 'maple-icons' ); ?>
</button>
<?php else : ?>
<?php if ( ! $is_active ) : ?>
<button type="button" class="button button-primary mi-activate-btn" data-slug="<?php echo esc_attr( $slug ); ?>">
<?php esc_html_e( 'Set Active', 'maple-icons' ); ?>
</button>
<?php else : ?>
<button type="button" class="button mi-deactivate-btn" data-slug="">
<?php esc_html_e( 'Deactivate', 'maple-icons' ); ?>
</button>
<?php endif; ?>
<button type="button" class="button mi-delete-btn" data-slug="<?php echo esc_attr( $slug ); ?>">
<?php esc_html_e( 'Delete', 'maple-icons' ); ?>
</button>
<?php endif; ?>
</div>
<div class="mi-set-progress" style="display: none;">
<div class="mi-progress-bar">
<div class="mi-progress-fill"></div>
</div>
<span class="mi-progress-text"></span>
</div>
<div class="mi-set-message" style="display: none;"></div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="mi-admin-usage">
<h2><?php esc_html_e( 'How to Use', 'maple-icons' ); ?></h2>
<ol>
<li><?php esc_html_e( 'Download one or more icon sets above.', 'maple-icons' ); ?></li>
<li><?php esc_html_e( 'Set one icon set as active.', 'maple-icons' ); ?></li>
<li><?php esc_html_e( 'In the Gutenberg editor, add a "Maple Icon" block.', 'maple-icons' ); ?></li>
<li><?php esc_html_e( 'Search and select an icon from your active set.', 'maple-icons' ); ?></li>
<li><?php esc_html_e( 'Customize size, color, and other settings in the block sidebar.', 'maple-icons' ); ?></li>
</ol>
</div>
<div class="mi-admin-info">
<h2><?php esc_html_e( 'About', 'maple-icons' ); ?></h2>
<p>
<?php esc_html_e( 'Maple Icons downloads SVG icons from CDN and stores them locally in your WordPress installation. Icons are sanitized for security and normalized for consistent rendering.', 'maple-icons' ); ?>
</p>
<p>
<?php esc_html_e( 'All icons use currentColor for styling, which means they automatically inherit text color from your theme or block settings.', 'maple-icons' ); ?>
</p>
<p>
<?php
printf(
/* translators: %s: Directory path */
esc_html__( 'Icons are stored in: %s', 'maple-icons' ),
'<code>' . esc_html( MI_ICONS_DIR ) . '</code>'
);
?>
</p>
</div>
</div>
<?php
}
}

View file

@ -1,376 +0,0 @@
<?php
/**
* AJAX Handler class - Handles all AJAX requests.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Ajax_Handler
*
* Handles all AJAX requests for the plugin.
*/
class MI_Ajax_Handler {
/**
* Constructor - Register AJAX handlers.
*/
public function __construct() {
// Admin AJAX handlers.
add_action( 'wp_ajax_mi_download_set', array( $this, 'handle_download_set' ) );
add_action( 'wp_ajax_mi_delete_set', array( $this, 'handle_delete_set' ) );
add_action( 'wp_ajax_mi_set_active', array( $this, 'handle_set_active' ) );
add_action( 'wp_ajax_mi_get_progress', array( $this, 'handle_get_progress' ) );
// Block editor AJAX handlers.
add_action( 'wp_ajax_mi_search_icons', array( $this, 'handle_search_icons' ) );
add_action( 'wp_ajax_mi_get_icon_svg', array( $this, 'handle_get_icon_svg' ) );
// No nopriv handlers - all functionality requires login.
}
/**
* Handle download set request.
*/
public function handle_download_set() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
if ( empty( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Icon set slug is required.', 'maple-icons' ) )
);
}
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) )
);
}
// Check if already downloaded.
$registry = MI_Icon_Registry::get_instance();
if ( $registry->is_downloaded( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'This icon set is already downloaded.', 'maple-icons' ) )
);
}
// 4. Process download.
// Increase time limit for large downloads.
set_time_limit( 600 ); // 10 minutes.
$downloader = new MI_Downloader();
$result = $downloader->download_set( $slug );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array( 'message' => $result->get_error_message() )
);
}
if ( ! $result['success'] ) {
wp_send_json_error(
array(
'message' => sprintf(
/* translators: %1$d: downloaded count, %2$d: failed count */
__( 'Download completed with errors. %1$d downloaded, %2$d failed.', 'maple-icons' ),
$result['downloaded'],
$result['failed']
),
'errors' => array_slice( $result['errors'], 0, 10 ), // Limit errors shown.
'downloaded' => $result['downloaded'],
'failed' => $result['failed'],
)
);
}
// Mark as downloaded.
$registry->mark_downloaded( $slug, $result['downloaded'] );
// If no active set, make this one active.
if ( ! $registry->get_active_set() ) {
$registry->set_active( $slug );
}
$registry->refresh();
wp_send_json_success(
array(
'message' => sprintf(
/* translators: %d: number of icons */
__( 'Successfully downloaded %d icons.', 'maple-icons' ),
$result['downloaded']
),
'icon_count' => $result['downloaded'],
'is_active' => $registry->get_active_set() === $slug,
)
);
}
/**
* Handle delete set request.
*/
public function handle_delete_set() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
if ( empty( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Icon set slug is required.', 'maple-icons' ) )
);
}
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) )
);
}
// 4. Delete the set.
$registry = MI_Icon_Registry::get_instance();
$result = $registry->delete_set( $slug );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array( 'message' => $result->get_error_message() )
);
}
wp_send_json_success(
array(
'message' => __( 'Icon set deleted successfully.', 'maple-icons' ),
)
);
}
/**
* Handle set active request.
*/
public function handle_set_active() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
// Empty slug is allowed (to deactivate).
if ( ! empty( $slug ) && ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) )
);
}
// 4. Set active.
$registry = MI_Icon_Registry::get_instance();
$result = $registry->set_active( $slug );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array( 'message' => $result->get_error_message() )
);
}
wp_send_json_success(
array(
'message' => empty( $slug )
? __( 'No icon set is now active.', 'maple-icons' )
: __( 'Icon set activated.', 'maple-icons' ),
'active_set' => $slug,
)
);
}
/**
* Handle get progress request.
*/
public function handle_get_progress() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_admin_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
if ( empty( $slug ) || ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
wp_send_json_error(
array( 'message' => __( 'Invalid icon set.', 'maple-icons' ) )
);
}
// 4. Get progress.
$downloader = new MI_Downloader();
$progress = $downloader->get_progress( $slug );
if ( false === $progress ) {
wp_send_json_success(
array(
'downloading' => false,
'completed' => 0,
'total' => 0,
)
);
}
wp_send_json_success(
array(
'downloading' => true,
'completed' => $progress['completed'],
'total' => $progress['total'],
'percentage' => $progress['total'] > 0
? round( ( $progress['completed'] / $progress['total'] ) * 100 )
: 0,
)
);
}
/**
* Handle search icons request (for block editor).
*/
public function handle_search_icons() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_block_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check (edit_posts for block usage).
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$query = isset( $_POST['query'] ) ? sanitize_text_field( $_POST['query'] ) : '';
$style = isset( $_POST['style'] ) ? sanitize_key( $_POST['style'] ) : '';
$limit = isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : MI_SEARCH_LIMIT;
$offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;
// Limit the limit.
if ( $limit > 100 ) {
$limit = 100;
}
// 4. Search.
$registry = MI_Icon_Registry::get_instance();
$results = $registry->search_icons( $query, $style, $limit, $offset );
wp_send_json_success( $results );
}
/**
* Handle get icon SVG request (for block editor).
*/
public function handle_get_icon_svg() {
// 1. Nonce verification.
if ( ! check_ajax_referer( 'mi_block_nonce', 'nonce', false ) ) {
wp_send_json_error(
array( 'message' => __( 'Security check failed.', 'maple-icons' ) ),
403
);
}
// 2. Capability check.
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error(
array( 'message' => __( 'You do not have permission to do this.', 'maple-icons' ) ),
403
);
}
// 3. Input validation.
$slug = isset( $_POST['set'] ) ? sanitize_key( $_POST['set'] ) : '';
$style = isset( $_POST['style'] ) ? sanitize_key( $_POST['style'] ) : '';
$name = isset( $_POST['name'] ) ? sanitize_file_name( $_POST['name'] ) : '';
if ( empty( $slug ) || empty( $style ) || empty( $name ) ) {
wp_send_json_error(
array( 'message' => __( 'Missing required parameters.', 'maple-icons' ) )
);
}
// 4. Get SVG.
$registry = MI_Icon_Registry::get_instance();
$svg = $registry->get_icon_svg( $slug, $style, $name );
if ( is_wp_error( $svg ) ) {
wp_send_json_error(
array( 'message' => $svg->get_error_message() )
);
}
wp_send_json_success(
array(
'svg' => $svg,
'set' => $slug,
'style' => $style,
'name' => $name,
)
);
}
}

View file

@ -1,505 +0,0 @@
<?php
/**
* Downloader class - Handles fetching icons from CDN and storing locally.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Downloader
*
* Downloads icon sets from CDN and stores them locally.
*/
class MI_Downloader {
/**
* Allowed SVG elements for sanitization.
*
* @var array
*/
private static $allowed_svg_elements = array(
'svg' => array(
'xmlns' => true,
'viewbox' => true,
'width' => true,
'height' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'class' => true,
'aria-hidden' => true,
'role' => true,
'focusable' => true,
'style' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'fill-rule' => true,
'clip-rule' => true,
'opacity' => true,
'fill-opacity' => true,
'stroke-opacity' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
),
'rect' => array(
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'rx' => true,
'ry' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
),
'line' => array(
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'opacity' => true,
),
'polyline' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'opacity' => true,
),
'polygon' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
),
'ellipse' => array(
'cx' => true,
'cy' => true,
'rx' => true,
'ry' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
),
'g' => array(
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'transform' => true,
'opacity' => true,
),
'defs' => array(),
'clippath' => array(
'id' => true,
),
'use' => array(
'href' => true,
'xlink:href' => true,
),
);
/**
* Download an entire icon set.
*
* @param string $slug Icon set slug.
* @param callable|null $progress_callback Optional progress callback.
* @return array|WP_Error Download results or error.
*/
public function download_set( $slug, $progress_callback = null ) {
// Validate slug.
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error(
'invalid_set',
__( 'Invalid icon set.', 'maple-icons' )
);
}
$set_config = MI_Icon_Sets::get( $slug );
$manifest = MI_Icon_Sets::load_manifest( $slug );
if ( is_wp_error( $manifest ) ) {
return $manifest;
}
// Create base directory.
$base_dir = MI_ICONS_DIR . $slug;
if ( ! $this->ensure_directory( $base_dir ) ) {
return new WP_Error(
'directory_error',
__( 'Could not create icon directory.', 'maple-icons' )
);
}
// Create style directories.
foreach ( $set_config['styles'] as $style_slug => $style_config ) {
$style_dir = $base_dir . '/' . $style_slug;
if ( ! $this->ensure_directory( $style_dir ) ) {
return new WP_Error(
'directory_error',
__( 'Could not create style directory.', 'maple-icons' )
);
}
}
$icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array();
$total_icons = count( $icons );
$downloaded = 0;
$failed = 0;
$errors = array();
$total_to_download = 0;
// Calculate total icons to download (icon × styles).
foreach ( $icons as $icon ) {
$icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] );
$total_to_download += count( $icon_styles );
}
// Initialize progress transient.
set_transient(
'mi_download_progress_' . $slug,
array(
'completed' => 0,
'total' => $total_to_download,
'status' => 'downloading',
),
HOUR_IN_SECONDS
);
$current = 0;
// Download each icon.
foreach ( $icons as $icon ) {
$icon_name = $icon['name'];
$icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] );
foreach ( $icon_styles as $style ) {
if ( ! isset( $set_config['styles'][ $style ] ) ) {
continue;
}
$result = $this->download_icon( $slug, $style, $icon_name );
if ( is_wp_error( $result ) ) {
$failed++;
$errors[] = sprintf( '%s/%s: %s', $style, $icon_name, $result->get_error_message() );
} else {
$downloaded++;
}
$current++;
// Update progress.
set_transient(
'mi_download_progress_' . $slug,
array(
'completed' => $current,
'total' => $total_to_download,
'status' => 'downloading',
),
HOUR_IN_SECONDS
);
// Call progress callback if provided.
if ( is_callable( $progress_callback ) ) {
call_user_func( $progress_callback, $current, $total_to_download );
}
// Allow some breathing room for the server.
if ( 0 === $current % MI_DOWNLOAD_BATCH_SIZE ) {
usleep( 100000 ); // 100ms pause every batch.
}
}
}
// Clear progress transient.
delete_transient( 'mi_download_progress_' . $slug );
return array(
'success' => $failed === 0,
'downloaded' => $downloaded,
'failed' => $failed,
'total' => $total_to_download,
'errors' => $errors,
);
}
/**
* Download a single icon from CDN.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string|WP_Error Local file path or error.
*/
public function download_icon( $slug, $style, $name ) {
// Validate inputs.
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) );
}
if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) {
return new WP_Error( 'invalid_style', __( 'Invalid icon style.', 'maple-icons' ) );
}
// Validate icon name (only allow alphanumeric, hyphens, underscores).
if ( ! preg_match( '/^[a-z0-9\-_]+$/i', $name ) ) {
return new WP_Error( 'invalid_name', __( 'Invalid icon name.', 'maple-icons' ) );
}
$cdn_url = MI_Icon_Sets::get_cdn_url( $slug, $style, $name );
$local_path = MI_Icon_Sets::get_local_path( $slug, $style, $name );
// Check if already downloaded.
if ( file_exists( $local_path ) ) {
return $local_path;
}
// Fetch from CDN.
$response = wp_remote_get(
$cdn_url,
array(
'timeout' => MI_DOWNLOAD_TIMEOUT,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
return new WP_Error(
'cdn_error',
sprintf(
/* translators: %d: HTTP status code */
__( 'CDN returned status %d.', 'maple-icons' ),
$status_code
)
);
}
$svg_content = wp_remote_retrieve_body( $response );
if ( empty( $svg_content ) ) {
return new WP_Error( 'empty_response', __( 'Empty response from CDN.', 'maple-icons' ) );
}
// Get set config for normalization.
$set_config = MI_Icon_Sets::get( $slug );
// Normalize and sanitize SVG.
$svg_content = $this->normalize_svg( $svg_content, $set_config );
$svg_content = $this->sanitize_svg( $svg_content );
if ( empty( $svg_content ) ) {
return new WP_Error( 'invalid_svg', __( 'Invalid or empty SVG content.', 'maple-icons' ) );
}
// Ensure directory exists.
$dir = dirname( $local_path );
if ( ! $this->ensure_directory( $dir ) ) {
return new WP_Error( 'directory_error', __( 'Could not create directory.', 'maple-icons' ) );
}
// Write file.
global $wp_filesystem;
if ( empty( $wp_filesystem ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if ( ! $wp_filesystem->put_contents( $local_path, $svg_content, FS_CHMOD_FILE ) ) {
return new WP_Error( 'write_error', __( 'Could not write SVG file.', 'maple-icons' ) );
}
return $local_path;
}
/**
* Normalize SVG content based on set configuration.
*
* @param string $svg SVG content.
* @param array $set_config Icon set configuration.
* @return string Normalized SVG content.
*/
private function normalize_svg( $svg, $set_config ) {
// Strip XML declaration.
$svg = preg_replace( '/<\?xml[^>]*\?>/i', '', $svg );
// Strip DOCTYPE.
$svg = preg_replace( '/<!DOCTYPE[^>]*>/i', '', $svg );
// Strip comments.
$svg = preg_replace( '/<!--.*?-->/s', '', $svg );
// Strip title and desc elements.
$svg = preg_replace( '/<title[^>]*>.*?<\/title>/is', '', $svg );
$svg = preg_replace( '/<desc[^>]*>.*?<\/desc>/is', '', $svg );
// Normalize viewBox for Phosphor (256 → 24).
if ( ! empty( $set_config['normalize'] ) ) {
$svg = preg_replace(
'/viewBox=["\']0\s+0\s+256\s+256["\']/i',
'viewBox="0 0 24 24"',
$svg
);
}
// Fix hardcoded colors for Material.
if ( ! empty( $set_config['color_fix'] ) ) {
// Replace hardcoded hex colors.
$svg = preg_replace( '/fill=["\']#[0-9a-fA-F]{3,6}["\']/', 'fill="currentColor"', $svg );
$svg = preg_replace( '/fill=["\']black["\']/', 'fill="currentColor"', $svg );
$svg = preg_replace( '/fill=["\']rgb\([^)]+\)["\']/', 'fill="currentColor"', $svg );
// Same for stroke.
$svg = preg_replace( '/stroke=["\']#[0-9a-fA-F]{3,6}["\']/', 'stroke="currentColor"', $svg );
$svg = preg_replace( '/stroke=["\']black["\']/', 'stroke="currentColor"', $svg );
}
// Remove width/height attributes (let CSS control size).
$svg = preg_replace( '/\s(width|height)=["\'][^"\']*["\']/i', '', $svg );
// Ensure there's no leading/trailing whitespace.
$svg = trim( $svg );
return $svg;
}
/**
* Sanitize SVG content to remove potentially dangerous elements.
*
* @param string $svg SVG content.
* @return string Sanitized SVG content.
*/
private function sanitize_svg( $svg ) {
// Remove script tags.
$svg = preg_replace( '/<script\b[^>]*>.*?<\/script>/is', '', $svg );
// Remove event handlers.
$svg = preg_replace( '/\s+on\w+\s*=/i', ' data-removed=', $svg );
// Remove javascript: URLs.
$svg = preg_replace( '/javascript:/i', 'removed:', $svg );
// Remove data: URLs (except for certain safe uses).
$svg = preg_replace( '/data:[^"\'>\s]+/i', 'removed:', $svg );
// Use WordPress kses with our allowed tags.
$svg = wp_kses( $svg, self::$allowed_svg_elements );
// Verify it's still a valid SVG.
if ( strpos( $svg, '<svg' ) === false || strpos( $svg, '</svg>' ) === false ) {
return '';
}
return $svg;
}
/**
* Ensure a directory exists, creating it if necessary.
*
* @param string $dir Directory path.
* @return bool True if directory exists or was created.
*/
private function ensure_directory( $dir ) {
if ( file_exists( $dir ) ) {
return is_dir( $dir );
}
return wp_mkdir_p( $dir );
}
/**
* Delete all icons for a set.
*
* @param string $slug Icon set slug.
* @return bool True on success.
*/
public function delete_set( $slug ) {
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return false;
}
$set_dir = MI_ICONS_DIR . $slug;
// Validate the path is within our icons directory.
$real_path = realpath( $set_dir );
$allowed_base = realpath( MI_ICONS_DIR );
if ( false === $real_path || false === $allowed_base ) {
// Directory doesn't exist, nothing to delete.
return true;
}
if ( strpos( $real_path, $allowed_base ) !== 0 ) {
// Path traversal attempt.
return false;
}
// Recursively delete the directory.
return $this->delete_directory( $set_dir );
}
/**
* Recursively delete a directory.
*
* @param string $dir Directory path.
* @return bool True on success.
*/
private function delete_directory( $dir ) {
if ( ! is_dir( $dir ) ) {
return true;
}
$files = array_diff( scandir( $dir ), array( '.', '..' ) );
foreach ( $files as $file ) {
$path = $dir . '/' . $file;
if ( is_dir( $path ) ) {
$this->delete_directory( $path );
} else {
wp_delete_file( $path );
}
}
return rmdir( $dir );
}
/**
* Get download progress for a set.
*
* @param string $slug Icon set slug.
* @return array|false Progress data or false if not downloading.
*/
public function get_progress( $slug ) {
return get_transient( 'mi_download_progress_' . $slug );
}
}

View file

@ -1,416 +0,0 @@
<?php
/**
* Icon Registry class - Manages downloaded sets and provides icon search/retrieval.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Icon_Registry
*
* Manages downloaded icon sets and provides search/retrieval functionality.
*/
class MI_Icon_Registry {
/**
* Singleton instance.
*
* @var MI_Icon_Registry|null
*/
private static $instance = null;
/**
* Cached settings.
*
* @var array|null
*/
private $settings = null;
/**
* Get singleton instance.
*
* @return MI_Icon_Registry
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*/
private function __construct() {
$this->load_settings();
}
/**
* Load settings from database.
*/
private function load_settings() {
$this->settings = get_option(
'maple_icons_settings',
array(
'active_set' => '',
'downloaded_sets' => array(),
)
);
}
/**
* Save settings to database.
*
* @return bool True on success.
*/
private function save_settings() {
return update_option( 'maple_icons_settings', $this->settings );
}
/**
* Get all downloaded sets.
*
* @return array Array of downloaded set data.
*/
public function get_downloaded_sets() {
return isset( $this->settings['downloaded_sets'] ) ? $this->settings['downloaded_sets'] : array();
}
/**
* Get the active set slug.
*
* @return string|null Active set slug or null if none.
*/
public function get_active_set() {
$active = isset( $this->settings['active_set'] ) ? $this->settings['active_set'] : '';
return ! empty( $active ) ? $active : null;
}
/**
* Set the active icon set.
*
* @param string $slug Icon set slug.
* @return bool|WP_Error True on success, error on failure.
*/
public function set_active( $slug ) {
// Allow empty string to deactivate.
if ( empty( $slug ) ) {
$this->settings['active_set'] = '';
return $this->save_settings();
}
// Validate slug.
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) );
}
// Check if downloaded.
if ( ! $this->is_downloaded( $slug ) ) {
return new WP_Error( 'not_downloaded', __( 'Icon set is not downloaded.', 'maple-icons' ) );
}
$this->settings['active_set'] = $slug;
return $this->save_settings();
}
/**
* Check if a set is downloaded.
*
* @param string $slug Icon set slug.
* @return bool True if downloaded.
*/
public function is_downloaded( $slug ) {
$downloaded = $this->get_downloaded_sets();
return isset( $downloaded[ $slug ] );
}
/**
* Mark a set as downloaded.
*
* @param string $slug Icon set slug.
* @param int $icon_count Number of icons downloaded.
* @return bool True on success.
*/
public function mark_downloaded( $slug, $icon_count ) {
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return false;
}
$set_config = MI_Icon_Sets::get( $slug );
$this->settings['downloaded_sets'][ $slug ] = array(
'version' => $set_config['version'],
'downloaded_at' => current_time( 'mysql' ),
'icon_count' => $icon_count,
);
return $this->save_settings();
}
/**
* Remove a set from downloaded list.
*
* @param string $slug Icon set slug.
* @return bool True on success.
*/
public function unmark_downloaded( $slug ) {
if ( isset( $this->settings['downloaded_sets'][ $slug ] ) ) {
unset( $this->settings['downloaded_sets'][ $slug ] );
}
// If this was the active set, clear it.
if ( $this->settings['active_set'] === $slug ) {
$this->settings['active_set'] = '';
}
return $this->save_settings();
}
/**
* Get all icons for a set and style.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @return array Array of icon data.
*/
public function get_icons_for_set( $slug, $style ) {
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return array();
}
if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) {
return array();
}
// Load manifest.
$manifest = MI_Icon_Sets::load_manifest( $slug );
if ( is_wp_error( $manifest ) ) {
return array();
}
$icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array();
$result = array();
foreach ( $icons as $icon ) {
$icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( MI_Icon_Sets::get( $slug )['styles'] );
// Only include if this icon has the requested style.
if ( in_array( $style, $icon_styles, true ) ) {
$result[] = array(
'name' => $icon['name'],
'tags' => isset( $icon['tags'] ) ? $icon['tags'] : array(),
'category' => isset( $icon['category'] ) ? $icon['category'] : '',
);
}
}
return $result;
}
/**
* Search icons in the active set.
*
* @param string $query Search query.
* @param string $style Optional style filter.
* @param int $limit Maximum results.
* @param int $offset Offset for pagination.
* @return array Search results.
*/
public function search_icons( $query = '', $style = '', $limit = MI_SEARCH_LIMIT, $offset = 0 ) {
$active_set = $this->get_active_set();
if ( ! $active_set ) {
return array(
'icons' => array(),
'total' => 0,
);
}
$set_config = MI_Icon_Sets::get( $active_set );
if ( ! $set_config ) {
return array(
'icons' => array(),
'total' => 0,
);
}
// Default to first style if not specified.
if ( empty( $style ) ) {
$style = $set_config['default_style'];
}
// Validate style.
if ( ! MI_Icon_Sets::is_valid_style( $active_set, $style ) ) {
$style = $set_config['default_style'];
}
// Load manifest.
$manifest = MI_Icon_Sets::load_manifest( $active_set );
if ( is_wp_error( $manifest ) ) {
return array(
'icons' => array(),
'total' => 0,
);
}
$icons = isset( $manifest['icons'] ) ? $manifest['icons'] : array();
$query = strtolower( trim( $query ) );
$results = array();
foreach ( $icons as $icon ) {
$icon_styles = isset( $icon['styles'] ) ? $icon['styles'] : array_keys( $set_config['styles'] );
// Skip if this icon doesn't have the requested style.
if ( ! in_array( $style, $icon_styles, true ) ) {
continue;
}
// If no query, include all.
if ( empty( $query ) ) {
$results[] = $icon;
continue;
}
// Search in name.
if ( strpos( strtolower( $icon['name'] ), $query ) !== false ) {
$results[] = $icon;
continue;
}
// Search in tags.
if ( isset( $icon['tags'] ) && is_array( $icon['tags'] ) ) {
foreach ( $icon['tags'] as $tag ) {
if ( strpos( strtolower( $tag ), $query ) !== false ) {
$results[] = $icon;
break;
}
}
}
}
$total = count( $results );
// Apply offset and limit.
$results = array_slice( $results, $offset, $limit );
// Format results.
$formatted = array();
foreach ( $results as $icon ) {
$formatted[] = array(
'name' => $icon['name'],
'tags' => isset( $icon['tags'] ) ? $icon['tags'] : array(),
'category' => isset( $icon['category'] ) ? $icon['category'] : '',
'set' => $active_set,
'style' => $style,
);
}
return array(
'icons' => $formatted,
'total' => $total,
'set' => $active_set,
'style' => $style,
'styles' => MI_Icon_Sets::get_style_labels( $active_set ),
);
}
/**
* Get SVG content for a specific icon.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string|WP_Error SVG content or error.
*/
public function get_icon_svg( $slug, $style, $name ) {
// Validate inputs.
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) );
}
if ( ! MI_Icon_Sets::is_valid_style( $slug, $style ) ) {
return new WP_Error( 'invalid_style', __( 'Invalid icon style.', 'maple-icons' ) );
}
// Validate icon name.
if ( ! preg_match( '/^[a-z0-9\-_]+$/i', $name ) ) {
return new WP_Error( 'invalid_name', __( 'Invalid icon name.', 'maple-icons' ) );
}
$local_path = MI_Icon_Sets::get_local_path( $slug, $style, $name );
// Validate path is within icons directory (prevent path traversal).
$real_path = realpath( $local_path );
$allowed_base = realpath( MI_ICONS_DIR );
if ( false === $real_path ) {
return new WP_Error( 'not_found', __( 'Icon not found.', 'maple-icons' ) );
}
if ( false === $allowed_base || strpos( $real_path, $allowed_base ) !== 0 ) {
return new WP_Error( 'invalid_path', __( 'Invalid icon path.', 'maple-icons' ) );
}
if ( ! file_exists( $real_path ) ) {
return new WP_Error( 'not_found', __( 'Icon not found.', 'maple-icons' ) );
}
$svg = file_get_contents( $real_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
if ( false === $svg ) {
return new WP_Error( 'read_error', __( 'Could not read icon file.', 'maple-icons' ) );
}
return $svg;
}
/**
* Delete a downloaded set.
*
* @param string $slug Icon set slug.
* @return bool|WP_Error True on success, error on failure.
*/
public function delete_set( $slug ) {
if ( ! MI_Icon_Sets::is_valid_slug( $slug ) ) {
return new WP_Error( 'invalid_set', __( 'Invalid icon set.', 'maple-icons' ) );
}
if ( ! $this->is_downloaded( $slug ) ) {
return new WP_Error( 'not_downloaded', __( 'Icon set is not downloaded.', 'maple-icons' ) );
}
// Delete files.
$downloader = new MI_Downloader();
$deleted = $downloader->delete_set( $slug );
if ( ! $deleted ) {
return new WP_Error( 'delete_error', __( 'Could not delete icon files.', 'maple-icons' ) );
}
// Update settings.
$this->unmark_downloaded( $slug );
return true;
}
/**
* Get info about a downloaded set.
*
* @param string $slug Icon set slug.
* @return array|null Set info or null if not downloaded.
*/
public function get_downloaded_info( $slug ) {
$downloaded = $this->get_downloaded_sets();
return isset( $downloaded[ $slug ] ) ? $downloaded[ $slug ] : null;
}
/**
* Refresh settings from database.
*/
public function refresh() {
$this->load_settings();
}
}

View file

@ -1,328 +0,0 @@
<?php
/**
* Icon Sets class - Static definitions of all preset icon sets.
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class MI_Icon_Sets
*
* Provides static definitions and utilities for preset icon sets.
*/
class MI_Icon_Sets {
/**
* Get all available preset icon sets.
*
* @return array Array of icon set configurations.
*/
public static function get_all() {
return array(
'heroicons' => array(
'slug' => 'heroicons',
'name' => 'Heroicons',
'version' => '2.1.1',
'license' => 'MIT',
'url' => 'https://heroicons.com',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/heroicons@2.1.1/',
'styles' => array(
'outline' => array(
'path' => '24/outline',
'label' => 'Outline',
),
'solid' => array(
'path' => '24/solid',
'label' => 'Solid',
),
'mini' => array(
'path' => '20/solid',
'label' => 'Mini',
),
),
'default_style' => 'outline',
'viewbox' => '0 0 24 24',
'normalize' => false,
'color_fix' => false,
'description' => 'Beautiful hand-crafted SVG icons by the makers of Tailwind CSS.',
'icon_count' => 292,
),
'lucide' => array(
'slug' => 'lucide',
'name' => 'Lucide',
'version' => '0.303.0',
'license' => 'ISC',
'url' => 'https://lucide.dev',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/lucide-static@0.303.0/',
'styles' => array(
'icons' => array(
'path' => 'icons',
'label' => 'Default',
),
),
'default_style' => 'icons',
'viewbox' => '0 0 24 24',
'normalize' => false,
'color_fix' => false,
'description' => 'Beautiful & consistent icon toolkit made by the community.',
'icon_count' => 1411,
),
'feather' => array(
'slug' => 'feather',
'name' => 'Feather',
'version' => '4.29.1',
'license' => 'MIT',
'url' => 'https://feathericons.com',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/feather-icons@4.29.1/',
'styles' => array(
'icons' => array(
'path' => 'dist/icons',
'label' => 'Default',
),
),
'default_style' => 'icons',
'viewbox' => '0 0 24 24',
'normalize' => false,
'color_fix' => false,
'description' => 'Simply beautiful open source icons.',
'icon_count' => 287,
),
'phosphor' => array(
'slug' => 'phosphor',
'name' => 'Phosphor',
'version' => '2.1.1',
'license' => 'MIT',
'url' => 'https://phosphoricons.com',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.1.1/',
'styles' => array(
'regular' => array(
'path' => 'assets/regular',
'label' => 'Regular',
),
'bold' => array(
'path' => 'assets/bold',
'label' => 'Bold',
),
'light' => array(
'path' => 'assets/light',
'label' => 'Light',
),
'thin' => array(
'path' => 'assets/thin',
'label' => 'Thin',
),
'fill' => array(
'path' => 'assets/fill',
'label' => 'Fill',
),
'duotone' => array(
'path' => 'assets/duotone',
'label' => 'Duotone',
),
),
'default_style' => 'regular',
'viewbox' => '0 0 256 256',
'normalize' => true, // Needs viewBox normalization to 24x24.
'color_fix' => false,
'description' => 'A flexible icon family for interfaces, diagrams, presentations, and more.',
'icon_count' => 1248,
),
'material' => array(
'slug' => 'material',
'name' => 'Material Design Icons',
'version' => '0.14.13',
'license' => 'Apache-2.0',
'url' => 'https://fonts.google.com/icons',
'cdn_base' => 'https://cdn.jsdelivr.net/npm/@material-design-icons/svg@0.14.13/',
'styles' => array(
'filled' => array(
'path' => 'filled',
'label' => 'Filled',
),
'outlined' => array(
'path' => 'outlined',
'label' => 'Outlined',
),
'round' => array(
'path' => 'round',
'label' => 'Round',
),
'sharp' => array(
'path' => 'sharp',
'label' => 'Sharp',
),
'two-tone' => array(
'path' => 'two-tone',
'label' => 'Two Tone',
),
),
'default_style' => 'filled',
'viewbox' => '0 0 24 24',
'normalize' => false,
'color_fix' => true, // Needs hardcoded color replacement.
'description' => 'Material Design Icons by Google. Beautiful, delightful, and easy to use.',
'icon_count' => 2189,
),
);
}
/**
* Get a specific icon set by slug.
*
* @param string $slug Icon set slug.
* @return array|null Icon set configuration or null if not found.
*/
public static function get( $slug ) {
$sets = self::get_all();
return isset( $sets[ $slug ] ) ? $sets[ $slug ] : null;
}
/**
* Get all available set slugs.
*
* @return array Array of set slugs.
*/
public static function get_slugs() {
return array_keys( self::get_all() );
}
/**
* Validate that a slug is a valid preset.
*
* @param string $slug Slug to validate.
* @return bool True if valid, false otherwise.
*/
public static function is_valid_slug( $slug ) {
return in_array( $slug, self::get_slugs(), true );
}
/**
* Validate that a style is valid for a given set.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @return bool True if valid, false otherwise.
*/
public static function is_valid_style( $slug, $style ) {
$set = self::get( $slug );
if ( ! $set ) {
return false;
}
return isset( $set['styles'][ $style ] );
}
/**
* Get the CDN URL for a specific icon.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string|null CDN URL or null if invalid.
*/
public static function get_cdn_url( $slug, $style, $name ) {
$set = self::get( $slug );
if ( ! $set || ! isset( $set['styles'][ $style ] ) ) {
return null;
}
$style_config = $set['styles'][ $style ];
return $set['cdn_base'] . $style_config['path'] . '/' . $name . '.svg';
}
/**
* Get the local file path for a specific icon.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string Local file path.
*/
public static function get_local_path( $slug, $style, $name ) {
return MI_ICONS_DIR . $slug . '/' . $style . '/' . $name . '.svg';
}
/**
* Get the local URL for a specific icon.
*
* @param string $slug Icon set slug.
* @param string $style Style slug.
* @param string $name Icon name.
* @return string Local URL.
*/
public static function get_local_url( $slug, $style, $name ) {
return MI_ICONS_URL . $slug . '/' . $style . '/' . $name . '.svg';
}
/**
* Get the manifest file path for a set.
*
* @param string $slug Icon set slug.
* @return string Manifest file path.
*/
public static function get_manifest_path( $slug ) {
return MI_PRESETS_DIR . $slug . '.json';
}
/**
* Load and parse a manifest file.
*
* @param string $slug Icon set slug.
* @return array|WP_Error Manifest data or error.
*/
public static function load_manifest( $slug ) {
$path = self::get_manifest_path( $slug );
if ( ! file_exists( $path ) ) {
return new WP_Error(
'manifest_not_found',
sprintf(
/* translators: %s: Icon set name */
__( 'Manifest file not found for %s.', 'maple-icons' ),
$slug
)
);
}
$content = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
if ( false === $content ) {
return new WP_Error(
'manifest_read_error',
__( 'Could not read manifest file.', 'maple-icons' )
);
}
$manifest = json_decode( $content, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error(
'manifest_parse_error',
__( 'Could not parse manifest file.', 'maple-icons' )
);
}
return $manifest;
}
/**
* Get style labels for a set.
*
* @param string $slug Icon set slug.
* @return array Array of style labels.
*/
public static function get_style_labels( $slug ) {
$set = self::get( $slug );
if ( ! $set ) {
return array();
}
$labels = array();
foreach ( $set['styles'] as $style_slug => $style_config ) {
$labels[ $style_slug ] = $style_config['label'];
}
return $labels;
}
}

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

View file

@ -1,230 +0,0 @@
<?php
/**
* Plugin Name: Maple Icons
* Plugin URI: https://jetrails.com/maple-icons
* Description: Insert beautiful open-source icons into your content with a Gutenberg block. Download icon sets from CDN and serve locally.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: JetRails
* Author URI: https://jetrails.com
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: maple-icons
* Domain Path: /languages
*
* @package MapleIcons
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Plugin constants.
*/
define( 'MI_VERSION', '1.0.0' );
define( 'MI_PLUGIN_FILE', __FILE__ );
define( 'MI_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MI_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'MI_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
define( 'MI_ICONS_DIR', WP_CONTENT_DIR . '/maple-icons/' );
define( 'MI_ICONS_URL', content_url( '/maple-icons/' ) );
define( 'MI_PRESETS_DIR', MI_PLUGIN_DIR . 'presets/' );
// Limits.
define( 'MI_DOWNLOAD_BATCH_SIZE', 50 );
define( 'MI_SEARCH_LIMIT', 50 );
define( 'MI_DOWNLOAD_TIMEOUT', 10 );
/**
* Autoloader for plugin classes.
*
* @param string $class_name The class name to load.
*/
function mi_autoloader( $class_name ) {
// Only handle our classes.
if ( strpos( $class_name, 'MI_' ) !== 0 ) {
return;
}
// Convert class name to filename.
$class_file = 'class-' . strtolower( str_replace( '_', '-', $class_name ) ) . '.php';
$class_path = MI_PLUGIN_DIR . 'includes/' . $class_file;
if ( file_exists( $class_path ) ) {
require_once $class_path;
}
}
spl_autoload_register( 'mi_autoloader' );
/**
* Plugin activation hook.
*/
function mi_activate() {
// Check WordPress version.
if ( version_compare( get_bloginfo( 'version' ), '6.5', '<' ) ) {
deactivate_plugins( MI_PLUGIN_BASENAME );
wp_die(
esc_html__( 'Maple Icons requires WordPress 6.5 or higher.', 'maple-icons' ),
esc_html__( 'Plugin Activation Error', 'maple-icons' ),
array( 'back_link' => true )
);
}
// Create icons directory if it doesn't exist.
if ( ! file_exists( MI_ICONS_DIR ) ) {
wp_mkdir_p( MI_ICONS_DIR );
}
// Initialize default settings.
$default_settings = array(
'active_set' => '',
'downloaded_sets' => array(),
);
if ( false === get_option( 'maple_icons_settings' ) ) {
add_option( 'maple_icons_settings', $default_settings );
}
// Flush rewrite rules.
flush_rewrite_rules();
}
register_activation_hook( MI_PLUGIN_FILE, 'mi_activate' );
/**
* Plugin deactivation hook.
*/
function mi_deactivate() {
// Clean up transients.
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mi_%' OR option_name LIKE '_transient_timeout_mi_%'"
);
flush_rewrite_rules();
}
register_deactivation_hook( MI_PLUGIN_FILE, 'mi_deactivate' );
/**
* Check WordPress version on admin init (in case WP was downgraded).
*/
function mi_check_wp_version() {
if ( version_compare( get_bloginfo( 'version' ), '6.5', '<' ) ) {
deactivate_plugins( MI_PLUGIN_BASENAME );
add_action( 'admin_notices', 'mi_wp_version_notice' );
}
}
add_action( 'admin_init', 'mi_check_wp_version' );
/**
* Display admin notice for WordPress version requirement.
*/
function mi_wp_version_notice() {
echo '<div class="error"><p>';
esc_html_e( 'Maple Icons has been deactivated. It requires WordPress 6.5 or higher.', 'maple-icons' );
echo '</p></div>';
}
/**
* Initialize the plugin.
*/
function mi_init() {
// Load text domain.
load_plugin_textdomain( 'maple-icons', false, dirname( MI_PLUGIN_BASENAME ) . '/languages' );
// Register the Gutenberg block.
mi_register_block();
}
add_action( 'init', 'mi_init' );
/**
* Register the Maple Icons Gutenberg block.
*/
function mi_register_block() {
// Check if build directory exists.
$block_path = MI_PLUGIN_DIR . 'build';
if ( ! file_exists( $block_path ) ) {
return;
}
register_block_type( $block_path );
}
/**
* Enqueue block editor assets.
*/
function mi_enqueue_block_assets() {
// Only load in block editor.
if ( ! is_admin() ) {
return;
}
$screen = get_current_screen();
if ( ! $screen || ! $screen->is_block_editor() ) {
return;
}
// Localize script data for the block editor.
$active_set = '';
$settings = get_option( 'maple_icons_settings', array() );
if ( ! empty( $settings['active_set'] ) ) {
$active_set = $settings['active_set'];
}
wp_localize_script(
'maple-icon-editor-script',
'miBlock',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mi_block_nonce' ),
'activeSet' => $active_set,
'settingsUrl' => admin_url( 'options-general.php?page=maple-icons' ),
)
);
}
add_action( 'enqueue_block_editor_assets', 'mi_enqueue_block_assets' );
/**
* Initialize admin functionality.
*/
function mi_admin_init() {
// Initialize admin page.
new MI_Admin_Page();
// Initialize AJAX handler.
new MI_Ajax_Handler();
}
add_action( 'admin_init', 'mi_admin_init' );
/**
* Add settings link to plugin action links.
*
* @param array $links Plugin action links.
* @return array Modified action links.
*/
function mi_add_action_links( $links ) {
$settings_link = sprintf(
'<a href="%s">%s</a>',
admin_url( 'options-general.php?page=maple-icons' ),
__( 'Settings', 'maple-icons' )
);
array_unshift( $links, $settings_link );
return $links;
}
add_filter( 'plugin_action_links_' . MI_PLUGIN_BASENAME, 'mi_add_action_links' );
/**
* Declare WooCommerce HPOS compatibility.
*/
function mi_declare_hpos_compatibility() {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
MI_PLUGIN_FILE,
true
);
}
}
add_action( 'before_woocommerce_init', 'mi_declare_hpos_compatibility' );

View file

@ -1,293 +0,0 @@
{
"slug": "feather",
"name": "Feather",
"version": "4.29.1",
"icons": [
{"name": "activity", "tags": ["pulse", "health", "action", "motion"], "category": "health", "styles": ["icons"]},
{"name": "airplay", "tags": ["stream", "cast", "mirroring"], "category": "devices", "styles": ["icons"]},
{"name": "alert-circle", "tags": ["warning", "alert", "danger"], "category": "alerts", "styles": ["icons"]},
{"name": "alert-octagon", "tags": ["warning", "alert", "danger", "stop"], "category": "alerts", "styles": ["icons"]},
{"name": "alert-triangle", "tags": ["warning", "alert", "danger"], "category": "alerts", "styles": ["icons"]},
{"name": "align-center", "tags": ["text", "alignment", "center"], "category": "text", "styles": ["icons"]},
{"name": "align-justify", "tags": ["text", "alignment", "justify"], "category": "text", "styles": ["icons"]},
{"name": "align-left", "tags": ["text", "alignment", "left"], "category": "text", "styles": ["icons"]},
{"name": "align-right", "tags": ["text", "alignment", "right"], "category": "text", "styles": ["icons"]},
{"name": "anchor", "tags": ["link", "nav", "navigation"], "category": "navigation", "styles": ["icons"]},
{"name": "aperture", "tags": ["camera", "photo", "lens"], "category": "photography", "styles": ["icons"]},
{"name": "archive", "tags": ["box", "storage", "backup"], "category": "files", "styles": ["icons"]},
{"name": "arrow-down", "tags": ["direction", "down"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-circle", "tags": ["direction", "down", "circle"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-left", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-right", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-left", "tags": ["direction", "back", "previous"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-left-circle", "tags": ["direction", "back", "circle"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-right", "tags": ["direction", "forward", "next"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-right-circle", "tags": ["direction", "forward", "circle"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up", "tags": ["direction", "up"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-circle", "tags": ["direction", "up", "circle"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-left", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-right", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]},
{"name": "at-sign", "tags": ["email", "mention", "at"], "category": "communication", "styles": ["icons"]},
{"name": "award", "tags": ["achievement", "badge", "trophy"], "category": "objects", "styles": ["icons"]},
{"name": "bar-chart", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "bar-chart-2", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "battery", "tags": ["power", "energy", "charge"], "category": "devices", "styles": ["icons"]},
{"name": "battery-charging", "tags": ["power", "energy", "charge"], "category": "devices", "styles": ["icons"]},
{"name": "bell", "tags": ["notification", "alert", "alarm"], "category": "notifications", "styles": ["icons"]},
{"name": "bell-off", "tags": ["notification", "mute", "silent"], "category": "notifications", "styles": ["icons"]},
{"name": "bluetooth", "tags": ["wireless", "connection"], "category": "connectivity", "styles": ["icons"]},
{"name": "bold", "tags": ["text", "format", "strong"], "category": "text", "styles": ["icons"]},
{"name": "book", "tags": ["read", "library", "education"], "category": "objects", "styles": ["icons"]},
{"name": "book-open", "tags": ["read", "library", "education"], "category": "objects", "styles": ["icons"]},
{"name": "bookmark", "tags": ["save", "favorite", "tag"], "category": "actions", "styles": ["icons"]},
{"name": "box", "tags": ["package", "container", "storage"], "category": "objects", "styles": ["icons"]},
{"name": "briefcase", "tags": ["work", "job", "business"], "category": "objects", "styles": ["icons"]},
{"name": "calendar", "tags": ["date", "schedule", "event"], "category": "time", "styles": ["icons"]},
{"name": "camera", "tags": ["photo", "picture", "image"], "category": "devices", "styles": ["icons"]},
{"name": "camera-off", "tags": ["photo", "disabled", "off"], "category": "devices", "styles": ["icons"]},
{"name": "cast", "tags": ["stream", "broadcast", "chromecast"], "category": "devices", "styles": ["icons"]},
{"name": "check", "tags": ["done", "complete", "success"], "category": "actions", "styles": ["icons"]},
{"name": "check-circle", "tags": ["done", "complete", "success"], "category": "actions", "styles": ["icons"]},
{"name": "check-square", "tags": ["done", "complete", "checkbox"], "category": "actions", "styles": ["icons"]},
{"name": "chevron-down", "tags": ["expand", "dropdown", "arrow"], "category": "arrows", "styles": ["icons"]},
{"name": "chevron-left", "tags": ["back", "previous", "arrow"], "category": "arrows", "styles": ["icons"]},
{"name": "chevron-right", "tags": ["forward", "next", "arrow"], "category": "arrows", "styles": ["icons"]},
{"name": "chevron-up", "tags": ["collapse", "up", "arrow"], "category": "arrows", "styles": ["icons"]},
{"name": "chevrons-down", "tags": ["expand", "more", "double"], "category": "arrows", "styles": ["icons"]},
{"name": "chevrons-left", "tags": ["back", "previous", "double"], "category": "arrows", "styles": ["icons"]},
{"name": "chevrons-right", "tags": ["forward", "next", "double"], "category": "arrows", "styles": ["icons"]},
{"name": "chevrons-up", "tags": ["collapse", "less", "double"], "category": "arrows", "styles": ["icons"]},
{"name": "chrome", "tags": ["browser", "google", "web"], "category": "brands", "styles": ["icons"]},
{"name": "circle", "tags": ["shape", "dot", "record"], "category": "shapes", "styles": ["icons"]},
{"name": "clipboard", "tags": ["copy", "paste", "board"], "category": "actions", "styles": ["icons"]},
{"name": "clock", "tags": ["time", "watch", "schedule"], "category": "time", "styles": ["icons"]},
{"name": "cloud", "tags": ["weather", "storage", "sky"], "category": "weather", "styles": ["icons"]},
{"name": "cloud-drizzle", "tags": ["weather", "rain", "drizzle"], "category": "weather", "styles": ["icons"]},
{"name": "cloud-lightning", "tags": ["weather", "storm", "thunder"], "category": "weather", "styles": ["icons"]},
{"name": "cloud-off", "tags": ["weather", "offline", "disconnect"], "category": "weather", "styles": ["icons"]},
{"name": "cloud-rain", "tags": ["weather", "rain", "precipitation"], "category": "weather", "styles": ["icons"]},
{"name": "cloud-snow", "tags": ["weather", "snow", "winter"], "category": "weather", "styles": ["icons"]},
{"name": "code", "tags": ["programming", "development", "html"], "category": "development", "styles": ["icons"]},
{"name": "codepen", "tags": ["brand", "code", "development"], "category": "brands", "styles": ["icons"]},
{"name": "codesandbox", "tags": ["brand", "code", "development"], "category": "brands", "styles": ["icons"]},
{"name": "coffee", "tags": ["drink", "cup", "cafe"], "category": "objects", "styles": ["icons"]},
{"name": "columns", "tags": ["layout", "grid", "table"], "category": "layout", "styles": ["icons"]},
{"name": "command", "tags": ["keyboard", "mac", "key"], "category": "devices", "styles": ["icons"]},
{"name": "compass", "tags": ["navigation", "direction", "explore"], "category": "navigation", "styles": ["icons"]},
{"name": "copy", "tags": ["duplicate", "clone", "clipboard"], "category": "actions", "styles": ["icons"]},
{"name": "corner-down-left", "tags": ["arrow", "return", "enter"], "category": "arrows", "styles": ["icons"]},
{"name": "corner-down-right", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]},
{"name": "corner-left-down", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]},
{"name": "corner-left-up", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]},
{"name": "corner-right-down", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]},
{"name": "corner-right-up", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]},
{"name": "corner-up-left", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]},
{"name": "corner-up-right", "tags": ["arrow", "direction"], "category": "arrows", "styles": ["icons"]},
{"name": "cpu", "tags": ["processor", "chip", "computer"], "category": "devices", "styles": ["icons"]},
{"name": "credit-card", "tags": ["payment", "card", "money"], "category": "commerce", "styles": ["icons"]},
{"name": "crop", "tags": ["image", "edit", "resize"], "category": "editing", "styles": ["icons"]},
{"name": "crosshair", "tags": ["target", "focus", "aim"], "category": "ui", "styles": ["icons"]},
{"name": "database", "tags": ["storage", "data", "server"], "category": "development", "styles": ["icons"]},
{"name": "delete", "tags": ["remove", "backspace", "erase"], "category": "actions", "styles": ["icons"]},
{"name": "disc", "tags": ["cd", "music", "record"], "category": "media", "styles": ["icons"]},
{"name": "divide", "tags": ["math", "division", "split"], "category": "math", "styles": ["icons"]},
{"name": "divide-circle", "tags": ["math", "division", "split"], "category": "math", "styles": ["icons"]},
{"name": "divide-square", "tags": ["math", "division", "split"], "category": "math", "styles": ["icons"]},
{"name": "dollar-sign", "tags": ["money", "currency", "payment"], "category": "commerce", "styles": ["icons"]},
{"name": "download", "tags": ["save", "export", "download"], "category": "actions", "styles": ["icons"]},
{"name": "download-cloud", "tags": ["save", "cloud", "download"], "category": "actions", "styles": ["icons"]},
{"name": "dribbble", "tags": ["brand", "design", "social"], "category": "brands", "styles": ["icons"]},
{"name": "droplet", "tags": ["water", "liquid", "drop"], "category": "weather", "styles": ["icons"]},
{"name": "edit", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]},
{"name": "edit-2", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]},
{"name": "edit-3", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]},
{"name": "external-link", "tags": ["link", "open", "new window"], "category": "actions", "styles": ["icons"]},
{"name": "eye", "tags": ["view", "visible", "show"], "category": "actions", "styles": ["icons"]},
{"name": "eye-off", "tags": ["hide", "invisible", "hidden"], "category": "actions", "styles": ["icons"]},
{"name": "facebook", "tags": ["brand", "social", "network"], "category": "brands", "styles": ["icons"]},
{"name": "fast-forward", "tags": ["media", "skip", "forward"], "category": "media", "styles": ["icons"]},
{"name": "feather", "tags": ["brand", "logo", "icon"], "category": "brands", "styles": ["icons"]},
{"name": "figma", "tags": ["brand", "design", "tool"], "category": "brands", "styles": ["icons"]},
{"name": "file", "tags": ["document", "page", "paper"], "category": "files", "styles": ["icons"]},
{"name": "file-minus", "tags": ["document", "remove", "delete"], "category": "files", "styles": ["icons"]},
{"name": "file-plus", "tags": ["document", "add", "new"], "category": "files", "styles": ["icons"]},
{"name": "file-text", "tags": ["document", "text", "content"], "category": "files", "styles": ["icons"]},
{"name": "film", "tags": ["video", "movie", "cinema"], "category": "media", "styles": ["icons"]},
{"name": "filter", "tags": ["funnel", "sort", "filter"], "category": "actions", "styles": ["icons"]},
{"name": "flag", "tags": ["report", "mark", "country"], "category": "objects", "styles": ["icons"]},
{"name": "folder", "tags": ["directory", "files", "organize"], "category": "files", "styles": ["icons"]},
{"name": "folder-minus", "tags": ["directory", "remove", "delete"], "category": "files", "styles": ["icons"]},
{"name": "folder-plus", "tags": ["directory", "add", "new"], "category": "files", "styles": ["icons"]},
{"name": "framer", "tags": ["brand", "design", "tool"], "category": "brands", "styles": ["icons"]},
{"name": "frown", "tags": ["sad", "unhappy", "emoji"], "category": "emoji", "styles": ["icons"]},
{"name": "gift", "tags": ["present", "reward", "surprise"], "category": "objects", "styles": ["icons"]},
{"name": "git-branch", "tags": ["version", "control", "code"], "category": "development", "styles": ["icons"]},
{"name": "git-commit", "tags": ["version", "control", "code"], "category": "development", "styles": ["icons"]},
{"name": "git-merge", "tags": ["version", "control", "code"], "category": "development", "styles": ["icons"]},
{"name": "git-pull-request", "tags": ["version", "control", "code"], "category": "development", "styles": ["icons"]},
{"name": "github", "tags": ["brand", "code", "repository"], "category": "brands", "styles": ["icons"]},
{"name": "gitlab", "tags": ["brand", "code", "repository"], "category": "brands", "styles": ["icons"]},
{"name": "globe", "tags": ["world", "earth", "web"], "category": "objects", "styles": ["icons"]},
{"name": "grid", "tags": ["layout", "squares", "dashboard"], "category": "layout", "styles": ["icons"]},
{"name": "hard-drive", "tags": ["storage", "disk", "data"], "category": "devices", "styles": ["icons"]},
{"name": "hash", "tags": ["tag", "hashtag", "number"], "category": "communication", "styles": ["icons"]},
{"name": "headphones", "tags": ["audio", "music", "listen"], "category": "devices", "styles": ["icons"]},
{"name": "heart", "tags": ["love", "favorite", "like"], "category": "objects", "styles": ["icons"]},
{"name": "help-circle", "tags": ["question", "help", "support"], "category": "actions", "styles": ["icons"]},
{"name": "hexagon", "tags": ["shape", "polygon", "6"], "category": "shapes", "styles": ["icons"]},
{"name": "home", "tags": ["house", "main", "dashboard"], "category": "navigation", "styles": ["icons"]},
{"name": "image", "tags": ["picture", "photo", "gallery"], "category": "media", "styles": ["icons"]},
{"name": "inbox", "tags": ["email", "messages", "mail"], "category": "communication", "styles": ["icons"]},
{"name": "info", "tags": ["information", "help", "about"], "category": "actions", "styles": ["icons"]},
{"name": "instagram", "tags": ["brand", "social", "photo"], "category": "brands", "styles": ["icons"]},
{"name": "italic", "tags": ["text", "format", "style"], "category": "text", "styles": ["icons"]},
{"name": "key", "tags": ["password", "security", "lock"], "category": "security", "styles": ["icons"]},
{"name": "layers", "tags": ["stack", "levels", "depth"], "category": "design", "styles": ["icons"]},
{"name": "layout", "tags": ["grid", "template", "design"], "category": "layout", "styles": ["icons"]},
{"name": "life-buoy", "tags": ["help", "support", "rescue"], "category": "objects", "styles": ["icons"]},
{"name": "link", "tags": ["url", "chain", "connect"], "category": "actions", "styles": ["icons"]},
{"name": "link-2", "tags": ["url", "chain", "connect"], "category": "actions", "styles": ["icons"]},
{"name": "linkedin", "tags": ["brand", "social", "professional"], "category": "brands", "styles": ["icons"]},
{"name": "list", "tags": ["menu", "items", "bullet"], "category": "text", "styles": ["icons"]},
{"name": "loader", "tags": ["loading", "spinner", "wait"], "category": "ui", "styles": ["icons"]},
{"name": "lock", "tags": ["security", "private", "password"], "category": "security", "styles": ["icons"]},
{"name": "log-in", "tags": ["login", "signin", "enter"], "category": "actions", "styles": ["icons"]},
{"name": "log-out", "tags": ["logout", "signout", "exit"], "category": "actions", "styles": ["icons"]},
{"name": "mail", "tags": ["email", "message", "envelope"], "category": "communication", "styles": ["icons"]},
{"name": "map", "tags": ["location", "navigation", "directions"], "category": "maps", "styles": ["icons"]},
{"name": "map-pin", "tags": ["location", "marker", "place"], "category": "maps", "styles": ["icons"]},
{"name": "maximize", "tags": ["fullscreen", "expand", "resize"], "category": "actions", "styles": ["icons"]},
{"name": "maximize-2", "tags": ["fullscreen", "expand", "resize"], "category": "actions", "styles": ["icons"]},
{"name": "meh", "tags": ["neutral", "emoji", "face"], "category": "emoji", "styles": ["icons"]},
{"name": "menu", "tags": ["hamburger", "navigation", "bars"], "category": "navigation", "styles": ["icons"]},
{"name": "message-circle", "tags": ["chat", "comment", "conversation"], "category": "communication", "styles": ["icons"]},
{"name": "message-square", "tags": ["chat", "comment", "conversation"], "category": "communication", "styles": ["icons"]},
{"name": "mic", "tags": ["microphone", "audio", "record"], "category": "media", "styles": ["icons"]},
{"name": "mic-off", "tags": ["microphone", "mute", "silent"], "category": "media", "styles": ["icons"]},
{"name": "minimize", "tags": ["collapse", "shrink", "resize"], "category": "actions", "styles": ["icons"]},
{"name": "minimize-2", "tags": ["collapse", "shrink", "resize"], "category": "actions", "styles": ["icons"]},
{"name": "minus", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["icons"]},
{"name": "minus-circle", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["icons"]},
{"name": "minus-square", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["icons"]},
{"name": "monitor", "tags": ["screen", "display", "desktop"], "category": "devices", "styles": ["icons"]},
{"name": "moon", "tags": ["dark", "night", "theme"], "category": "weather", "styles": ["icons"]},
{"name": "more-horizontal", "tags": ["menu", "options", "dots"], "category": "ui", "styles": ["icons"]},
{"name": "more-vertical", "tags": ["menu", "options", "dots"], "category": "ui", "styles": ["icons"]},
{"name": "mouse-pointer", "tags": ["cursor", "click", "select"], "category": "ui", "styles": ["icons"]},
{"name": "move", "tags": ["drag", "reorder", "arrows"], "category": "actions", "styles": ["icons"]},
{"name": "music", "tags": ["audio", "sound", "note"], "category": "media", "styles": ["icons"]},
{"name": "navigation", "tags": ["direction", "arrow", "location"], "category": "navigation", "styles": ["icons"]},
{"name": "navigation-2", "tags": ["direction", "arrow", "location"], "category": "navigation", "styles": ["icons"]},
{"name": "octagon", "tags": ["shape", "stop", "polygon"], "category": "shapes", "styles": ["icons"]},
{"name": "package", "tags": ["box", "delivery", "shipping"], "category": "objects", "styles": ["icons"]},
{"name": "paperclip", "tags": ["attachment", "file", "clip"], "category": "actions", "styles": ["icons"]},
{"name": "pause", "tags": ["media", "stop", "wait"], "category": "media", "styles": ["icons"]},
{"name": "pause-circle", "tags": ["media", "stop", "wait"], "category": "media", "styles": ["icons"]},
{"name": "pen-tool", "tags": ["design", "draw", "vector"], "category": "design", "styles": ["icons"]},
{"name": "percent", "tags": ["discount", "math", "percentage"], "category": "math", "styles": ["icons"]},
{"name": "phone", "tags": ["call", "contact", "mobile"], "category": "communication", "styles": ["icons"]},
{"name": "phone-call", "tags": ["call", "ringing", "incoming"], "category": "communication", "styles": ["icons"]},
{"name": "phone-forwarded", "tags": ["call", "forward", "redirect"], "category": "communication", "styles": ["icons"]},
{"name": "phone-incoming", "tags": ["call", "receive", "answer"], "category": "communication", "styles": ["icons"]},
{"name": "phone-missed", "tags": ["call", "missed", "unanswered"], "category": "communication", "styles": ["icons"]},
{"name": "phone-off", "tags": ["call", "hang up", "end"], "category": "communication", "styles": ["icons"]},
{"name": "phone-outgoing", "tags": ["call", "dial", "outbound"], "category": "communication", "styles": ["icons"]},
{"name": "pie-chart", "tags": ["analytics", "statistics", "data"], "category": "charts", "styles": ["icons"]},
{"name": "play", "tags": ["media", "start", "video"], "category": "media", "styles": ["icons"]},
{"name": "play-circle", "tags": ["media", "start", "video"], "category": "media", "styles": ["icons"]},
{"name": "plus", "tags": ["add", "create", "new"], "category": "math", "styles": ["icons"]},
{"name": "plus-circle", "tags": ["add", "create", "new"], "category": "math", "styles": ["icons"]},
{"name": "plus-square", "tags": ["add", "create", "new"], "category": "math", "styles": ["icons"]},
{"name": "pocket", "tags": ["save", "bookmark", "read later"], "category": "brands", "styles": ["icons"]},
{"name": "power", "tags": ["on", "off", "shutdown"], "category": "devices", "styles": ["icons"]},
{"name": "printer", "tags": ["print", "document", "paper"], "category": "devices", "styles": ["icons"]},
{"name": "radio", "tags": ["audio", "broadcast", "music"], "category": "media", "styles": ["icons"]},
{"name": "refresh-ccw", "tags": ["reload", "sync", "update"], "category": "actions", "styles": ["icons"]},
{"name": "refresh-cw", "tags": ["reload", "sync", "update"], "category": "actions", "styles": ["icons"]},
{"name": "repeat", "tags": ["loop", "replay", "refresh"], "category": "media", "styles": ["icons"]},
{"name": "rewind", "tags": ["media", "back", "previous"], "category": "media", "styles": ["icons"]},
{"name": "rotate-ccw", "tags": ["undo", "rotate", "turn"], "category": "actions", "styles": ["icons"]},
{"name": "rotate-cw", "tags": ["redo", "rotate", "turn"], "category": "actions", "styles": ["icons"]},
{"name": "rss", "tags": ["feed", "subscribe", "blog"], "category": "communication", "styles": ["icons"]},
{"name": "save", "tags": ["disk", "floppy", "store"], "category": "actions", "styles": ["icons"]},
{"name": "scissors", "tags": ["cut", "trim", "edit"], "category": "actions", "styles": ["icons"]},
{"name": "search", "tags": ["find", "magnify", "look"], "category": "actions", "styles": ["icons"]},
{"name": "send", "tags": ["message", "email", "submit"], "category": "communication", "styles": ["icons"]},
{"name": "server", "tags": ["hosting", "database", "backend"], "category": "devices", "styles": ["icons"]},
{"name": "settings", "tags": ["gear", "cog", "preferences"], "category": "ui", "styles": ["icons"]},
{"name": "share", "tags": ["social", "send", "forward"], "category": "actions", "styles": ["icons"]},
{"name": "share-2", "tags": ["social", "send", "network"], "category": "actions", "styles": ["icons"]},
{"name": "shield", "tags": ["security", "protection", "safe"], "category": "security", "styles": ["icons"]},
{"name": "shield-off", "tags": ["security", "unprotected", "unsafe"], "category": "security", "styles": ["icons"]},
{"name": "shopping-bag", "tags": ["shop", "purchase", "buy"], "category": "commerce", "styles": ["icons"]},
{"name": "shopping-cart", "tags": ["shop", "cart", "ecommerce"], "category": "commerce", "styles": ["icons"]},
{"name": "shuffle", "tags": ["random", "mix", "music"], "category": "media", "styles": ["icons"]},
{"name": "sidebar", "tags": ["layout", "panel", "navigation"], "category": "layout", "styles": ["icons"]},
{"name": "skip-back", "tags": ["media", "previous", "rewind"], "category": "media", "styles": ["icons"]},
{"name": "skip-forward", "tags": ["media", "next", "forward"], "category": "media", "styles": ["icons"]},
{"name": "slack", "tags": ["brand", "chat", "communication"], "category": "brands", "styles": ["icons"]},
{"name": "slash", "tags": ["ban", "cancel", "disabled"], "category": "ui", "styles": ["icons"]},
{"name": "sliders", "tags": ["settings", "controls", "adjust"], "category": "ui", "styles": ["icons"]},
{"name": "smartphone", "tags": ["phone", "mobile", "device"], "category": "devices", "styles": ["icons"]},
{"name": "smile", "tags": ["happy", "emoji", "face"], "category": "emoji", "styles": ["icons"]},
{"name": "speaker", "tags": ["audio", "sound", "volume"], "category": "media", "styles": ["icons"]},
{"name": "square", "tags": ["shape", "box", "rectangle"], "category": "shapes", "styles": ["icons"]},
{"name": "star", "tags": ["favorite", "rating", "bookmark"], "category": "objects", "styles": ["icons"]},
{"name": "stop-circle", "tags": ["media", "stop", "end"], "category": "media", "styles": ["icons"]},
{"name": "sun", "tags": ["light", "day", "brightness"], "category": "weather", "styles": ["icons"]},
{"name": "sunrise", "tags": ["morning", "dawn", "sun"], "category": "weather", "styles": ["icons"]},
{"name": "sunset", "tags": ["evening", "dusk", "sun"], "category": "weather", "styles": ["icons"]},
{"name": "tablet", "tags": ["device", "ipad", "screen"], "category": "devices", "styles": ["icons"]},
{"name": "tag", "tags": ["label", "category", "price"], "category": "commerce", "styles": ["icons"]},
{"name": "target", "tags": ["goal", "aim", "focus"], "category": "ui", "styles": ["icons"]},
{"name": "terminal", "tags": ["console", "command", "code"], "category": "development", "styles": ["icons"]},
{"name": "thermometer", "tags": ["temperature", "weather", "health"], "category": "weather", "styles": ["icons"]},
{"name": "thumbs-down", "tags": ["dislike", "bad", "negative"], "category": "actions", "styles": ["icons"]},
{"name": "thumbs-up", "tags": ["like", "good", "positive"], "category": "actions", "styles": ["icons"]},
{"name": "toggle-left", "tags": ["switch", "off", "disable"], "category": "ui", "styles": ["icons"]},
{"name": "toggle-right", "tags": ["switch", "on", "enable"], "category": "ui", "styles": ["icons"]},
{"name": "tool", "tags": ["wrench", "settings", "repair"], "category": "objects", "styles": ["icons"]},
{"name": "trash", "tags": ["delete", "remove", "bin"], "category": "actions", "styles": ["icons"]},
{"name": "trash-2", "tags": ["delete", "remove", "bin"], "category": "actions", "styles": ["icons"]},
{"name": "trello", "tags": ["brand", "project", "kanban"], "category": "brands", "styles": ["icons"]},
{"name": "trending-down", "tags": ["chart", "decrease", "analytics"], "category": "charts", "styles": ["icons"]},
{"name": "trending-up", "tags": ["chart", "increase", "analytics"], "category": "charts", "styles": ["icons"]},
{"name": "triangle", "tags": ["shape", "polygon", "warning"], "category": "shapes", "styles": ["icons"]},
{"name": "truck", "tags": ["delivery", "shipping", "transport"], "category": "objects", "styles": ["icons"]},
{"name": "tv", "tags": ["television", "screen", "display"], "category": "devices", "styles": ["icons"]},
{"name": "twitch", "tags": ["brand", "streaming", "gaming"], "category": "brands", "styles": ["icons"]},
{"name": "twitter", "tags": ["brand", "social", "network"], "category": "brands", "styles": ["icons"]},
{"name": "type", "tags": ["text", "font", "typography"], "category": "text", "styles": ["icons"]},
{"name": "umbrella", "tags": ["weather", "rain", "protection"], "category": "weather", "styles": ["icons"]},
{"name": "underline", "tags": ["text", "format", "style"], "category": "text", "styles": ["icons"]},
{"name": "unlock", "tags": ["security", "open", "access"], "category": "security", "styles": ["icons"]},
{"name": "upload", "tags": ["export", "send", "share"], "category": "actions", "styles": ["icons"]},
{"name": "upload-cloud", "tags": ["export", "cloud", "share"], "category": "actions", "styles": ["icons"]},
{"name": "user", "tags": ["person", "account", "profile"], "category": "users", "styles": ["icons"]},
{"name": "user-check", "tags": ["person", "verified", "approved"], "category": "users", "styles": ["icons"]},
{"name": "user-minus", "tags": ["person", "remove", "unfriend"], "category": "users", "styles": ["icons"]},
{"name": "user-plus", "tags": ["person", "add", "invite"], "category": "users", "styles": ["icons"]},
{"name": "user-x", "tags": ["person", "delete", "remove"], "category": "users", "styles": ["icons"]},
{"name": "users", "tags": ["people", "team", "group"], "category": "users", "styles": ["icons"]},
{"name": "video", "tags": ["camera", "film", "record"], "category": "media", "styles": ["icons"]},
{"name": "video-off", "tags": ["camera", "disabled", "mute"], "category": "media", "styles": ["icons"]},
{"name": "voicemail", "tags": ["message", "audio", "phone"], "category": "communication", "styles": ["icons"]},
{"name": "volume", "tags": ["sound", "audio", "speaker"], "category": "media", "styles": ["icons"]},
{"name": "volume-1", "tags": ["sound", "audio", "low"], "category": "media", "styles": ["icons"]},
{"name": "volume-2", "tags": ["sound", "audio", "high"], "category": "media", "styles": ["icons"]},
{"name": "volume-x", "tags": ["mute", "silent", "no sound"], "category": "media", "styles": ["icons"]},
{"name": "watch", "tags": ["time", "wearable", "clock"], "category": "devices", "styles": ["icons"]},
{"name": "wifi", "tags": ["wireless", "internet", "connection"], "category": "connectivity", "styles": ["icons"]},
{"name": "wifi-off", "tags": ["wireless", "offline", "disconnect"], "category": "connectivity", "styles": ["icons"]},
{"name": "wind", "tags": ["weather", "air", "breeze"], "category": "weather", "styles": ["icons"]},
{"name": "x", "tags": ["close", "cancel", "remove"], "category": "actions", "styles": ["icons"]},
{"name": "x-circle", "tags": ["close", "cancel", "error"], "category": "actions", "styles": ["icons"]},
{"name": "x-octagon", "tags": ["close", "stop", "error"], "category": "actions", "styles": ["icons"]},
{"name": "x-square", "tags": ["close", "cancel", "remove"], "category": "actions", "styles": ["icons"]},
{"name": "youtube", "tags": ["brand", "video", "streaming"], "category": "brands", "styles": ["icons"]},
{"name": "zap", "tags": ["lightning", "power", "energy"], "category": "objects", "styles": ["icons"]},
{"name": "zap-off", "tags": ["lightning", "disabled", "off"], "category": "objects", "styles": ["icons"]},
{"name": "zoom-in", "tags": ["magnify", "enlarge", "plus"], "category": "actions", "styles": ["icons"]},
{"name": "zoom-out", "tags": ["magnify", "shrink", "minus"], "category": "actions", "styles": ["icons"]}
]
}

View file

@ -1,295 +0,0 @@
{
"slug": "heroicons",
"name": "Heroicons",
"version": "2.1.1",
"icons": [
{"name": "academic-cap", "tags": ["education", "graduation", "school", "hat", "learning"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "adjustments-horizontal", "tags": ["settings", "controls", "sliders", "options", "configuration"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "adjustments-vertical", "tags": ["settings", "controls", "sliders", "options", "configuration"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "archive-box", "tags": ["storage", "box", "container", "archive"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "archive-box-arrow-down", "tags": ["download", "storage", "archive", "save"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "archive-box-x-mark", "tags": ["delete", "remove", "archive", "cancel"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-down", "tags": ["direction", "navigation", "down", "move"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-down-circle", "tags": ["direction", "navigation", "down", "circle"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-down-left", "tags": ["direction", "navigation", "diagonal"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-down-on-square", "tags": ["download", "save", "import"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-down-on-square-stack", "tags": ["download", "save", "multiple"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-down-right", "tags": ["direction", "navigation", "diagonal"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-down-tray", "tags": ["download", "save", "import"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-left", "tags": ["direction", "navigation", "back", "previous"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-left-circle", "tags": ["direction", "navigation", "back", "circle"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-left-end-on-rectangle", "tags": ["logout", "exit", "leave"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-left-start-on-rectangle", "tags": ["login", "enter", "signin"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-long-down", "tags": ["direction", "navigation", "long"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-long-left", "tags": ["direction", "navigation", "long", "back"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-long-right", "tags": ["direction", "navigation", "long", "forward"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-long-up", "tags": ["direction", "navigation", "long"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-path", "tags": ["refresh", "reload", "sync", "repeat"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-path-rounded-square", "tags": ["refresh", "reload", "sync"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-right", "tags": ["direction", "navigation", "forward", "next"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-right-circle", "tags": ["direction", "navigation", "forward", "circle"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-right-end-on-rectangle", "tags": ["login", "enter", "signin"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-right-start-on-rectangle", "tags": ["logout", "exit", "leave"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-top-right-on-square", "tags": ["external", "link", "open", "new window"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-trending-down", "tags": ["chart", "decrease", "analytics", "down"], "category": "charts", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-trending-up", "tags": ["chart", "increase", "analytics", "up"], "category": "charts", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-up", "tags": ["direction", "navigation", "up", "move"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-up-circle", "tags": ["direction", "navigation", "up", "circle"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-up-left", "tags": ["direction", "navigation", "diagonal"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-up-on-square", "tags": ["upload", "share", "export"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-up-on-square-stack", "tags": ["upload", "share", "multiple"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-up-right", "tags": ["direction", "navigation", "diagonal"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-up-tray", "tags": ["upload", "export", "share"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-uturn-down", "tags": ["undo", "return", "back"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-uturn-left", "tags": ["undo", "return", "back"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-uturn-right", "tags": ["redo", "forward"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrow-uturn-up", "tags": ["return", "back"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrows-pointing-in", "tags": ["minimize", "collapse", "shrink"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrows-pointing-out", "tags": ["maximize", "expand", "fullscreen"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "arrows-right-left", "tags": ["swap", "exchange", "transfer"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "arrows-up-down", "tags": ["sort", "reorder", "move"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "at-symbol", "tags": ["email", "at", "mention", "contact"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "backspace", "tags": ["delete", "remove", "erase", "keyboard"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "backward", "tags": ["media", "rewind", "previous"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "banknotes", "tags": ["money", "cash", "payment", "currency"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "bars-2", "tags": ["menu", "hamburger", "navigation"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "bars-3", "tags": ["menu", "hamburger", "navigation"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "bars-3-bottom-left", "tags": ["menu", "text", "align left"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "bars-3-bottom-right", "tags": ["menu", "text", "align right"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "bars-3-center-left", "tags": ["menu", "text", "align"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "bars-4", "tags": ["menu", "list", "navigation"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "bars-arrow-down", "tags": ["sort", "descending", "order"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "bars-arrow-up", "tags": ["sort", "ascending", "order"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "battery-0", "tags": ["power", "energy", "empty"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "battery-50", "tags": ["power", "energy", "half"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "battery-100", "tags": ["power", "energy", "full"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "beaker", "tags": ["science", "lab", "chemistry", "experiment"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "bell", "tags": ["notification", "alert", "alarm", "ring"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "bell-alert", "tags": ["notification", "alert", "alarm", "warning"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "bell-slash", "tags": ["notification", "mute", "silent", "off"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "bell-snooze", "tags": ["notification", "snooze", "delay", "later"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "bolt", "tags": ["lightning", "power", "energy", "electric"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "bolt-slash", "tags": ["lightning", "power", "off", "disabled"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "book-open", "tags": ["read", "education", "learning", "library"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "bookmark", "tags": ["save", "favorite", "tag", "mark"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "bookmark-slash", "tags": ["unsave", "remove", "unbookmark"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "bookmark-square", "tags": ["save", "favorite", "square"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "briefcase", "tags": ["work", "job", "business", "career"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "bug-ant", "tags": ["bug", "debug", "error", "insect"], "category": "development", "styles": ["outline", "solid", "mini"]},
{"name": "building-library", "tags": ["library", "institution", "government"], "category": "buildings", "styles": ["outline", "solid", "mini"]},
{"name": "building-office", "tags": ["office", "work", "business", "company"], "category": "buildings", "styles": ["outline", "solid", "mini"]},
{"name": "building-office-2", "tags": ["office", "skyscraper", "business"], "category": "buildings", "styles": ["outline", "solid", "mini"]},
{"name": "building-storefront", "tags": ["shop", "store", "retail", "commerce"], "category": "buildings", "styles": ["outline", "solid", "mini"]},
{"name": "cake", "tags": ["birthday", "celebration", "party", "dessert"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "calculator", "tags": ["math", "calculate", "numbers", "finance"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "calendar", "tags": ["date", "schedule", "event", "time"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "calendar-days", "tags": ["date", "schedule", "month", "planner"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "camera", "tags": ["photo", "picture", "image", "photography"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "chart-bar", "tags": ["analytics", "statistics", "graph", "data"], "category": "charts", "styles": ["outline", "solid", "mini"]},
{"name": "chart-bar-square", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["outline", "solid", "mini"]},
{"name": "chart-pie", "tags": ["analytics", "statistics", "pie chart", "data"], "category": "charts", "styles": ["outline", "solid", "mini"]},
{"name": "chat-bubble-bottom-center", "tags": ["message", "chat", "comment", "conversation"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "chat-bubble-bottom-center-text", "tags": ["message", "chat", "text", "comment"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "chat-bubble-left", "tags": ["message", "chat", "comment"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "chat-bubble-left-ellipsis", "tags": ["message", "typing", "chat"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "chat-bubble-left-right", "tags": ["conversation", "chat", "discuss"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "chat-bubble-oval-left", "tags": ["message", "chat", "bubble"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "chat-bubble-oval-left-ellipsis", "tags": ["message", "typing", "chat"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "check", "tags": ["done", "complete", "success", "tick"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "check-badge", "tags": ["verified", "approved", "certified"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "check-circle", "tags": ["done", "complete", "success", "approved"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-double-down", "tags": ["expand", "more", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-double-left", "tags": ["previous", "back", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-double-right", "tags": ["next", "forward", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-double-up", "tags": ["collapse", "less", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-down", "tags": ["expand", "dropdown", "more"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-left", "tags": ["previous", "back", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-right", "tags": ["next", "forward", "navigation"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-up", "tags": ["collapse", "less", "close"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "chevron-up-down", "tags": ["sort", "select", "dropdown"], "category": "arrows", "styles": ["outline", "solid", "mini"]},
{"name": "circle-stack", "tags": ["database", "storage", "data", "layers"], "category": "development", "styles": ["outline", "solid", "mini"]},
{"name": "clipboard", "tags": ["copy", "paste", "clipboard"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "clipboard-document", "tags": ["copy", "document", "paste"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "clipboard-document-check", "tags": ["copy", "done", "verified"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "clipboard-document-list", "tags": ["list", "tasks", "checklist"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "clock", "tags": ["time", "schedule", "watch", "hour"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "cloud", "tags": ["weather", "storage", "sky", "upload"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "cloud-arrow-down", "tags": ["download", "cloud", "sync"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "cloud-arrow-up", "tags": ["upload", "cloud", "sync"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "code-bracket", "tags": ["code", "development", "programming"], "category": "development", "styles": ["outline", "solid", "mini"]},
{"name": "code-bracket-square", "tags": ["code", "development", "programming"], "category": "development", "styles": ["outline", "solid", "mini"]},
{"name": "cog", "tags": ["settings", "gear", "configuration", "options"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "cog-6-tooth", "tags": ["settings", "gear", "configuration"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "cog-8-tooth", "tags": ["settings", "gear", "configuration"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "command-line", "tags": ["terminal", "console", "cli", "code"], "category": "development", "styles": ["outline", "solid", "mini"]},
{"name": "computer-desktop", "tags": ["desktop", "monitor", "screen", "pc"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "cpu-chip", "tags": ["processor", "chip", "hardware", "computer"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "credit-card", "tags": ["payment", "card", "money", "finance"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "cube", "tags": ["3d", "box", "shape", "object"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "cube-transparent", "tags": ["3d", "box", "transparent"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "currency-bangladeshi", "tags": ["money", "currency", "taka"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "currency-dollar", "tags": ["money", "currency", "usd", "dollar"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "currency-euro", "tags": ["money", "currency", "eur", "euro"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "currency-pound", "tags": ["money", "currency", "gbp", "pound"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "currency-rupee", "tags": ["money", "currency", "inr", "rupee"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "currency-yen", "tags": ["money", "currency", "jpy", "yen"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "cursor-arrow-rays", "tags": ["cursor", "click", "pointer"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "cursor-arrow-ripple", "tags": ["cursor", "click", "pointer"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "device-phone-mobile", "tags": ["phone", "mobile", "smartphone"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "device-tablet", "tags": ["tablet", "ipad", "device"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "document", "tags": ["file", "document", "page", "paper"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-arrow-down", "tags": ["download", "file", "document"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-arrow-up", "tags": ["upload", "file", "document"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-chart-bar", "tags": ["report", "analytics", "document"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-check", "tags": ["verified", "approved", "document"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-duplicate", "tags": ["copy", "duplicate", "clone"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-magnifying-glass", "tags": ["search", "find", "document"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-minus", "tags": ["remove", "delete", "document"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-plus", "tags": ["add", "new", "create", "document"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "document-text", "tags": ["file", "text", "document", "content"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "ellipsis-horizontal", "tags": ["more", "menu", "options", "dots"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "ellipsis-horizontal-circle", "tags": ["more", "menu", "options"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "ellipsis-vertical", "tags": ["more", "menu", "options", "dots"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "envelope", "tags": ["email", "mail", "message", "letter"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "envelope-open", "tags": ["email", "mail", "read", "open"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "exclamation-circle", "tags": ["warning", "alert", "error", "danger"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "exclamation-triangle", "tags": ["warning", "alert", "caution", "danger"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "eye", "tags": ["view", "visible", "show", "watch"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "eye-dropper", "tags": ["color", "picker", "dropper", "design"], "category": "design", "styles": ["outline", "solid", "mini"]},
{"name": "eye-slash", "tags": ["hide", "invisible", "hidden", "privacy"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "face-frown", "tags": ["sad", "unhappy", "emoji", "emotion"], "category": "emoji", "styles": ["outline", "solid", "mini"]},
{"name": "face-smile", "tags": ["happy", "smile", "emoji", "emotion"], "category": "emoji", "styles": ["outline", "solid", "mini"]},
{"name": "film", "tags": ["video", "movie", "media", "cinema"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "finger-print", "tags": ["security", "identity", "biometric", "auth"], "category": "security", "styles": ["outline", "solid", "mini"]},
{"name": "fire", "tags": ["flame", "hot", "trending", "popular"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "flag", "tags": ["report", "mark", "flag", "bookmark"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "folder", "tags": ["directory", "folder", "files", "organize"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "folder-arrow-down", "tags": ["download", "folder", "save"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "folder-minus", "tags": ["remove", "folder", "delete"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "folder-open", "tags": ["open", "folder", "browse"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "folder-plus", "tags": ["add", "folder", "new", "create"], "category": "files", "styles": ["outline", "solid", "mini"]},
{"name": "forward", "tags": ["media", "next", "skip"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "funnel", "tags": ["filter", "sort", "funnel"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "gif", "tags": ["image", "animation", "gif", "media"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "gift", "tags": ["present", "gift", "reward", "surprise"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "gift-top", "tags": ["present", "gift", "reward"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "globe-alt", "tags": ["world", "earth", "internet", "web"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "globe-americas", "tags": ["world", "earth", "americas"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "globe-asia-australia", "tags": ["world", "earth", "asia"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "globe-europe-africa", "tags": ["world", "earth", "europe"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "hand-raised", "tags": ["stop", "hand", "halt", "wait"], "category": "gestures", "styles": ["outline", "solid", "mini"]},
{"name": "hand-thumb-down", "tags": ["dislike", "thumbs down", "bad"], "category": "gestures", "styles": ["outline", "solid", "mini"]},
{"name": "hand-thumb-up", "tags": ["like", "thumbs up", "good", "approve"], "category": "gestures", "styles": ["outline", "solid", "mini"]},
{"name": "hashtag", "tags": ["tag", "hashtag", "topic", "social"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "heart", "tags": ["love", "favorite", "like", "heart"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "home", "tags": ["house", "home", "main", "dashboard"], "category": "navigation", "styles": ["outline", "solid", "mini"]},
{"name": "home-modern", "tags": ["house", "home", "building"], "category": "buildings", "styles": ["outline", "solid", "mini"]},
{"name": "identification", "tags": ["id", "card", "identity", "badge"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "inbox", "tags": ["mail", "inbox", "messages"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "inbox-arrow-down", "tags": ["receive", "inbox", "download"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "inbox-stack", "tags": ["inbox", "messages", "stack"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "information-circle", "tags": ["info", "help", "about", "details"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "key", "tags": ["password", "security", "access", "lock"], "category": "security", "styles": ["outline", "solid", "mini"]},
{"name": "language", "tags": ["translate", "language", "international"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "lifebuoy", "tags": ["help", "support", "rescue", "assistance"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "light-bulb", "tags": ["idea", "light", "bulb", "creative"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "link", "tags": ["url", "link", "chain", "connect"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "list-bullet", "tags": ["list", "bullet", "items", "menu"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "lock-closed", "tags": ["security", "lock", "private", "protected"], "category": "security", "styles": ["outline", "solid", "mini"]},
{"name": "lock-open", "tags": ["unlock", "open", "access"], "category": "security", "styles": ["outline", "solid", "mini"]},
{"name": "magnifying-glass", "tags": ["search", "find", "zoom", "look"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "magnifying-glass-circle", "tags": ["search", "find", "explore"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "magnifying-glass-minus", "tags": ["zoom out", "search", "minus"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "magnifying-glass-plus", "tags": ["zoom in", "search", "plus"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "map", "tags": ["location", "map", "directions", "navigate"], "category": "maps", "styles": ["outline", "solid", "mini"]},
{"name": "map-pin", "tags": ["location", "pin", "marker", "place"], "category": "maps", "styles": ["outline", "solid", "mini"]},
{"name": "megaphone", "tags": ["announce", "broadcast", "marketing"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "microphone", "tags": ["audio", "voice", "record", "speak"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "minus", "tags": ["subtract", "remove", "minus", "decrease"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "minus-circle", "tags": ["remove", "delete", "minus"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "moon", "tags": ["dark", "night", "mode", "theme"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "musical-note", "tags": ["music", "audio", "sound", "note"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "newspaper", "tags": ["news", "article", "blog", "press"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "no-symbol", "tags": ["ban", "prohibited", "forbidden", "block"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "paint-brush", "tags": ["design", "art", "paint", "brush"], "category": "design", "styles": ["outline", "solid", "mini"]},
{"name": "paper-airplane", "tags": ["send", "message", "email", "submit"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "paper-clip", "tags": ["attachment", "clip", "attach", "file"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "pause", "tags": ["media", "pause", "stop", "wait"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "pause-circle", "tags": ["media", "pause", "stop"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "pencil", "tags": ["edit", "write", "pencil", "modify"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "pencil-square", "tags": ["edit", "write", "compose"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "phone", "tags": ["call", "phone", "contact", "mobile"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "phone-arrow-down-left", "tags": ["incoming", "call", "receive"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "phone-arrow-up-right", "tags": ["outgoing", "call", "dial"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "phone-x-mark", "tags": ["hang up", "end call", "decline"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "photo", "tags": ["image", "picture", "photo", "gallery"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "play", "tags": ["media", "play", "start", "video"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "play-circle", "tags": ["media", "play", "video"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "play-pause", "tags": ["media", "toggle", "play", "pause"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "plus", "tags": ["add", "create", "new", "plus"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "plus-circle", "tags": ["add", "create", "new"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "power", "tags": ["on", "off", "power", "shutdown"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "presentation-chart-bar", "tags": ["presentation", "chart", "analytics"], "category": "charts", "styles": ["outline", "solid", "mini"]},
{"name": "presentation-chart-line", "tags": ["presentation", "chart", "analytics"], "category": "charts", "styles": ["outline", "solid", "mini"]},
{"name": "printer", "tags": ["print", "printer", "document"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "puzzle-piece", "tags": ["plugin", "extension", "puzzle", "addon"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "qr-code", "tags": ["qr", "code", "scan", "barcode"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "question-mark-circle", "tags": ["help", "question", "support", "faq"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "queue-list", "tags": ["list", "queue", "playlist"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "radio", "tags": ["radio", "audio", "broadcast"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "receipt-percent", "tags": ["discount", "sale", "coupon"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "receipt-refund", "tags": ["refund", "return", "money back"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "rectangle-group", "tags": ["layout", "grid", "components"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "rectangle-stack", "tags": ["layers", "stack", "cards"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "rocket-launch", "tags": ["launch", "startup", "rocket", "deploy"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "rss", "tags": ["feed", "rss", "subscribe", "blog"], "category": "communication", "styles": ["outline", "solid", "mini"]},
{"name": "scale", "tags": ["balance", "justice", "legal", "weight"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "scissors", "tags": ["cut", "scissors", "trim", "edit"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "server", "tags": ["server", "hosting", "database", "backend"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "server-stack", "tags": ["servers", "hosting", "infrastructure"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "share", "tags": ["share", "send", "social", "forward"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "shield-check", "tags": ["security", "protected", "verified", "safe"], "category": "security", "styles": ["outline", "solid", "mini"]},
{"name": "shield-exclamation", "tags": ["security", "warning", "alert"], "category": "security", "styles": ["outline", "solid", "mini"]},
{"name": "shopping-bag", "tags": ["shop", "bag", "purchase", "buy"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "shopping-cart", "tags": ["cart", "shop", "ecommerce", "buy"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "signal", "tags": ["wifi", "signal", "connection", "network"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "signal-slash", "tags": ["no signal", "offline", "disconnected"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "sparkles", "tags": ["magic", "sparkle", "new", "ai"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "speaker-wave", "tags": ["audio", "sound", "volume", "speaker"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "speaker-x-mark", "tags": ["mute", "silent", "no sound"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "square-2-stack", "tags": ["copy", "duplicate", "stack"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "square-3-stack-3d", "tags": ["layers", "3d", "stack"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "squares-2x2", "tags": ["grid", "layout", "dashboard"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "squares-plus", "tags": ["add", "widget", "new"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "star", "tags": ["favorite", "star", "rating", "bookmark"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "stop", "tags": ["media", "stop", "end"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "stop-circle", "tags": ["media", "stop", "end"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "sun", "tags": ["light", "day", "bright", "theme"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "swatch", "tags": ["color", "palette", "design", "theme"], "category": "design", "styles": ["outline", "solid", "mini"]},
{"name": "table-cells", "tags": ["table", "grid", "spreadsheet", "data"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "tag", "tags": ["label", "tag", "category", "price"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "ticket", "tags": ["ticket", "event", "pass", "coupon"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "trash", "tags": ["delete", "remove", "trash", "bin"], "category": "actions", "styles": ["outline", "solid", "mini"]},
{"name": "trophy", "tags": ["award", "winner", "achievement", "prize"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "truck", "tags": ["delivery", "shipping", "truck", "transport"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "tv", "tags": ["television", "tv", "screen", "display"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "user", "tags": ["person", "user", "account", "profile"], "category": "users", "styles": ["outline", "solid", "mini"]},
{"name": "user-circle", "tags": ["avatar", "profile", "account"], "category": "users", "styles": ["outline", "solid", "mini"]},
{"name": "user-group", "tags": ["team", "group", "users", "people"], "category": "users", "styles": ["outline", "solid", "mini"]},
{"name": "user-minus", "tags": ["remove user", "unfriend", "delete"], "category": "users", "styles": ["outline", "solid", "mini"]},
{"name": "user-plus", "tags": ["add user", "invite", "new user"], "category": "users", "styles": ["outline", "solid", "mini"]},
{"name": "users", "tags": ["people", "team", "community"], "category": "users", "styles": ["outline", "solid", "mini"]},
{"name": "variable", "tags": ["code", "variable", "math", "programming"], "category": "development", "styles": ["outline", "solid", "mini"]},
{"name": "video-camera", "tags": ["video", "camera", "record", "film"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "video-camera-slash", "tags": ["video off", "camera off", "mute"], "category": "media", "styles": ["outline", "solid", "mini"]},
{"name": "view-columns", "tags": ["columns", "layout", "grid"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "view-finder-circle", "tags": ["focus", "target", "aim"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "wallet", "tags": ["money", "payment", "wallet", "finance"], "category": "commerce", "styles": ["outline", "solid", "mini"]},
{"name": "wifi", "tags": ["wireless", "internet", "connection", "network"], "category": "devices", "styles": ["outline", "solid", "mini"]},
{"name": "window", "tags": ["browser", "window", "app"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "wrench", "tags": ["tools", "settings", "repair", "fix"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "wrench-screwdriver", "tags": ["tools", "settings", "repair"], "category": "objects", "styles": ["outline", "solid", "mini"]},
{"name": "x-circle", "tags": ["close", "cancel", "remove", "delete"], "category": "ui", "styles": ["outline", "solid", "mini"]},
{"name": "x-mark", "tags": ["close", "cancel", "remove", "x"], "category": "ui", "styles": ["outline", "solid", "mini"]}
]
}

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

View file

@ -1,357 +0,0 @@
{
"slug": "lucide",
"name": "Lucide",
"version": "0.303.0",
"icons": [
{"name": "accessibility", "tags": ["wheelchair", "disabled", "handicap"], "category": "accessibility", "styles": ["icons"]},
{"name": "activity", "tags": ["pulse", "health", "heart"], "category": "medical", "styles": ["icons"]},
{"name": "air-vent", "tags": ["ventilation", "hvac", "climate"], "category": "home", "styles": ["icons"]},
{"name": "airplay", "tags": ["stream", "cast", "screen"], "category": "devices", "styles": ["icons"]},
{"name": "alarm-clock", "tags": ["time", "wake", "alert"], "category": "time", "styles": ["icons"]},
{"name": "album", "tags": ["music", "photos", "collection"], "category": "media", "styles": ["icons"]},
{"name": "alert-circle", "tags": ["warning", "error", "danger"], "category": "alerts", "styles": ["icons"]},
{"name": "alert-octagon", "tags": ["warning", "stop", "error"], "category": "alerts", "styles": ["icons"]},
{"name": "alert-triangle", "tags": ["warning", "caution", "error"], "category": "alerts", "styles": ["icons"]},
{"name": "align-center", "tags": ["text", "format", "paragraph"], "category": "text", "styles": ["icons"]},
{"name": "align-center-horizontal", "tags": ["layout", "distribute"], "category": "layout", "styles": ["icons"]},
{"name": "align-center-vertical", "tags": ["layout", "distribute"], "category": "layout", "styles": ["icons"]},
{"name": "align-end-horizontal", "tags": ["layout", "distribute"], "category": "layout", "styles": ["icons"]},
{"name": "align-end-vertical", "tags": ["layout", "distribute"], "category": "layout", "styles": ["icons"]},
{"name": "align-horizontal-distribute-center", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-horizontal-distribute-end", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-horizontal-distribute-start", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-horizontal-justify-center", "tags": ["layout", "justify"], "category": "layout", "styles": ["icons"]},
{"name": "align-horizontal-justify-end", "tags": ["layout", "justify"], "category": "layout", "styles": ["icons"]},
{"name": "align-horizontal-justify-start", "tags": ["layout", "justify"], "category": "layout", "styles": ["icons"]},
{"name": "align-horizontal-space-around", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-horizontal-space-between", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-justify", "tags": ["text", "format", "paragraph"], "category": "text", "styles": ["icons"]},
{"name": "align-left", "tags": ["text", "format", "paragraph"], "category": "text", "styles": ["icons"]},
{"name": "align-right", "tags": ["text", "format", "paragraph"], "category": "text", "styles": ["icons"]},
{"name": "align-start-horizontal", "tags": ["layout", "distribute"], "category": "layout", "styles": ["icons"]},
{"name": "align-start-vertical", "tags": ["layout", "distribute"], "category": "layout", "styles": ["icons"]},
{"name": "align-vertical-distribute-center", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-vertical-distribute-end", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-vertical-distribute-start", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-vertical-justify-center", "tags": ["layout", "justify"], "category": "layout", "styles": ["icons"]},
{"name": "align-vertical-justify-end", "tags": ["layout", "justify"], "category": "layout", "styles": ["icons"]},
{"name": "align-vertical-justify-start", "tags": ["layout", "justify"], "category": "layout", "styles": ["icons"]},
{"name": "align-vertical-space-around", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "align-vertical-space-between", "tags": ["layout", "spacing"], "category": "layout", "styles": ["icons"]},
{"name": "anchor", "tags": ["link", "marine", "ship"], "category": "navigation", "styles": ["icons"]},
{"name": "angry", "tags": ["emoji", "emotion", "face"], "category": "emoji", "styles": ["icons"]},
{"name": "annoyed", "tags": ["emoji", "emotion", "face"], "category": "emoji", "styles": ["icons"]},
{"name": "aperture", "tags": ["camera", "lens", "photo"], "category": "photography", "styles": ["icons"]},
{"name": "app-window", "tags": ["application", "program", "software"], "category": "development", "styles": ["icons"]},
{"name": "apple", "tags": ["fruit", "food", "healthy"], "category": "food", "styles": ["icons"]},
{"name": "archive", "tags": ["box", "storage", "files"], "category": "files", "styles": ["icons"]},
{"name": "archive-restore", "tags": ["unarchive", "restore", "files"], "category": "files", "styles": ["icons"]},
{"name": "archive-x", "tags": ["delete", "remove", "files"], "category": "files", "styles": ["icons"]},
{"name": "armchair", "tags": ["furniture", "seat", "chair"], "category": "furniture", "styles": ["icons"]},
{"name": "arrow-big-down", "tags": ["direction", "down", "navigation"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-big-down-dash", "tags": ["direction", "down", "end"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-big-left", "tags": ["direction", "left", "back"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-big-left-dash", "tags": ["direction", "left", "start"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-big-right", "tags": ["direction", "right", "forward"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-big-right-dash", "tags": ["direction", "right", "end"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-big-up", "tags": ["direction", "up", "navigation"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-big-up-dash", "tags": ["direction", "up", "start"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down", "tags": ["direction", "down"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-circle", "tags": ["direction", "down", "circle"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-from-line", "tags": ["download", "export"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-left", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-left-from-circle", "tags": ["direction", "exit"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-right", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-right-from-circle", "tags": ["direction", "exit"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-to-dot", "tags": ["direction", "target"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-down-to-line", "tags": ["download", "bottom"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-left", "tags": ["direction", "back", "previous"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-left-circle", "tags": ["direction", "back", "circle"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-left-from-line", "tags": ["exit", "leave"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-left-right", "tags": ["swap", "exchange", "horizontal"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-left-to-line", "tags": ["start", "beginning"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-right", "tags": ["direction", "forward", "next"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-right-circle", "tags": ["direction", "forward", "circle"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-right-from-line", "tags": ["exit", "export"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-right-to-line", "tags": ["end", "finish"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up", "tags": ["direction", "up"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-circle", "tags": ["direction", "up", "circle"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-down", "tags": ["sort", "reorder", "vertical"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-from-dot", "tags": ["direction", "source"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-from-line", "tags": ["upload", "export"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-left", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-right", "tags": ["direction", "diagonal", "external"], "category": "arrows", "styles": ["icons"]},
{"name": "arrow-up-to-line", "tags": ["upload", "top"], "category": "arrows", "styles": ["icons"]},
{"name": "asterisk", "tags": ["star", "required", "note"], "category": "symbols", "styles": ["icons"]},
{"name": "at-sign", "tags": ["email", "mention", "at"], "category": "communication", "styles": ["icons"]},
{"name": "atom", "tags": ["science", "physics", "nuclear"], "category": "science", "styles": ["icons"]},
{"name": "award", "tags": ["achievement", "badge", "medal"], "category": "awards", "styles": ["icons"]},
{"name": "axe", "tags": ["tool", "chop", "wood"], "category": "tools", "styles": ["icons"]},
{"name": "baby", "tags": ["child", "infant", "kid"], "category": "people", "styles": ["icons"]},
{"name": "backpack", "tags": ["bag", "school", "travel"], "category": "objects", "styles": ["icons"]},
{"name": "badge", "tags": ["label", "tag", "verified"], "category": "ui", "styles": ["icons"]},
{"name": "badge-alert", "tags": ["warning", "notification"], "category": "ui", "styles": ["icons"]},
{"name": "badge-check", "tags": ["verified", "approved", "done"], "category": "ui", "styles": ["icons"]},
{"name": "badge-dollar-sign", "tags": ["money", "price", "cost"], "category": "commerce", "styles": ["icons"]},
{"name": "badge-help", "tags": ["question", "support", "info"], "category": "ui", "styles": ["icons"]},
{"name": "badge-info", "tags": ["information", "details", "about"], "category": "ui", "styles": ["icons"]},
{"name": "badge-minus", "tags": ["remove", "subtract", "delete"], "category": "ui", "styles": ["icons"]},
{"name": "badge-percent", "tags": ["discount", "sale", "offer"], "category": "commerce", "styles": ["icons"]},
{"name": "badge-plus", "tags": ["add", "new", "create"], "category": "ui", "styles": ["icons"]},
{"name": "badge-x", "tags": ["remove", "delete", "cancel"], "category": "ui", "styles": ["icons"]},
{"name": "baggage-claim", "tags": ["airport", "luggage", "travel"], "category": "travel", "styles": ["icons"]},
{"name": "ban", "tags": ["block", "forbidden", "prohibited"], "category": "ui", "styles": ["icons"]},
{"name": "banana", "tags": ["fruit", "food", "healthy"], "category": "food", "styles": ["icons"]},
{"name": "banknote", "tags": ["money", "cash", "currency"], "category": "commerce", "styles": ["icons"]},
{"name": "bar-chart", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "bar-chart-2", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "bar-chart-3", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "bar-chart-4", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "bar-chart-big", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "bar-chart-horizontal", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "bar-chart-horizontal-big", "tags": ["analytics", "statistics", "graph"], "category": "charts", "styles": ["icons"]},
{"name": "battery", "tags": ["power", "energy", "charge"], "category": "devices", "styles": ["icons"]},
{"name": "battery-charging", "tags": ["power", "energy", "charge"], "category": "devices", "styles": ["icons"]},
{"name": "battery-full", "tags": ["power", "energy", "full"], "category": "devices", "styles": ["icons"]},
{"name": "battery-low", "tags": ["power", "energy", "low"], "category": "devices", "styles": ["icons"]},
{"name": "battery-medium", "tags": ["power", "energy", "half"], "category": "devices", "styles": ["icons"]},
{"name": "battery-warning", "tags": ["power", "energy", "alert"], "category": "devices", "styles": ["icons"]},
{"name": "beaker", "tags": ["science", "lab", "chemistry"], "category": "science", "styles": ["icons"]},
{"name": "bean", "tags": ["food", "vegetable", "seed"], "category": "food", "styles": ["icons"]},
{"name": "bean-off", "tags": ["food", "allergy", "no"], "category": "food", "styles": ["icons"]},
{"name": "bed", "tags": ["sleep", "hotel", "rest"], "category": "furniture", "styles": ["icons"]},
{"name": "bed-double", "tags": ["sleep", "hotel", "bedroom"], "category": "furniture", "styles": ["icons"]},
{"name": "bed-single", "tags": ["sleep", "hotel", "bedroom"], "category": "furniture", "styles": ["icons"]},
{"name": "beer", "tags": ["drink", "alcohol", "bar"], "category": "food", "styles": ["icons"]},
{"name": "bell", "tags": ["notification", "alert", "alarm"], "category": "notifications", "styles": ["icons"]},
{"name": "bell-dot", "tags": ["notification", "alert", "new"], "category": "notifications", "styles": ["icons"]},
{"name": "bell-minus", "tags": ["notification", "remove", "mute"], "category": "notifications", "styles": ["icons"]},
{"name": "bell-off", "tags": ["notification", "mute", "silent"], "category": "notifications", "styles": ["icons"]},
{"name": "bell-plus", "tags": ["notification", "add", "subscribe"], "category": "notifications", "styles": ["icons"]},
{"name": "bell-ring", "tags": ["notification", "alert", "ringing"], "category": "notifications", "styles": ["icons"]},
{"name": "bike", "tags": ["bicycle", "cycling", "transport"], "category": "transportation", "styles": ["icons"]},
{"name": "binary", "tags": ["code", "programming", "data"], "category": "development", "styles": ["icons"]},
{"name": "bluetooth", "tags": ["wireless", "connection", "device"], "category": "connectivity", "styles": ["icons"]},
{"name": "bluetooth-connected", "tags": ["wireless", "paired", "device"], "category": "connectivity", "styles": ["icons"]},
{"name": "bluetooth-off", "tags": ["wireless", "disabled", "device"], "category": "connectivity", "styles": ["icons"]},
{"name": "bluetooth-searching", "tags": ["wireless", "scanning", "device"], "category": "connectivity", "styles": ["icons"]},
{"name": "bold", "tags": ["text", "format", "strong"], "category": "text", "styles": ["icons"]},
{"name": "bomb", "tags": ["explosive", "danger", "warning"], "category": "objects", "styles": ["icons"]},
{"name": "bone", "tags": ["skeleton", "body", "medical"], "category": "medical", "styles": ["icons"]},
{"name": "book", "tags": ["read", "library", "education"], "category": "education", "styles": ["icons"]},
{"name": "book-copy", "tags": ["duplicate", "clone", "library"], "category": "education", "styles": ["icons"]},
{"name": "book-down", "tags": ["download", "save", "library"], "category": "education", "styles": ["icons"]},
{"name": "book-key", "tags": ["password", "secret", "secure"], "category": "education", "styles": ["icons"]},
{"name": "book-lock", "tags": ["private", "secure", "protected"], "category": "education", "styles": ["icons"]},
{"name": "book-marked", "tags": ["bookmark", "saved", "reading"], "category": "education", "styles": ["icons"]},
{"name": "book-minus", "tags": ["remove", "delete", "library"], "category": "education", "styles": ["icons"]},
{"name": "book-open", "tags": ["read", "library", "education"], "category": "education", "styles": ["icons"]},
{"name": "book-open-check", "tags": ["read", "complete", "done"], "category": "education", "styles": ["icons"]},
{"name": "book-plus", "tags": ["add", "new", "library"], "category": "education", "styles": ["icons"]},
{"name": "book-up", "tags": ["upload", "export", "library"], "category": "education", "styles": ["icons"]},
{"name": "book-x", "tags": ["remove", "cancel", "library"], "category": "education", "styles": ["icons"]},
{"name": "bookmark", "tags": ["save", "favorite", "tag"], "category": "actions", "styles": ["icons"]},
{"name": "bookmark-minus", "tags": ["remove", "unsave", "tag"], "category": "actions", "styles": ["icons"]},
{"name": "bookmark-plus", "tags": ["add", "save", "tag"], "category": "actions", "styles": ["icons"]},
{"name": "bot", "tags": ["robot", "ai", "automation"], "category": "development", "styles": ["icons"]},
{"name": "box", "tags": ["package", "container", "storage"], "category": "objects", "styles": ["icons"]},
{"name": "box-select", "tags": ["selection", "area", "region"], "category": "ui", "styles": ["icons"]},
{"name": "boxes", "tags": ["packages", "inventory", "storage"], "category": "objects", "styles": ["icons"]},
{"name": "braces", "tags": ["code", "curly", "programming"], "category": "development", "styles": ["icons"]},
{"name": "brackets", "tags": ["code", "array", "programming"], "category": "development", "styles": ["icons"]},
{"name": "brain", "tags": ["mind", "thinking", "intelligence"], "category": "medical", "styles": ["icons"]},
{"name": "briefcase", "tags": ["work", "job", "business"], "category": "business", "styles": ["icons"]},
{"name": "brush", "tags": ["paint", "art", "design"], "category": "design", "styles": ["icons"]},
{"name": "bug", "tags": ["insect", "error", "debug"], "category": "development", "styles": ["icons"]},
{"name": "building", "tags": ["office", "company", "business"], "category": "buildings", "styles": ["icons"]},
{"name": "building-2", "tags": ["office", "company", "business"], "category": "buildings", "styles": ["icons"]},
{"name": "bus", "tags": ["transport", "vehicle", "travel"], "category": "transportation", "styles": ["icons"]},
{"name": "cable", "tags": ["wire", "connection", "plug"], "category": "devices", "styles": ["icons"]},
{"name": "cake", "tags": ["birthday", "celebration", "party"], "category": "food", "styles": ["icons"]},
{"name": "calculator", "tags": ["math", "calculate", "numbers"], "category": "tools", "styles": ["icons"]},
{"name": "calendar", "tags": ["date", "schedule", "event"], "category": "time", "styles": ["icons"]},
{"name": "calendar-check", "tags": ["date", "done", "confirmed"], "category": "time", "styles": ["icons"]},
{"name": "calendar-clock", "tags": ["date", "time", "schedule"], "category": "time", "styles": ["icons"]},
{"name": "calendar-days", "tags": ["date", "month", "schedule"], "category": "time", "styles": ["icons"]},
{"name": "camera", "tags": ["photo", "picture", "image"], "category": "devices", "styles": ["icons"]},
{"name": "camera-off", "tags": ["photo", "disabled", "no"], "category": "devices", "styles": ["icons"]},
{"name": "car", "tags": ["vehicle", "transport", "drive"], "category": "transportation", "styles": ["icons"]},
{"name": "check", "tags": ["done", "complete", "success"], "category": "ui", "styles": ["icons"]},
{"name": "check-check", "tags": ["done", "verified", "double"], "category": "ui", "styles": ["icons"]},
{"name": "check-circle", "tags": ["done", "complete", "success"], "category": "ui", "styles": ["icons"]},
{"name": "check-circle-2", "tags": ["done", "complete", "success"], "category": "ui", "styles": ["icons"]},
{"name": "check-square", "tags": ["done", "checkbox", "task"], "category": "ui", "styles": ["icons"]},
{"name": "chevron-down", "tags": ["arrow", "expand", "dropdown"], "category": "arrows", "styles": ["icons"]},
{"name": "chevron-left", "tags": ["arrow", "back", "previous"], "category": "arrows", "styles": ["icons"]},
{"name": "chevron-right", "tags": ["arrow", "forward", "next"], "category": "arrows", "styles": ["icons"]},
{"name": "chevron-up", "tags": ["arrow", "collapse", "up"], "category": "arrows", "styles": ["icons"]},
{"name": "chevrons-down", "tags": ["arrows", "expand", "more"], "category": "arrows", "styles": ["icons"]},
{"name": "chevrons-left", "tags": ["arrows", "back", "first"], "category": "arrows", "styles": ["icons"]},
{"name": "chevrons-right", "tags": ["arrows", "forward", "last"], "category": "arrows", "styles": ["icons"]},
{"name": "chevrons-up", "tags": ["arrows", "collapse", "less"], "category": "arrows", "styles": ["icons"]},
{"name": "circle", "tags": ["shape", "dot", "record"], "category": "shapes", "styles": ["icons"]},
{"name": "clipboard", "tags": ["copy", "paste", "board"], "category": "actions", "styles": ["icons"]},
{"name": "clipboard-check", "tags": ["done", "verified", "complete"], "category": "actions", "styles": ["icons"]},
{"name": "clipboard-copy", "tags": ["copy", "duplicate", "paste"], "category": "actions", "styles": ["icons"]},
{"name": "clipboard-list", "tags": ["tasks", "checklist", "todo"], "category": "actions", "styles": ["icons"]},
{"name": "clock", "tags": ["time", "watch", "schedule"], "category": "time", "styles": ["icons"]},
{"name": "cloud", "tags": ["weather", "storage", "sky"], "category": "weather", "styles": ["icons"]},
{"name": "cloud-download", "tags": ["download", "save", "sync"], "category": "actions", "styles": ["icons"]},
{"name": "cloud-off", "tags": ["offline", "disconnect", "no"], "category": "weather", "styles": ["icons"]},
{"name": "cloud-upload", "tags": ["upload", "sync", "backup"], "category": "actions", "styles": ["icons"]},
{"name": "code", "tags": ["programming", "html", "development"], "category": "development", "styles": ["icons"]},
{"name": "code-2", "tags": ["programming", "brackets", "development"], "category": "development", "styles": ["icons"]},
{"name": "coffee", "tags": ["drink", "cup", "cafe"], "category": "food", "styles": ["icons"]},
{"name": "cog", "tags": ["settings", "gear", "config"], "category": "ui", "styles": ["icons"]},
{"name": "command", "tags": ["keyboard", "mac", "shortcut"], "category": "devices", "styles": ["icons"]},
{"name": "compass", "tags": ["navigation", "direction", "explore"], "category": "navigation", "styles": ["icons"]},
{"name": "copy", "tags": ["duplicate", "clipboard", "clone"], "category": "actions", "styles": ["icons"]},
{"name": "credit-card", "tags": ["payment", "card", "money"], "category": "commerce", "styles": ["icons"]},
{"name": "crop", "tags": ["image", "edit", "resize"], "category": "editing", "styles": ["icons"]},
{"name": "database", "tags": ["storage", "data", "server"], "category": "development", "styles": ["icons"]},
{"name": "delete", "tags": ["remove", "trash", "erase"], "category": "actions", "styles": ["icons"]},
{"name": "download", "tags": ["save", "export", "arrow"], "category": "actions", "styles": ["icons"]},
{"name": "download-cloud", "tags": ["save", "sync", "backup"], "category": "actions", "styles": ["icons"]},
{"name": "edit", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]},
{"name": "edit-2", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]},
{"name": "edit-3", "tags": ["pencil", "write", "modify"], "category": "actions", "styles": ["icons"]},
{"name": "external-link", "tags": ["link", "open", "new window"], "category": "actions", "styles": ["icons"]},
{"name": "eye", "tags": ["view", "visible", "show"], "category": "actions", "styles": ["icons"]},
{"name": "eye-off", "tags": ["hide", "invisible", "hidden"], "category": "actions", "styles": ["icons"]},
{"name": "file", "tags": ["document", "page", "paper"], "category": "files", "styles": ["icons"]},
{"name": "file-plus", "tags": ["document", "add", "new"], "category": "files", "styles": ["icons"]},
{"name": "file-text", "tags": ["document", "text", "content"], "category": "files", "styles": ["icons"]},
{"name": "filter", "tags": ["funnel", "sort", "search"], "category": "actions", "styles": ["icons"]},
{"name": "flag", "tags": ["report", "mark", "country"], "category": "objects", "styles": ["icons"]},
{"name": "folder", "tags": ["directory", "files", "organize"], "category": "files", "styles": ["icons"]},
{"name": "folder-open", "tags": ["directory", "browse", "explore"], "category": "files", "styles": ["icons"]},
{"name": "folder-plus", "tags": ["directory", "add", "new"], "category": "files", "styles": ["icons"]},
{"name": "gift", "tags": ["present", "reward", "surprise"], "category": "objects", "styles": ["icons"]},
{"name": "globe", "tags": ["world", "earth", "web"], "category": "objects", "styles": ["icons"]},
{"name": "grid", "tags": ["layout", "squares", "dashboard"], "category": "layout", "styles": ["icons"]},
{"name": "hash", "tags": ["tag", "hashtag", "number"], "category": "communication", "styles": ["icons"]},
{"name": "heart", "tags": ["love", "favorite", "like"], "category": "objects", "styles": ["icons"]},
{"name": "help-circle", "tags": ["question", "help", "support"], "category": "ui", "styles": ["icons"]},
{"name": "home", "tags": ["house", "main", "dashboard"], "category": "navigation", "styles": ["icons"]},
{"name": "image", "tags": ["picture", "photo", "gallery"], "category": "media", "styles": ["icons"]},
{"name": "inbox", "tags": ["email", "messages", "mail"], "category": "communication", "styles": ["icons"]},
{"name": "info", "tags": ["information", "help", "about"], "category": "ui", "styles": ["icons"]},
{"name": "key", "tags": ["password", "security", "lock"], "category": "security", "styles": ["icons"]},
{"name": "layers", "tags": ["stack", "levels", "depth"], "category": "design", "styles": ["icons"]},
{"name": "layout", "tags": ["grid", "template", "design"], "category": "layout", "styles": ["icons"]},
{"name": "link", "tags": ["url", "chain", "connect"], "category": "actions", "styles": ["icons"]},
{"name": "list", "tags": ["menu", "items", "bullet"], "category": "text", "styles": ["icons"]},
{"name": "loader", "tags": ["loading", "spinner", "wait"], "category": "ui", "styles": ["icons"]},
{"name": "loader-2", "tags": ["loading", "spinner", "wait"], "category": "ui", "styles": ["icons"]},
{"name": "lock", "tags": ["security", "private", "password"], "category": "security", "styles": ["icons"]},
{"name": "log-in", "tags": ["login", "signin", "enter"], "category": "actions", "styles": ["icons"]},
{"name": "log-out", "tags": ["logout", "signout", "exit"], "category": "actions", "styles": ["icons"]},
{"name": "mail", "tags": ["email", "message", "envelope"], "category": "communication", "styles": ["icons"]},
{"name": "map", "tags": ["location", "navigation", "directions"], "category": "maps", "styles": ["icons"]},
{"name": "map-pin", "tags": ["location", "marker", "place"], "category": "maps", "styles": ["icons"]},
{"name": "maximize", "tags": ["fullscreen", "expand", "resize"], "category": "actions", "styles": ["icons"]},
{"name": "maximize-2", "tags": ["fullscreen", "expand", "resize"], "category": "actions", "styles": ["icons"]},
{"name": "menu", "tags": ["hamburger", "navigation", "bars"], "category": "navigation", "styles": ["icons"]},
{"name": "message-circle", "tags": ["chat", "comment", "conversation"], "category": "communication", "styles": ["icons"]},
{"name": "message-square", "tags": ["chat", "comment", "conversation"], "category": "communication", "styles": ["icons"]},
{"name": "mic", "tags": ["microphone", "audio", "record"], "category": "media", "styles": ["icons"]},
{"name": "mic-off", "tags": ["microphone", "mute", "silent"], "category": "media", "styles": ["icons"]},
{"name": "minimize", "tags": ["collapse", "shrink", "resize"], "category": "actions", "styles": ["icons"]},
{"name": "minimize-2", "tags": ["collapse", "shrink", "resize"], "category": "actions", "styles": ["icons"]},
{"name": "minus", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["icons"]},
{"name": "minus-circle", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["icons"]},
{"name": "monitor", "tags": ["screen", "display", "desktop"], "category": "devices", "styles": ["icons"]},
{"name": "moon", "tags": ["dark", "night", "theme"], "category": "weather", "styles": ["icons"]},
{"name": "more-horizontal", "tags": ["menu", "options", "dots"], "category": "ui", "styles": ["icons"]},
{"name": "more-vertical", "tags": ["menu", "options", "dots"], "category": "ui", "styles": ["icons"]},
{"name": "move", "tags": ["drag", "reorder", "arrows"], "category": "actions", "styles": ["icons"]},
{"name": "music", "tags": ["audio", "sound", "note"], "category": "media", "styles": ["icons"]},
{"name": "navigation", "tags": ["direction", "arrow", "location"], "category": "navigation", "styles": ["icons"]},
{"name": "package", "tags": ["box", "delivery", "shipping"], "category": "objects", "styles": ["icons"]},
{"name": "paperclip", "tags": ["attachment", "file", "clip"], "category": "actions", "styles": ["icons"]},
{"name": "pause", "tags": ["media", "stop", "wait"], "category": "media", "styles": ["icons"]},
{"name": "pencil", "tags": ["edit", "write", "draw"], "category": "actions", "styles": ["icons"]},
{"name": "phone", "tags": ["call", "contact", "mobile"], "category": "communication", "styles": ["icons"]},
{"name": "pie-chart", "tags": ["analytics", "statistics", "data"], "category": "charts", "styles": ["icons"]},
{"name": "play", "tags": ["media", "start", "video"], "category": "media", "styles": ["icons"]},
{"name": "play-circle", "tags": ["media", "start", "video"], "category": "media", "styles": ["icons"]},
{"name": "plus", "tags": ["add", "create", "new"], "category": "math", "styles": ["icons"]},
{"name": "plus-circle", "tags": ["add", "create", "new"], "category": "math", "styles": ["icons"]},
{"name": "power", "tags": ["on", "off", "shutdown"], "category": "devices", "styles": ["icons"]},
{"name": "printer", "tags": ["print", "document", "paper"], "category": "devices", "styles": ["icons"]},
{"name": "refresh-ccw", "tags": ["reload", "sync", "update"], "category": "actions", "styles": ["icons"]},
{"name": "refresh-cw", "tags": ["reload", "sync", "update"], "category": "actions", "styles": ["icons"]},
{"name": "repeat", "tags": ["loop", "replay", "refresh"], "category": "media", "styles": ["icons"]},
{"name": "rotate-ccw", "tags": ["undo", "rotate", "turn"], "category": "actions", "styles": ["icons"]},
{"name": "rotate-cw", "tags": ["redo", "rotate", "turn"], "category": "actions", "styles": ["icons"]},
{"name": "rss", "tags": ["feed", "subscribe", "blog"], "category": "communication", "styles": ["icons"]},
{"name": "save", "tags": ["disk", "floppy", "store"], "category": "actions", "styles": ["icons"]},
{"name": "scissors", "tags": ["cut", "trim", "edit"], "category": "actions", "styles": ["icons"]},
{"name": "search", "tags": ["find", "magnify", "look"], "category": "actions", "styles": ["icons"]},
{"name": "send", "tags": ["message", "email", "submit"], "category": "communication", "styles": ["icons"]},
{"name": "server", "tags": ["hosting", "database", "backend"], "category": "devices", "styles": ["icons"]},
{"name": "settings", "tags": ["gear", "cog", "preferences"], "category": "ui", "styles": ["icons"]},
{"name": "settings-2", "tags": ["sliders", "controls", "adjust"], "category": "ui", "styles": ["icons"]},
{"name": "share", "tags": ["social", "send", "forward"], "category": "actions", "styles": ["icons"]},
{"name": "share-2", "tags": ["social", "send", "network"], "category": "actions", "styles": ["icons"]},
{"name": "shield", "tags": ["security", "protection", "safe"], "category": "security", "styles": ["icons"]},
{"name": "shopping-bag", "tags": ["shop", "purchase", "buy"], "category": "commerce", "styles": ["icons"]},
{"name": "shopping-cart", "tags": ["shop", "cart", "ecommerce"], "category": "commerce", "styles": ["icons"]},
{"name": "shuffle", "tags": ["random", "mix", "music"], "category": "media", "styles": ["icons"]},
{"name": "skip-back", "tags": ["media", "previous", "rewind"], "category": "media", "styles": ["icons"]},
{"name": "skip-forward", "tags": ["media", "next", "forward"], "category": "media", "styles": ["icons"]},
{"name": "slash", "tags": ["ban", "cancel", "disabled"], "category": "ui", "styles": ["icons"]},
{"name": "sliders", "tags": ["settings", "controls", "adjust"], "category": "ui", "styles": ["icons"]},
{"name": "smartphone", "tags": ["phone", "mobile", "device"], "category": "devices", "styles": ["icons"]},
{"name": "smile", "tags": ["happy", "emoji", "face"], "category": "emoji", "styles": ["icons"]},
{"name": "sparkles", "tags": ["magic", "new", "ai"], "category": "objects", "styles": ["icons"]},
{"name": "speaker", "tags": ["audio", "sound", "volume"], "category": "media", "styles": ["icons"]},
{"name": "star", "tags": ["favorite", "rating", "bookmark"], "category": "objects", "styles": ["icons"]},
{"name": "stop-circle", "tags": ["media", "stop", "end"], "category": "media", "styles": ["icons"]},
{"name": "sun", "tags": ["light", "day", "brightness"], "category": "weather", "styles": ["icons"]},
{"name": "table", "tags": ["data", "grid", "spreadsheet"], "category": "data", "styles": ["icons"]},
{"name": "tablet", "tags": ["device", "ipad", "screen"], "category": "devices", "styles": ["icons"]},
{"name": "tag", "tags": ["label", "category", "price"], "category": "commerce", "styles": ["icons"]},
{"name": "target", "tags": ["goal", "aim", "focus"], "category": "ui", "styles": ["icons"]},
{"name": "terminal", "tags": ["console", "command", "code"], "category": "development", "styles": ["icons"]},
{"name": "thumbs-down", "tags": ["dislike", "bad", "negative"], "category": "actions", "styles": ["icons"]},
{"name": "thumbs-up", "tags": ["like", "good", "positive"], "category": "actions", "styles": ["icons"]},
{"name": "toggle-left", "tags": ["switch", "off", "disable"], "category": "ui", "styles": ["icons"]},
{"name": "toggle-right", "tags": ["switch", "on", "enable"], "category": "ui", "styles": ["icons"]},
{"name": "trash", "tags": ["delete", "remove", "bin"], "category": "actions", "styles": ["icons"]},
{"name": "trash-2", "tags": ["delete", "remove", "bin"], "category": "actions", "styles": ["icons"]},
{"name": "trending-down", "tags": ["chart", "decrease", "analytics"], "category": "charts", "styles": ["icons"]},
{"name": "trending-up", "tags": ["chart", "increase", "analytics"], "category": "charts", "styles": ["icons"]},
{"name": "triangle", "tags": ["shape", "polygon", "warning"], "category": "shapes", "styles": ["icons"]},
{"name": "truck", "tags": ["delivery", "shipping", "transport"], "category": "transportation", "styles": ["icons"]},
{"name": "tv", "tags": ["television", "screen", "display"], "category": "devices", "styles": ["icons"]},
{"name": "type", "tags": ["text", "font", "typography"], "category": "text", "styles": ["icons"]},
{"name": "umbrella", "tags": ["weather", "rain", "protection"], "category": "weather", "styles": ["icons"]},
{"name": "underline", "tags": ["text", "format", "style"], "category": "text", "styles": ["icons"]},
{"name": "undo", "tags": ["back", "reverse", "history"], "category": "actions", "styles": ["icons"]},
{"name": "unlock", "tags": ["security", "open", "access"], "category": "security", "styles": ["icons"]},
{"name": "upload", "tags": ["export", "send", "share"], "category": "actions", "styles": ["icons"]},
{"name": "upload-cloud", "tags": ["export", "sync", "backup"], "category": "actions", "styles": ["icons"]},
{"name": "user", "tags": ["person", "account", "profile"], "category": "users", "styles": ["icons"]},
{"name": "user-check", "tags": ["person", "verified", "approved"], "category": "users", "styles": ["icons"]},
{"name": "user-minus", "tags": ["person", "remove", "unfriend"], "category": "users", "styles": ["icons"]},
{"name": "user-plus", "tags": ["person", "add", "invite"], "category": "users", "styles": ["icons"]},
{"name": "user-x", "tags": ["person", "delete", "remove"], "category": "users", "styles": ["icons"]},
{"name": "users", "tags": ["people", "team", "group"], "category": "users", "styles": ["icons"]},
{"name": "video", "tags": ["camera", "film", "record"], "category": "media", "styles": ["icons"]},
{"name": "video-off", "tags": ["camera", "disabled", "mute"], "category": "media", "styles": ["icons"]},
{"name": "volume", "tags": ["sound", "audio", "speaker"], "category": "media", "styles": ["icons"]},
{"name": "volume-1", "tags": ["sound", "audio", "low"], "category": "media", "styles": ["icons"]},
{"name": "volume-2", "tags": ["sound", "audio", "high"], "category": "media", "styles": ["icons"]},
{"name": "volume-x", "tags": ["mute", "silent", "no sound"], "category": "media", "styles": ["icons"]},
{"name": "wallet", "tags": ["money", "payment", "finance"], "category": "commerce", "styles": ["icons"]},
{"name": "wifi", "tags": ["wireless", "internet", "connection"], "category": "connectivity", "styles": ["icons"]},
{"name": "wifi-off", "tags": ["wireless", "offline", "disconnect"], "category": "connectivity", "styles": ["icons"]},
{"name": "x", "tags": ["close", "cancel", "remove"], "category": "actions", "styles": ["icons"]},
{"name": "x-circle", "tags": ["close", "cancel", "error"], "category": "actions", "styles": ["icons"]},
{"name": "zap", "tags": ["lightning", "power", "energy"], "category": "objects", "styles": ["icons"]},
{"name": "zap-off", "tags": ["lightning", "disabled", "off"], "category": "objects", "styles": ["icons"]},
{"name": "zoom-in", "tags": ["magnify", "enlarge", "plus"], "category": "actions", "styles": ["icons"]},
{"name": "zoom-out", "tags": ["magnify", "shrink", "minus"], "category": "actions", "styles": ["icons"]}
]
}

View file

@ -1,455 +0,0 @@
{
"name": "Material Symbols",
"slug": "material",
"version": "latest",
"license": "Apache-2.0",
"url": "https://fonts.google.com/icons",
"styles": ["outlined", "rounded", "sharp"],
"defaultStyle": "outlined",
"viewBox": "0 0 24 24",
"cdn": {
"base": "https://cdn.jsdelivr.net/npm/@material-symbols/svg-400@0.14.5",
"pattern": "{style}/{name}.svg"
},
"icons": [
{"name": "home", "tags": ["house", "residence", "building"]},
{"name": "search", "tags": ["find", "magnify", "lookup"]},
{"name": "settings", "tags": ["cog", "gear", "preferences", "config"]},
{"name": "menu", "tags": ["hamburger", "navigation", "bars"]},
{"name": "close", "tags": ["x", "dismiss", "cancel", "remove"]},
{"name": "check", "tags": ["tick", "done", "complete", "success"]},
{"name": "add", "tags": ["plus", "new", "create"]},
{"name": "remove", "tags": ["minus", "subtract", "delete"]},
{"name": "edit", "tags": ["pencil", "modify", "change"]},
{"name": "delete", "tags": ["trash", "bin", "remove"]},
{"name": "favorite", "tags": ["heart", "love", "like"]},
{"name": "star", "tags": ["rating", "bookmark", "featured"]},
{"name": "share", "tags": ["social", "send", "distribute"]},
{"name": "person", "tags": ["user", "profile", "account"]},
{"name": "people", "tags": ["users", "group", "team"]},
{"name": "mail", "tags": ["email", "envelope", "message"]},
{"name": "call", "tags": ["phone", "telephone", "contact"]},
{"name": "chat", "tags": ["message", "conversation", "bubble"]},
{"name": "notifications", "tags": ["bell", "alert", "alarm"]},
{"name": "calendar_today", "tags": ["date", "schedule", "event"]},
{"name": "schedule", "tags": ["time", "clock", "watch"]},
{"name": "location_on", "tags": ["pin", "map", "place", "marker"]},
{"name": "directions", "tags": ["navigate", "route", "arrow"]},
{"name": "photo", "tags": ["image", "picture", "gallery"]},
{"name": "camera", "tags": ["photo", "capture", "snapshot"]},
{"name": "videocam", "tags": ["video", "movie", "record"]},
{"name": "mic", "tags": ["microphone", "audio", "voice"]},
{"name": "volume_up", "tags": ["sound", "speaker", "audio"]},
{"name": "volume_off", "tags": ["mute", "silent", "quiet"]},
{"name": "play_arrow", "tags": ["start", "begin", "media"]},
{"name": "pause", "tags": ["stop", "hold", "media"]},
{"name": "stop", "tags": ["end", "halt", "media"]},
{"name": "skip_next", "tags": ["forward", "next", "media"]},
{"name": "skip_previous", "tags": ["back", "previous", "media"]},
{"name": "replay", "tags": ["repeat", "refresh", "again"]},
{"name": "shuffle", "tags": ["random", "mix", "media"]},
{"name": "folder", "tags": ["directory", "file", "storage"]},
{"name": "folder_open", "tags": ["directory", "file", "open"]},
{"name": "file_copy", "tags": ["document", "duplicate", "clone"]},
{"name": "attach_file", "tags": ["attachment", "paperclip", "document"]},
{"name": "cloud", "tags": ["weather", "storage", "upload"]},
{"name": "cloud_upload", "tags": ["upload", "save", "storage"]},
{"name": "cloud_download", "tags": ["download", "save", "storage"]},
{"name": "download", "tags": ["save", "arrow", "get"]},
{"name": "upload", "tags": ["send", "arrow", "put"]},
{"name": "sync", "tags": ["refresh", "update", "reload"]},
{"name": "refresh", "tags": ["reload", "update", "sync"]},
{"name": "lock", "tags": ["security", "password", "secure"]},
{"name": "lock_open", "tags": ["unlock", "open", "access"]},
{"name": "visibility", "tags": ["eye", "view", "show"]},
{"name": "visibility_off", "tags": ["eye", "hide", "hidden"]},
{"name": "key", "tags": ["password", "access", "security"]},
{"name": "shield", "tags": ["security", "protect", "safe"]},
{"name": "verified", "tags": ["check", "approved", "trusted"]},
{"name": "warning", "tags": ["alert", "caution", "danger"]},
{"name": "error", "tags": ["alert", "problem", "issue"]},
{"name": "info", "tags": ["information", "help", "about"]},
{"name": "help", "tags": ["question", "support", "info"]},
{"name": "lightbulb", "tags": ["idea", "tip", "suggestion"]},
{"name": "thumb_up", "tags": ["like", "approve", "good"]},
{"name": "thumb_down", "tags": ["dislike", "reject", "bad"]},
{"name": "mood", "tags": ["emoji", "face", "happy"]},
{"name": "sentiment_satisfied", "tags": ["happy", "smile", "positive"]},
{"name": "sentiment_dissatisfied", "tags": ["sad", "unhappy", "negative"]},
{"name": "shopping_cart", "tags": ["cart", "buy", "purchase", "ecommerce"]},
{"name": "shopping_bag", "tags": ["bag", "buy", "purchase", "retail"]},
{"name": "store", "tags": ["shop", "retail", "business"]},
{"name": "payments", "tags": ["money", "card", "credit"]},
{"name": "credit_card", "tags": ["payment", "money", "bank"]},
{"name": "account_balance", "tags": ["bank", "finance", "money"]},
{"name": "receipt", "tags": ["invoice", "bill", "payment"]},
{"name": "local_shipping", "tags": ["delivery", "truck", "shipping"]},
{"name": "inventory", "tags": ["stock", "warehouse", "storage"]},
{"name": "work", "tags": ["briefcase", "job", "business"]},
{"name": "business", "tags": ["building", "company", "office"]},
{"name": "apartment", "tags": ["building", "home", "residence"]},
{"name": "school", "tags": ["education", "learning", "building"]},
{"name": "restaurant", "tags": ["food", "dining", "eat"]},
{"name": "local_cafe", "tags": ["coffee", "drink", "cup"]},
{"name": "local_bar", "tags": ["drink", "cocktail", "alcohol"]},
{"name": "fitness_center", "tags": ["gym", "exercise", "workout"]},
{"name": "sports", "tags": ["athletics", "game", "activity"]},
{"name": "directions_car", "tags": ["car", "vehicle", "auto", "drive"]},
{"name": "directions_bus", "tags": ["bus", "transit", "transport"]},
{"name": "directions_bike", "tags": ["bicycle", "cycling", "bike"]},
{"name": "directions_walk", "tags": ["walk", "pedestrian", "foot"]},
{"name": "flight", "tags": ["plane", "airplane", "travel"]},
{"name": "train", "tags": ["rail", "transit", "transport"]},
{"name": "subway", "tags": ["metro", "underground", "transit"]},
{"name": "keyboard", "tags": ["type", "input", "keys"]},
{"name": "mouse", "tags": ["cursor", "click", "input"]},
{"name": "computer", "tags": ["desktop", "pc", "monitor"]},
{"name": "laptop", "tags": ["computer", "notebook", "device"]},
{"name": "smartphone", "tags": ["phone", "mobile", "device"]},
{"name": "tablet", "tags": ["ipad", "device", "screen"]},
{"name": "watch", "tags": ["time", "wearable", "smartwatch"]},
{"name": "headphones", "tags": ["audio", "music", "listen"]},
{"name": "speaker", "tags": ["audio", "sound", "music"]},
{"name": "tv", "tags": ["television", "screen", "display"]},
{"name": "print", "tags": ["printer", "document", "paper"]},
{"name": "scanner", "tags": ["scan", "copy", "document"]},
{"name": "usb", "tags": ["cable", "port", "connect"]},
{"name": "bluetooth", "tags": ["wireless", "connect", "pair"]},
{"name": "wifi", "tags": ["wireless", "internet", "network"]},
{"name": "signal_cellular_alt", "tags": ["mobile", "network", "bars"]},
{"name": "battery_full", "tags": ["power", "charge", "energy"]},
{"name": "battery_low", "tags": ["power", "charge", "low"]},
{"name": "power", "tags": ["on", "off", "switch"]},
{"name": "bolt", "tags": ["lightning", "flash", "power"]},
{"name": "brightness_high", "tags": ["sun", "light", "display"]},
{"name": "brightness_low", "tags": ["dim", "dark", "display"]},
{"name": "dark_mode", "tags": ["night", "moon", "theme"]},
{"name": "light_mode", "tags": ["day", "sun", "theme"]},
{"name": "thermostat", "tags": ["temperature", "climate", "heat"]},
{"name": "ac_unit", "tags": ["cooling", "air", "snowflake"]},
{"name": "wb_sunny", "tags": ["sun", "weather", "bright"]},
{"name": "cloud_queue", "tags": ["weather", "cloudy", "sky"]},
{"name": "water_drop", "tags": ["rain", "liquid", "droplet"]},
{"name": "air", "tags": ["wind", "breeze", "weather"]},
{"name": "eco", "tags": ["leaf", "nature", "green"]},
{"name": "park", "tags": ["tree", "nature", "outdoor"]},
{"name": "pets", "tags": ["paw", "animal", "dog"]},
{"name": "bug_report", "tags": ["insect", "issue", "debug"]},
{"name": "code", "tags": ["programming", "developer", "brackets"]},
{"name": "terminal", "tags": ["console", "command", "shell"]},
{"name": "data_object", "tags": ["json", "braces", "code"]},
{"name": "database", "tags": ["storage", "data", "server"]},
{"name": "dns", "tags": ["server", "network", "domain"]},
{"name": "storage", "tags": ["disk", "save", "memory"]},
{"name": "memory", "tags": ["ram", "chip", "hardware"]},
{"name": "developer_board", "tags": ["circuit", "hardware", "tech"]},
{"name": "build", "tags": ["wrench", "tool", "construct"]},
{"name": "construction", "tags": ["tools", "work", "build"]},
{"name": "handyman", "tags": ["tools", "repair", "fix"]},
{"name": "science", "tags": ["flask", "chemistry", "lab"]},
{"name": "biotech", "tags": ["dna", "genetics", "science"]},
{"name": "psychology", "tags": ["brain", "mind", "mental"]},
{"name": "medical_services", "tags": ["health", "hospital", "doctor"]},
{"name": "healing", "tags": ["bandage", "medical", "health"]},
{"name": "medication", "tags": ["pill", "drug", "pharmacy"]},
{"name": "vaccines", "tags": ["syringe", "injection", "medical"]},
{"name": "accessibility", "tags": ["person", "handicap", "access"]},
{"name": "elderly", "tags": ["senior", "old", "person"]},
{"name": "child_care", "tags": ["baby", "kid", "infant"]},
{"name": "family", "tags": ["people", "group", "parents"]},
{"name": "wc", "tags": ["toilet", "bathroom", "restroom"]},
{"name": "smoking_rooms", "tags": ["cigarette", "smoke", "tobacco"]},
{"name": "no_smoking", "tags": ["prohibited", "ban", "cigarette"]},
{"name": "language", "tags": ["globe", "world", "international"]},
{"name": "translate", "tags": ["language", "convert", "localize"]},
{"name": "font_download", "tags": ["typography", "text", "typeface"]},
{"name": "format_bold", "tags": ["text", "typography", "b"]},
{"name": "format_italic", "tags": ["text", "typography", "i"]},
{"name": "format_underlined", "tags": ["text", "typography", "u"]},
{"name": "format_strikethrough", "tags": ["text", "delete", "cross"]},
{"name": "format_size", "tags": ["text", "font", "typography"]},
{"name": "format_color_text", "tags": ["color", "font", "typography"]},
{"name": "format_color_fill", "tags": ["bucket", "paint", "color"]},
{"name": "format_align_left", "tags": ["text", "paragraph", "align"]},
{"name": "format_align_center", "tags": ["text", "paragraph", "align"]},
{"name": "format_align_right", "tags": ["text", "paragraph", "align"]},
{"name": "format_align_justify", "tags": ["text", "paragraph", "align"]},
{"name": "format_list_bulleted", "tags": ["list", "ul", "bullets"]},
{"name": "format_list_numbered", "tags": ["list", "ol", "numbers"]},
{"name": "format_quote", "tags": ["quotation", "blockquote", "cite"]},
{"name": "format_indent_increase", "tags": ["indent", "tab", "spacing"]},
{"name": "format_indent_decrease", "tags": ["outdent", "tab", "spacing"]},
{"name": "link", "tags": ["url", "chain", "hyperlink"]},
{"name": "link_off", "tags": ["unlink", "broken", "disconnect"]},
{"name": "image", "tags": ["photo", "picture", "media"]},
{"name": "insert_photo", "tags": ["image", "picture", "add"]},
{"name": "add_photo_alternate", "tags": ["image", "upload", "new"]},
{"name": "crop", "tags": ["image", "resize", "cut"]},
{"name": "rotate_left", "tags": ["image", "turn", "ccw"]},
{"name": "rotate_right", "tags": ["image", "turn", "cw"]},
{"name": "flip", "tags": ["mirror", "image", "reflect"]},
{"name": "filter", "tags": ["image", "effect", "adjust"]},
{"name": "tune", "tags": ["adjust", "settings", "sliders"]},
{"name": "palette", "tags": ["color", "paint", "art"]},
{"name": "brush", "tags": ["paint", "draw", "art"]},
{"name": "create", "tags": ["pencil", "edit", "write"]},
{"name": "draw", "tags": ["pencil", "sketch", "art"]},
{"name": "gesture", "tags": ["hand", "draw", "touch"]},
{"name": "shape_line", "tags": ["line", "draw", "shape"]},
{"name": "rectangle", "tags": ["square", "shape", "box"]},
{"name": "circle", "tags": ["shape", "round", "ellipse"]},
{"name": "hexagon", "tags": ["shape", "polygon", "six"]},
{"name": "pentagon", "tags": ["shape", "polygon", "five"]},
{"name": "change_history", "tags": ["triangle", "shape", "arrow"]},
{"name": "category", "tags": ["shapes", "organize", "sort"]},
{"name": "grid_view", "tags": ["layout", "tiles", "gallery"]},
{"name": "view_list", "tags": ["layout", "rows", "lines"]},
{"name": "view_module", "tags": ["layout", "grid", "blocks"]},
{"name": "view_agenda", "tags": ["layout", "cards", "list"]},
{"name": "dashboard", "tags": ["layout", "widgets", "overview"]},
{"name": "widgets", "tags": ["components", "blocks", "modules"]},
{"name": "layers", "tags": ["stack", "overlap", "design"]},
{"name": "aspect_ratio", "tags": ["resize", "scale", "dimensions"]},
{"name": "zoom_in", "tags": ["magnify", "enlarge", "plus"]},
{"name": "zoom_out", "tags": ["shrink", "reduce", "minus"]},
{"name": "fullscreen", "tags": ["expand", "maximize", "enlarge"]},
{"name": "fullscreen_exit", "tags": ["shrink", "minimize", "reduce"]},
{"name": "center_focus_strong", "tags": ["target", "aim", "focus"]},
{"name": "filter_center_focus", "tags": ["camera", "focus", "center"]},
{"name": "panorama", "tags": ["wide", "image", "photo"]},
{"name": "photo_library", "tags": ["gallery", "images", "album"]},
{"name": "collections", "tags": ["gallery", "photos", "album"]},
{"name": "slideshow", "tags": ["presentation", "play", "images"]},
{"name": "movie", "tags": ["film", "video", "cinema"]},
{"name": "theaters", "tags": ["cinema", "film", "drama"]},
{"name": "music_note", "tags": ["audio", "song", "melody"]},
{"name": "album", "tags": ["music", "record", "disc"]},
{"name": "radio", "tags": ["music", "broadcast", "audio"]},
{"name": "equalizer", "tags": ["audio", "levels", "music"]},
{"name": "graphic_eq", "tags": ["audio", "visualizer", "music"]},
{"name": "mic_none", "tags": ["microphone", "record", "audio"]},
{"name": "record_voice_over", "tags": ["speak", "voice", "narrate"]},
{"name": "hearing", "tags": ["ear", "listen", "audio"]},
{"name": "closed_caption", "tags": ["subtitles", "cc", "text"]},
{"name": "subtitles", "tags": ["cc", "captions", "text"]},
{"name": "hd", "tags": ["quality", "high", "resolution"]},
{"name": "4k", "tags": ["quality", "ultra", "resolution"]},
{"name": "speed", "tags": ["fast", "gauge", "performance"]},
{"name": "timer", "tags": ["clock", "countdown", "stopwatch"]},
{"name": "hourglass_empty", "tags": ["time", "wait", "loading"]},
{"name": "hourglass_full", "tags": ["time", "complete", "done"]},
{"name": "access_time", "tags": ["clock", "time", "schedule"]},
{"name": "alarm", "tags": ["clock", "alert", "reminder"]},
{"name": "alarm_on", "tags": ["clock", "active", "set"]},
{"name": "alarm_off", "tags": ["clock", "disabled", "silent"]},
{"name": "snooze", "tags": ["sleep", "delay", "alarm"]},
{"name": "event", "tags": ["calendar", "date", "schedule"]},
{"name": "event_available", "tags": ["calendar", "free", "open"]},
{"name": "event_busy", "tags": ["calendar", "occupied", "booked"]},
{"name": "today", "tags": ["calendar", "now", "current"]},
{"name": "date_range", "tags": ["calendar", "period", "span"]},
{"name": "update", "tags": ["refresh", "sync", "reload"]},
{"name": "history", "tags": ["time", "past", "clock"]},
{"name": "restore", "tags": ["undo", "back", "recover"]},
{"name": "undo", "tags": ["back", "reverse", "arrow"]},
{"name": "redo", "tags": ["forward", "repeat", "arrow"]},
{"name": "content_copy", "tags": ["copy", "duplicate", "clipboard"]},
{"name": "content_cut", "tags": ["scissors", "cut", "clipboard"]},
{"name": "content_paste", "tags": ["clipboard", "paste", "insert"]},
{"name": "select_all", "tags": ["checkbox", "all", "select"]},
{"name": "save", "tags": ["disk", "floppy", "store"]},
{"name": "save_as", "tags": ["disk", "export", "store"]},
{"name": "draft", "tags": ["document", "edit", "paper"]},
{"name": "note", "tags": ["paper", "memo", "text"]},
{"name": "note_add", "tags": ["document", "new", "create"]},
{"name": "description", "tags": ["document", "file", "text"]},
{"name": "article", "tags": ["document", "text", "news"]},
{"name": "newspaper", "tags": ["news", "article", "media"]},
{"name": "feed", "tags": ["rss", "news", "stream"]},
{"name": "rss_feed", "tags": ["news", "subscribe", "blog"]},
{"name": "bookmark", "tags": ["save", "favorite", "tag"]},
{"name": "bookmark_border", "tags": ["save", "favorite", "outline"]},
{"name": "bookmarks", "tags": ["saved", "favorites", "collection"]},
{"name": "label", "tags": ["tag", "category", "badge"]},
{"name": "sell", "tags": ["tag", "price", "sale"]},
{"name": "loyalty", "tags": ["heart", "tag", "reward"]},
{"name": "new_releases", "tags": ["starburst", "badge", "new"]},
{"name": "stars", "tags": ["rating", "favorite", "best"]},
{"name": "grade", "tags": ["star", "rating", "review"]},
{"name": "military_tech", "tags": ["medal", "badge", "award"]},
{"name": "emoji_events", "tags": ["trophy", "award", "winner"]},
{"name": "workspace_premium", "tags": ["badge", "crown", "premium"]},
{"name": "verified_user", "tags": ["shield", "check", "secure"]},
{"name": "admin_panel_settings", "tags": ["shield", "gear", "admin"]},
{"name": "manage_accounts", "tags": ["user", "settings", "admin"]},
{"name": "supervised_user_circle", "tags": ["admin", "user", "manage"]},
{"name": "group", "tags": ["people", "team", "users"]},
{"name": "group_add", "tags": ["people", "team", "invite"]},
{"name": "group_remove", "tags": ["people", "team", "delete"]},
{"name": "person_add", "tags": ["user", "invite", "new"]},
{"name": "person_remove", "tags": ["user", "delete", "remove"]},
{"name": "face", "tags": ["emoji", "person", "avatar"]},
{"name": "sentiment_very_satisfied", "tags": ["happy", "joy", "smile"]},
{"name": "sentiment_neutral", "tags": ["meh", "okay", "face"]},
{"name": "sentiment_very_dissatisfied", "tags": ["angry", "upset", "mad"]},
{"name": "mood_bad", "tags": ["sad", "unhappy", "frown"]},
{"name": "sick", "tags": ["ill", "unwell", "face"]},
{"name": "masks", "tags": ["theater", "drama", "comedy"]},
{"name": "cake", "tags": ["birthday", "celebration", "party"]},
{"name": "celebration", "tags": ["party", "confetti", "event"]},
{"name": "card_giftcard", "tags": ["gift", "present", "voucher"]},
{"name": "redeem", "tags": ["gift", "box", "present"]},
{"name": "volunteer_activism", "tags": ["heart", "hand", "donate"]},
{"name": "handshake", "tags": ["deal", "agreement", "partner"]},
{"name": "diversity_3", "tags": ["people", "team", "group"]},
{"name": "public", "tags": ["globe", "world", "earth"]},
{"name": "travel_explore", "tags": ["globe", "search", "world"]},
{"name": "flag", "tags": ["country", "nation", "report"]},
{"name": "tour", "tags": ["flag", "marker", "location"]},
{"name": "place", "tags": ["pin", "location", "marker"]},
{"name": "my_location", "tags": ["gps", "target", "current"]},
{"name": "near_me", "tags": ["location", "arrow", "nearby"]},
{"name": "explore", "tags": ["compass", "navigate", "discover"]},
{"name": "navigation", "tags": ["arrow", "direction", "gps"]},
{"name": "map", "tags": ["location", "geography", "atlas"]},
{"name": "satellite", "tags": ["space", "orbit", "map"]},
{"name": "terrain", "tags": ["mountain", "landscape", "map"]},
{"name": "layers_clear", "tags": ["stack", "remove", "design"]},
{"name": "streetview", "tags": ["map", "360", "view"]},
{"name": "traffic", "tags": ["signal", "light", "road"]},
{"name": "alt_route", "tags": ["path", "alternative", "direction"]},
{"name": "merge", "tags": ["combine", "join", "arrows"]},
{"name": "call_split", "tags": ["fork", "divide", "arrows"]},
{"name": "compare_arrows", "tags": ["swap", "exchange", "switch"]},
{"name": "swap_horiz", "tags": ["exchange", "switch", "horizontal"]},
{"name": "swap_vert", "tags": ["exchange", "switch", "vertical"]},
{"name": "import_export", "tags": ["arrows", "transfer", "data"]},
{"name": "arrow_upward", "tags": ["up", "direction", "top"]},
{"name": "arrow_downward", "tags": ["down", "direction", "bottom"]},
{"name": "arrow_forward", "tags": ["right", "direction", "next"]},
{"name": "arrow_back", "tags": ["left", "direction", "previous"]},
{"name": "north", "tags": ["up", "direction", "compass"]},
{"name": "south", "tags": ["down", "direction", "compass"]},
{"name": "east", "tags": ["right", "direction", "compass"]},
{"name": "west", "tags": ["left", "direction", "compass"]},
{"name": "expand_more", "tags": ["down", "chevron", "dropdown"]},
{"name": "expand_less", "tags": ["up", "chevron", "collapse"]},
{"name": "chevron_right", "tags": ["arrow", "right", "next"]},
{"name": "chevron_left", "tags": ["arrow", "left", "previous"]},
{"name": "unfold_more", "tags": ["expand", "vertical", "arrows"]},
{"name": "unfold_less", "tags": ["collapse", "vertical", "arrows"]},
{"name": "first_page", "tags": ["start", "beginning", "arrow"]},
{"name": "last_page", "tags": ["end", "final", "arrow"]},
{"name": "subdirectory_arrow_right", "tags": ["nested", "child", "sub"]},
{"name": "subdirectory_arrow_left", "tags": ["nested", "parent", "back"]},
{"name": "trending_up", "tags": ["arrow", "growth", "increase"]},
{"name": "trending_down", "tags": ["arrow", "decline", "decrease"]},
{"name": "trending_flat", "tags": ["arrow", "stable", "neutral"]},
{"name": "show_chart", "tags": ["graph", "line", "analytics"]},
{"name": "bar_chart", "tags": ["graph", "statistics", "analytics"]},
{"name": "pie_chart", "tags": ["graph", "statistics", "donut"]},
{"name": "bubble_chart", "tags": ["graph", "data", "visualization"]},
{"name": "scatter_plot", "tags": ["graph", "data", "points"]},
{"name": "analytics", "tags": ["chart", "data", "statistics"]},
{"name": "insights", "tags": ["chart", "data", "analysis"]},
{"name": "leaderboard", "tags": ["chart", "ranking", "bars"]},
{"name": "table_chart", "tags": ["spreadsheet", "data", "grid"]},
{"name": "table_view", "tags": ["grid", "data", "spreadsheet"]},
{"name": "table_rows", "tags": ["grid", "horizontal", "data"]},
{"name": "view_column", "tags": ["grid", "vertical", "layout"]},
{"name": "view_week", "tags": ["calendar", "columns", "schedule"]},
{"name": "view_day", "tags": ["calendar", "single", "schedule"]},
{"name": "timeline", "tags": ["history", "time", "events"]},
{"name": "account_tree", "tags": ["hierarchy", "org", "structure"]},
{"name": "hub", "tags": ["network", "nodes", "connected"]},
{"name": "workspaces", "tags": ["circles", "venn", "overlap"]},
{"name": "token", "tags": ["chip", "badge", "tag"]},
{"name": "api", "tags": ["code", "integration", "connect"]},
{"name": "extension", "tags": ["puzzle", "plugin", "addon"]},
{"name": "integration_instructions", "tags": ["code", "embed", "api"]},
{"name": "webhook", "tags": ["api", "callback", "hook"]},
{"name": "javascript", "tags": ["code", "programming", "js"]},
{"name": "css", "tags": ["code", "style", "web"]},
{"name": "html", "tags": ["code", "web", "markup"]},
{"name": "php", "tags": ["code", "programming", "web"]},
{"name": "deployed_code", "tags": ["box", "package", "release"]},
{"name": "package_2", "tags": ["box", "delivery", "shipping"]},
{"name": "inventory_2", "tags": ["box", "storage", "stock"]},
{"name": "move_to_inbox", "tags": ["archive", "box", "download"]},
{"name": "outbox", "tags": ["send", "upload", "mail"]},
{"name": "inbox", "tags": ["mail", "receive", "box"]},
{"name": "all_inbox", "tags": ["mail", "messages", "combined"]},
{"name": "mark_email_read", "tags": ["mail", "open", "check"]},
{"name": "mark_email_unread", "tags": ["mail", "new", "dot"]},
{"name": "drafts", "tags": ["mail", "edit", "compose"]},
{"name": "send", "tags": ["mail", "submit", "arrow"]},
{"name": "forward_to_inbox", "tags": ["mail", "forward", "arrow"]},
{"name": "reply", "tags": ["mail", "respond", "arrow"]},
{"name": "reply_all", "tags": ["mail", "respond", "group"]},
{"name": "mark_as_unread", "tags": ["mail", "new", "notification"]},
{"name": "markunread_mailbox", "tags": ["mail", "mailbox", "new"]},
{"name": "contact_mail", "tags": ["email", "person", "address"]},
{"name": "contact_phone", "tags": ["call", "person", "number"]},
{"name": "contacts", "tags": ["people", "address", "book"]},
{"name": "perm_contact_calendar", "tags": ["person", "schedule", "date"]},
{"name": "badge", "tags": ["id", "name", "tag"]},
{"name": "assignment", "tags": ["clipboard", "task", "document"]},
{"name": "assignment_ind", "tags": ["person", "task", "assign"]},
{"name": "assignment_turned_in", "tags": ["task", "done", "check"]},
{"name": "assignment_late", "tags": ["task", "overdue", "warning"]},
{"name": "assignment_return", "tags": ["task", "back", "arrow"]},
{"name": "task", "tags": ["checkbox", "todo", "done"]},
{"name": "task_alt", "tags": ["check", "circle", "done"]},
{"name": "checklist", "tags": ["tasks", "todo", "list"]},
{"name": "fact_check", "tags": ["clipboard", "verify", "check"]},
{"name": "rule", "tags": ["check", "cross", "decision"]},
{"name": "done", "tags": ["check", "complete", "success"]},
{"name": "done_all", "tags": ["check", "double", "complete"]},
{"name": "done_outline", "tags": ["check", "circle", "complete"]},
{"name": "check_circle", "tags": ["done", "success", "complete"]},
{"name": "check_box", "tags": ["done", "tick", "checked"]},
{"name": "check_box_outline_blank", "tags": ["empty", "unchecked", "box"]},
{"name": "indeterminate_check_box", "tags": ["partial", "minus", "box"]},
{"name": "radio_button_checked", "tags": ["selected", "option", "circle"]},
{"name": "radio_button_unchecked", "tags": ["empty", "option", "circle"]},
{"name": "toggle_on", "tags": ["switch", "enabled", "active"]},
{"name": "toggle_off", "tags": ["switch", "disabled", "inactive"]},
{"name": "add_circle", "tags": ["plus", "new", "create"]},
{"name": "remove_circle", "tags": ["minus", "delete", "subtract"]},
{"name": "cancel", "tags": ["x", "close", "remove"]},
{"name": "highlight_off", "tags": ["x", "remove", "circle"]},
{"name": "do_not_disturb", "tags": ["minus", "block", "circle"]},
{"name": "block", "tags": ["prohibited", "ban", "stop"]},
{"name": "not_interested", "tags": ["circle", "slash", "disabled"]},
{"name": "report", "tags": ["octagon", "warning", "flag"]},
{"name": "report_problem", "tags": ["triangle", "warning", "alert"]},
{"name": "priority_high", "tags": ["exclamation", "important", "alert"]},
{"name": "new_label", "tags": ["tag", "badge", "add"]},
{"name": "fiber_new", "tags": ["badge", "new", "fresh"]},
{"name": "auto_awesome", "tags": ["sparkle", "magic", "ai"]},
{"name": "auto_fix_high", "tags": ["wand", "magic", "auto"]},
{"name": "flare", "tags": ["light", "sparkle", "shine"]},
{"name": "flash_on", "tags": ["lightning", "power", "camera"]},
{"name": "flash_off", "tags": ["lightning", "disabled", "camera"]},
{"name": "flash_auto", "tags": ["lightning", "auto", "camera"]},
{"name": "highlight", "tags": ["marker", "pen", "text"]},
{"name": "colorize", "tags": ["eyedropper", "pick", "color"]},
{"name": "opacity", "tags": ["transparent", "drop", "alpha"]},
{"name": "gradient", "tags": ["color", "blend", "fade"]},
{"name": "texture", "tags": ["pattern", "surface", "material"]},
{"name": "vignette", "tags": ["photo", "effect", "fade"]},
{"name": "blur_on", "tags": ["effect", "focus", "soft"]},
{"name": "blur_off", "tags": ["sharp", "clear", "focus"]},
{"name": "hdr_on", "tags": ["photo", "range", "dynamic"]},
{"name": "filter_vintage", "tags": ["flower", "retro", "effect"]},
{"name": "filter_drama", "tags": ["cloud", "weather", "sky"]},
{"name": "filter_frames", "tags": ["border", "photo", "frame"]},
{"name": "monochrome_photos", "tags": ["bw", "grayscale", "filter"]},
{"name": "photo_filter", "tags": ["star", "effect", "image"]},
{"name": "looks", "tags": ["sparkle", "enhance", "effect"]},
{"name": "straighten", "tags": ["rotate", "align", "level"]},
{"name": "transform", "tags": ["resize", "scale", "edit"]},
{"name": "style", "tags": ["format", "design", "appearance"]},
{"name": "format_paint", "tags": ["brush", "style", "copy"]}
]
}

View file

@ -1,418 +0,0 @@
{
"slug": "phosphor",
"name": "Phosphor",
"version": "2.1.1",
"icons": [
{"name": "activity", "tags": ["pulse", "health", "heart"], "category": "health", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "address-book", "tags": ["contacts", "directory", "people"], "category": "office", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "airplane", "tags": ["flight", "travel", "plane"], "category": "travel", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "airplane-in-flight", "tags": ["flight", "travel", "plane"], "category": "travel", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "airplane-landing", "tags": ["flight", "arrival", "plane"], "category": "travel", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "airplane-takeoff", "tags": ["flight", "departure", "plane"], "category": "travel", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "alarm", "tags": ["clock", "time", "alert"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "alien", "tags": ["space", "ufo", "extraterrestrial"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "align-bottom", "tags": ["layout", "vertical", "align"], "category": "design", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "align-center-horizontal", "tags": ["layout", "horizontal", "align"], "category": "design", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "align-center-vertical", "tags": ["layout", "vertical", "align"], "category": "design", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "align-left", "tags": ["text", "format", "paragraph"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "align-right", "tags": ["text", "format", "paragraph"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "align-top", "tags": ["layout", "vertical", "align"], "category": "design", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "anchor", "tags": ["marine", "ship", "link"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "aperture", "tags": ["camera", "lens", "photo"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "archive", "tags": ["box", "storage", "files"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "archive-box", "tags": ["storage", "container", "files"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "armchair", "tags": ["furniture", "seat", "chair"], "category": "furniture", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-arc-left", "tags": ["direction", "curved", "back"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-arc-right", "tags": ["direction", "curved", "forward"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-double-up-left", "tags": ["direction", "turn", "back"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-double-up-right", "tags": ["direction", "turn", "forward"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-down-left", "tags": ["direction", "turn", "corner"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-down-right", "tags": ["direction", "turn", "corner"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-left-down", "tags": ["direction", "turn", "corner"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-left-up", "tags": ["direction", "turn", "corner"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-right-down", "tags": ["direction", "turn", "corner"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-right-up", "tags": ["direction", "turn", "corner"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-up-left", "tags": ["direction", "turn", "corner"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-bend-up-right", "tags": ["direction", "turn", "corner"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-circle-down", "tags": ["direction", "download", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-circle-down-left", "tags": ["direction", "diagonal", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-circle-down-right", "tags": ["direction", "diagonal", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-circle-left", "tags": ["direction", "back", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-circle-right", "tags": ["direction", "forward", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-circle-up", "tags": ["direction", "upload", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-circle-up-left", "tags": ["direction", "diagonal", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-circle-up-right", "tags": ["direction", "diagonal", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-clockwise", "tags": ["refresh", "reload", "sync"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-counter-clockwise", "tags": ["undo", "back", "history"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-down", "tags": ["direction", "down", "download"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-down-left", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-down-right", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-fat-down", "tags": ["direction", "down", "bold"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-fat-left", "tags": ["direction", "left", "bold"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-fat-right", "tags": ["direction", "right", "bold"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-fat-up", "tags": ["direction", "up", "bold"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-left", "tags": ["direction", "back", "previous"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-line-down", "tags": ["direction", "download", "end"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-line-left", "tags": ["direction", "start", "begin"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-line-right", "tags": ["direction", "end", "finish"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-line-up", "tags": ["direction", "upload", "top"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-right", "tags": ["direction", "forward", "next"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-square-down", "tags": ["direction", "download", "square"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-square-left", "tags": ["direction", "back", "square"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-square-right", "tags": ["direction", "forward", "square"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-square-up", "tags": ["direction", "upload", "square"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-u-down-left", "tags": ["direction", "turn", "uturn"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-u-down-right", "tags": ["direction", "turn", "uturn"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-u-left-down", "tags": ["direction", "turn", "uturn"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-u-left-up", "tags": ["direction", "turn", "uturn"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-u-right-down", "tags": ["direction", "turn", "uturn"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-u-right-up", "tags": ["direction", "turn", "uturn"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-u-up-left", "tags": ["direction", "turn", "uturn"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-u-up-right", "tags": ["direction", "turn", "uturn"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-up", "tags": ["direction", "up", "upload"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-up-left", "tags": ["direction", "diagonal"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrow-up-right", "tags": ["direction", "diagonal", "external"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-clockwise", "tags": ["refresh", "sync", "rotate"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-counter-clockwise", "tags": ["refresh", "sync", "rotate"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-down-up", "tags": ["sort", "swap", "vertical"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-horizontal", "tags": ["resize", "horizontal", "expand"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-in", "tags": ["collapse", "minimize", "shrink"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-in-cardinal", "tags": ["collapse", "center", "focus"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-in-line-horizontal", "tags": ["collapse", "horizontal"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-in-line-vertical", "tags": ["collapse", "vertical"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-in-simple", "tags": ["collapse", "minimize"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-left-right", "tags": ["swap", "horizontal", "exchange"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-merge", "tags": ["combine", "join", "merge"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-out", "tags": ["expand", "maximize", "fullscreen"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-out-cardinal", "tags": ["expand", "move", "directions"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-out-line-horizontal", "tags": ["expand", "horizontal"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-out-line-vertical", "tags": ["expand", "vertical"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-out-simple", "tags": ["expand", "maximize"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-split", "tags": ["fork", "divide", "branch"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "arrows-vertical", "tags": ["resize", "vertical", "expand"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "at", "tags": ["email", "mention", "at sign"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "atom", "tags": ["science", "physics", "nuclear"], "category": "science", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "baby", "tags": ["child", "infant", "kid"], "category": "people", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "backpack", "tags": ["bag", "school", "travel"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bag", "tags": ["shopping", "tote", "carry"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bag-simple", "tags": ["shopping", "tote", "simple"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "balloon", "tags": ["party", "celebration", "birthday"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bank", "tags": ["finance", "money", "building"], "category": "buildings", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "barbell", "tags": ["gym", "fitness", "weight"], "category": "fitness", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "barcode", "tags": ["scan", "product", "code"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "basket", "tags": ["shopping", "cart", "store"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "basketball", "tags": ["sport", "ball", "game"], "category": "sports", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bathtub", "tags": ["bathroom", "bath", "clean"], "category": "home", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-charging", "tags": ["power", "energy", "charge"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-charging-vertical", "tags": ["power", "energy", "charge"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-empty", "tags": ["power", "energy", "empty"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-full", "tags": ["power", "energy", "full"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-high", "tags": ["power", "energy", "high"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-low", "tags": ["power", "energy", "low"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-medium", "tags": ["power", "energy", "half"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-plus", "tags": ["power", "add", "charge"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-vertical-empty", "tags": ["power", "energy", "empty"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-vertical-full", "tags": ["power", "energy", "full"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-vertical-high", "tags": ["power", "energy", "high"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-vertical-low", "tags": ["power", "energy", "low"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-vertical-medium", "tags": ["power", "energy", "half"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-warning", "tags": ["power", "alert", "low"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "battery-warning-vertical", "tags": ["power", "alert", "low"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bell", "tags": ["notification", "alert", "alarm"], "category": "notifications", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bell-ringing", "tags": ["notification", "alert", "alarm"], "category": "notifications", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bell-simple", "tags": ["notification", "alert", "simple"], "category": "notifications", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bell-simple-ringing", "tags": ["notification", "alert", "alarm"], "category": "notifications", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bell-simple-slash", "tags": ["notification", "mute", "off"], "category": "notifications", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bell-simple-z", "tags": ["notification", "snooze", "sleep"], "category": "notifications", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bell-slash", "tags": ["notification", "mute", "off"], "category": "notifications", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bell-z", "tags": ["notification", "snooze", "sleep"], "category": "notifications", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bluetooth", "tags": ["wireless", "connection", "device"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bluetooth-connected", "tags": ["wireless", "paired", "device"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bluetooth-slash", "tags": ["wireless", "disabled", "off"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bluetooth-x", "tags": ["wireless", "disconnect", "error"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "book", "tags": ["read", "library", "education"], "category": "education", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "book-bookmark", "tags": ["saved", "favorite", "reading"], "category": "education", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "book-open", "tags": ["read", "library", "education"], "category": "education", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "book-open-text", "tags": ["read", "content", "article"], "category": "education", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bookmark", "tags": ["save", "favorite", "tag"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bookmark-simple", "tags": ["save", "favorite", "simple"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bookmarks", "tags": ["saved", "favorites", "collection"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bookmarks-simple", "tags": ["saved", "favorites", "simple"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bounding-box", "tags": ["selection", "area", "crop"], "category": "design", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "briefcase", "tags": ["work", "job", "business"], "category": "business", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "briefcase-metal", "tags": ["work", "job", "metal"], "category": "business", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "browser", "tags": ["web", "internet", "window"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "browsers", "tags": ["web", "windows", "tabs"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bug", "tags": ["insect", "error", "debug"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bug-beetle", "tags": ["insect", "beetle", "bug"], "category": "nature", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "bug-droid", "tags": ["android", "robot", "bug"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "buildings", "tags": ["city", "office", "skyline"], "category": "buildings", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "calculator", "tags": ["math", "numbers", "compute"], "category": "tools", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "calendar", "tags": ["date", "schedule", "event"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "calendar-blank", "tags": ["date", "schedule", "empty"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "calendar-check", "tags": ["date", "done", "confirmed"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "calendar-plus", "tags": ["date", "add", "new"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "calendar-x", "tags": ["date", "cancel", "remove"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "camera", "tags": ["photo", "picture", "image"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "camera-slash", "tags": ["photo", "disabled", "off"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "car", "tags": ["vehicle", "auto", "drive"], "category": "transportation", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "car-simple", "tags": ["vehicle", "auto", "simple"], "category": "transportation", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "caret-circle-down", "tags": ["arrow", "down", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "caret-circle-left", "tags": ["arrow", "left", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "caret-circle-right", "tags": ["arrow", "right", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "caret-circle-up", "tags": ["arrow", "up", "circle"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "caret-down", "tags": ["arrow", "down", "expand"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "caret-left", "tags": ["arrow", "left", "back"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "caret-right", "tags": ["arrow", "right", "forward"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "caret-up", "tags": ["arrow", "up", "collapse"], "category": "arrows", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chart-bar", "tags": ["analytics", "graph", "data"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chart-bar-horizontal", "tags": ["analytics", "graph", "horizontal"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chart-line", "tags": ["analytics", "graph", "trend"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chart-line-down", "tags": ["analytics", "decrease", "trend"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chart-line-up", "tags": ["analytics", "increase", "trend"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chart-pie", "tags": ["analytics", "graph", "pie"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chart-pie-slice", "tags": ["analytics", "graph", "segment"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat", "tags": ["message", "conversation", "talk"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat-centered", "tags": ["message", "conversation", "center"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat-centered-dots", "tags": ["message", "typing", "dots"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat-centered-text", "tags": ["message", "text", "conversation"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat-circle", "tags": ["message", "conversation", "circle"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat-circle-dots", "tags": ["message", "typing", "circle"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat-circle-text", "tags": ["message", "text", "circle"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat-dots", "tags": ["message", "typing", "dots"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chat-text", "tags": ["message", "text", "conversation"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chats", "tags": ["messages", "conversations", "multiple"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "chats-circle", "tags": ["messages", "conversations", "circles"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "check", "tags": ["done", "complete", "success"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "check-circle", "tags": ["done", "complete", "circle"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "check-square", "tags": ["done", "checkbox", "complete"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "circle", "tags": ["shape", "dot", "round"], "category": "shapes", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "circle-dashed", "tags": ["shape", "dashed", "border"], "category": "shapes", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "circle-half", "tags": ["shape", "half", "contrast"], "category": "shapes", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "circle-half-tilt", "tags": ["shape", "half", "tilt"], "category": "shapes", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "circle-notch", "tags": ["loading", "spinner", "progress"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "clipboard", "tags": ["copy", "paste", "board"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "clipboard-text", "tags": ["copy", "paste", "text"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "clock", "tags": ["time", "watch", "schedule"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "clock-afternoon", "tags": ["time", "pm", "afternoon"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "clock-clockwise", "tags": ["time", "rotate", "forward"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "clock-counter-clockwise", "tags": ["time", "rotate", "back"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud", "tags": ["weather", "storage", "sky"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-arrow-down", "tags": ["download", "cloud", "save"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-arrow-up", "tags": ["upload", "cloud", "sync"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-check", "tags": ["cloud", "done", "synced"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-fog", "tags": ["weather", "fog", "mist"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-lightning", "tags": ["weather", "storm", "thunder"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-moon", "tags": ["weather", "night", "cloudy"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-rain", "tags": ["weather", "rain", "precipitation"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-slash", "tags": ["cloud", "offline", "disconnect"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-snow", "tags": ["weather", "snow", "winter"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-sun", "tags": ["weather", "day", "partly cloudy"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-warning", "tags": ["cloud", "alert", "error"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cloud-x", "tags": ["cloud", "error", "disconnect"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "code", "tags": ["programming", "html", "brackets"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "code-block", "tags": ["programming", "snippet", "code"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "code-simple", "tags": ["programming", "brackets", "simple"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "coffee", "tags": ["drink", "cup", "cafe"], "category": "food", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "command", "tags": ["keyboard", "mac", "shortcut"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "compass", "tags": ["navigation", "direction", "explore"], "category": "navigation", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "copy", "tags": ["duplicate", "clipboard", "clone"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "copy-simple", "tags": ["duplicate", "clone", "simple"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "credit-card", "tags": ["payment", "card", "money"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "crop", "tags": ["image", "edit", "resize"], "category": "editing", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cube", "tags": ["3d", "box", "shape"], "category": "shapes", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "currency-btc", "tags": ["bitcoin", "crypto", "currency"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "currency-dollar", "tags": ["money", "usd", "currency"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "currency-eur", "tags": ["money", "euro", "currency"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "currency-gbp", "tags": ["money", "pound", "currency"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "currency-jpy", "tags": ["money", "yen", "currency"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cursor", "tags": ["pointer", "mouse", "click"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "cursor-click", "tags": ["pointer", "click", "select"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "database", "tags": ["storage", "data", "server"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "desktop", "tags": ["computer", "monitor", "screen"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "desktop-tower", "tags": ["computer", "pc", "tower"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "device-mobile", "tags": ["phone", "mobile", "smartphone"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "device-tablet", "tags": ["tablet", "ipad", "device"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "download", "tags": ["save", "export", "arrow"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "download-simple", "tags": ["save", "export", "simple"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "envelope", "tags": ["email", "mail", "message"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "envelope-open", "tags": ["email", "mail", "read"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "envelope-simple", "tags": ["email", "mail", "simple"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "eye", "tags": ["view", "visible", "show"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "eye-closed", "tags": ["hide", "invisible", "hidden"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "eye-slash", "tags": ["hide", "invisible", "hidden"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "file", "tags": ["document", "page", "paper"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "file-arrow-down", "tags": ["download", "document", "save"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "file-arrow-up", "tags": ["upload", "document", "export"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "file-code", "tags": ["code", "programming", "document"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "file-plus", "tags": ["add", "new", "document"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "file-text", "tags": ["document", "text", "content"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "filter", "tags": ["funnel", "sort", "search"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "flag", "tags": ["report", "mark", "country"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "folder", "tags": ["directory", "files", "organize"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "folder-open", "tags": ["directory", "browse", "explore"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "folder-plus", "tags": ["directory", "add", "new"], "category": "files", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "funnel", "tags": ["filter", "sort", "funnel"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "gear", "tags": ["settings", "cog", "config"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "gear-six", "tags": ["settings", "cog", "config"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "gift", "tags": ["present", "reward", "surprise"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "globe", "tags": ["world", "earth", "web"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "globe-hemisphere-east", "tags": ["world", "earth", "asia"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "globe-hemisphere-west", "tags": ["world", "earth", "americas"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "grid-four", "tags": ["layout", "grid", "squares"], "category": "layout", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "hand", "tags": ["palm", "stop", "gesture"], "category": "gestures", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "hand-pointing", "tags": ["finger", "point", "gesture"], "category": "gestures", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "hash", "tags": ["tag", "hashtag", "number"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "heart", "tags": ["love", "favorite", "like"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "heart-break", "tags": ["love", "broken", "sad"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "house", "tags": ["home", "building", "main"], "category": "buildings", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "house-simple", "tags": ["home", "building", "simple"], "category": "buildings", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "image", "tags": ["picture", "photo", "gallery"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "image-square", "tags": ["picture", "photo", "square"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "info", "tags": ["information", "help", "about"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "key", "tags": ["password", "security", "lock"], "category": "security", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "keyboard", "tags": ["typing", "input", "keys"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "laptop", "tags": ["computer", "notebook", "device"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "link", "tags": ["url", "chain", "connect"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "link-break", "tags": ["unlink", "disconnect", "broken"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "list", "tags": ["menu", "items", "bullet"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "list-bullets", "tags": ["menu", "items", "bullet"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "list-checks", "tags": ["tasks", "checklist", "todo"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "list-numbers", "tags": ["menu", "items", "ordered"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "lock", "tags": ["security", "private", "password"], "category": "security", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "lock-key", "tags": ["security", "password", "key"], "category": "security", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "lock-key-open", "tags": ["security", "unlocked", "open"], "category": "security", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "lock-open", "tags": ["security", "unlocked", "open"], "category": "security", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "magnifying-glass", "tags": ["search", "find", "zoom"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "magnifying-glass-minus", "tags": ["zoom out", "search", "smaller"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "magnifying-glass-plus", "tags": ["zoom in", "search", "bigger"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "map-pin", "tags": ["location", "marker", "place"], "category": "maps", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "map-trifold", "tags": ["navigation", "directions", "map"], "category": "maps", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "megaphone", "tags": ["announce", "broadcast", "marketing"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "microphone", "tags": ["audio", "voice", "record"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "microphone-slash", "tags": ["audio", "mute", "off"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "minus", "tags": ["subtract", "remove", "delete"], "category": "math", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "minus-circle", "tags": ["subtract", "remove", "circle"], "category": "math", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "moon", "tags": ["dark", "night", "theme"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "moon-stars", "tags": ["night", "dark", "stars"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "music-note", "tags": ["audio", "sound", "melody"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "music-notes", "tags": ["audio", "sound", "melody"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "navigation-arrow", "tags": ["direction", "compass", "arrow"], "category": "navigation", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "paper-plane", "tags": ["send", "message", "mail"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "paper-plane-right", "tags": ["send", "submit", "forward"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "paper-plane-tilt", "tags": ["send", "message", "tilt"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "paperclip", "tags": ["attachment", "file", "clip"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "paperclip-horizontal", "tags": ["attachment", "file", "horizontal"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "pause", "tags": ["media", "stop", "wait"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "pause-circle", "tags": ["media", "stop", "circle"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "pencil", "tags": ["edit", "write", "draw"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "pencil-line", "tags": ["edit", "write", "underline"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "pencil-simple", "tags": ["edit", "write", "simple"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "phone", "tags": ["call", "contact", "mobile"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "phone-call", "tags": ["call", "ringing", "incoming"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "play", "tags": ["media", "start", "video"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "play-circle", "tags": ["media", "start", "circle"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "plus", "tags": ["add", "create", "new"], "category": "math", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "plus-circle", "tags": ["add", "create", "circle"], "category": "math", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "power", "tags": ["on", "off", "shutdown"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "printer", "tags": ["print", "document", "paper"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "question", "tags": ["help", "support", "faq"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "rocket", "tags": ["launch", "startup", "space"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "rocket-launch", "tags": ["launch", "startup", "deploy"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "rss", "tags": ["feed", "subscribe", "blog"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "rss-simple", "tags": ["feed", "subscribe", "simple"], "category": "communication", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "scissors", "tags": ["cut", "trim", "edit"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "share", "tags": ["social", "send", "forward"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "share-network", "tags": ["social", "connect", "network"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "shield", "tags": ["security", "protection", "safe"], "category": "security", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "shield-check", "tags": ["security", "verified", "safe"], "category": "security", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "shopping-bag", "tags": ["shop", "purchase", "buy"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "shopping-cart", "tags": ["shop", "cart", "ecommerce"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "sign-in", "tags": ["login", "enter", "access"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "sign-out", "tags": ["logout", "exit", "leave"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "sliders", "tags": ["settings", "controls", "adjust"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "sliders-horizontal", "tags": ["settings", "controls", "horizontal"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "smiley", "tags": ["emoji", "happy", "face"], "category": "emoji", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "smiley-sad", "tags": ["emoji", "sad", "face"], "category": "emoji", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "sparkle", "tags": ["magic", "new", "shine"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-high", "tags": ["audio", "volume", "loud"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-low", "tags": ["audio", "volume", "quiet"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-none", "tags": ["audio", "mute", "silent"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-simple-high", "tags": ["audio", "volume", "loud"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-simple-low", "tags": ["audio", "volume", "quiet"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-simple-none", "tags": ["audio", "mute", "silent"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-simple-slash", "tags": ["audio", "mute", "off"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-simple-x", "tags": ["audio", "mute", "off"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-slash", "tags": ["audio", "mute", "off"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "speaker-x", "tags": ["audio", "mute", "off"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "spinner", "tags": ["loading", "wait", "progress"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "spinner-gap", "tags": ["loading", "wait", "progress"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "star", "tags": ["favorite", "rating", "bookmark"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "star-four", "tags": ["sparkle", "magic", "star"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "star-half", "tags": ["rating", "half", "partial"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "stop", "tags": ["media", "stop", "end"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "stop-circle", "tags": ["media", "stop", "circle"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "sun", "tags": ["light", "day", "brightness"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "sun-dim", "tags": ["light", "dim", "brightness"], "category": "weather", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "tag", "tags": ["label", "category", "price"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "target", "tags": ["goal", "aim", "focus"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "terminal", "tags": ["console", "command", "code"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "terminal-window", "tags": ["console", "command", "window"], "category": "development", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-aa", "tags": ["font", "typography", "text"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-align-center", "tags": ["text", "format", "center"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-align-justify", "tags": ["text", "format", "justify"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-align-left", "tags": ["text", "format", "left"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-align-right", "tags": ["text", "format", "right"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-b", "tags": ["bold", "format", "strong"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-italic", "tags": ["italic", "format", "emphasis"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-strikethrough", "tags": ["strikethrough", "format", "delete"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "text-underline", "tags": ["underline", "format", "text"], "category": "text", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "thumbs-down", "tags": ["dislike", "bad", "negative"], "category": "gestures", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "thumbs-up", "tags": ["like", "good", "positive"], "category": "gestures", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "ticket", "tags": ["event", "pass", "coupon"], "category": "objects", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "timer", "tags": ["time", "countdown", "clock"], "category": "time", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "toggle-left", "tags": ["switch", "off", "disable"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "toggle-right", "tags": ["switch", "on", "enable"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "trash", "tags": ["delete", "remove", "bin"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "trash-simple", "tags": ["delete", "remove", "simple"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "trend-down", "tags": ["chart", "decrease", "analytics"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "trend-up", "tags": ["chart", "increase", "analytics"], "category": "charts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "trophy", "tags": ["award", "winner", "achievement"], "category": "awards", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "truck", "tags": ["delivery", "shipping", "transport"], "category": "transportation", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "tv", "tags": ["television", "screen", "display"], "category": "devices", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "upload", "tags": ["export", "send", "share"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "upload-simple", "tags": ["export", "send", "simple"], "category": "actions", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user", "tags": ["person", "account", "profile"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-circle", "tags": ["avatar", "profile", "account"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-circle-gear", "tags": ["settings", "account", "config"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-circle-minus", "tags": ["remove", "unfriend", "user"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-circle-plus", "tags": ["add", "invite", "user"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-focus", "tags": ["target", "focus", "user"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-gear", "tags": ["settings", "account", "config"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-list", "tags": ["contacts", "directory", "users"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-minus", "tags": ["remove", "unfriend", "delete"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-plus", "tags": ["add", "invite", "new"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-rectangle", "tags": ["id", "card", "profile"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "user-square", "tags": ["avatar", "profile", "square"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "users", "tags": ["people", "team", "group"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "users-four", "tags": ["people", "team", "group"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "users-three", "tags": ["people", "team", "group"], "category": "users", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "video-camera", "tags": ["video", "film", "record"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "video-camera-slash", "tags": ["video", "off", "mute"], "category": "media", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "wallet", "tags": ["money", "payment", "finance"], "category": "commerce", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "warning", "tags": ["alert", "error", "caution"], "category": "alerts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "warning-circle", "tags": ["alert", "error", "circle"], "category": "alerts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "warning-diamond", "tags": ["alert", "error", "diamond"], "category": "alerts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "warning-octagon", "tags": ["alert", "stop", "error"], "category": "alerts", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "wifi-high", "tags": ["wireless", "internet", "strong"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "wifi-low", "tags": ["wireless", "internet", "weak"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "wifi-medium", "tags": ["wireless", "internet", "medium"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "wifi-none", "tags": ["wireless", "offline", "disconnect"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "wifi-slash", "tags": ["wireless", "offline", "disconnect"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "wifi-x", "tags": ["wireless", "error", "disconnect"], "category": "connectivity", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "wrench", "tags": ["tool", "settings", "repair"], "category": "tools", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "x", "tags": ["close", "cancel", "remove"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "x-circle", "tags": ["close", "cancel", "error"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]},
{"name": "x-square", "tags": ["close", "cancel", "remove"], "category": "ui", "styles": ["regular", "bold", "light", "thin", "fill", "duotone"]}
]
}

View file

@ -1,124 +0,0 @@
=== Maple Icons ===
Contributors: jetrails
Tags: icons, svg, gutenberg, block, heroicons, lucide, feather, phosphor, material
Requires at least: 6.5
Tested up to: 6.7
Stable tag: 1.0.0
Requires PHP: 7.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Insert beautiful open-source icons into your content with a Gutenberg block. Download icon sets from CDN and serve locally.
== Description ==
Maple Icons provides a simple way to insert high-quality, open-source icons into your WordPress content using the Gutenberg block editor.
= Features =
* **Multiple Icon Sets** - Choose from popular open-source icon libraries:
* Heroicons (~290 icons) - MIT License
* Lucide (~1400 icons) - ISC License
* Feather (~287 icons) - MIT License
* Phosphor (~1200 icons per style) - MIT License
* Material Symbols (~400 icons) - Apache 2.0 License
* **Local Storage** - Icons are downloaded from CDN and stored locally in your WordPress installation. No external requests are made when displaying icons on your site.
* **Gutenberg Block** - Easy-to-use block with:
* Search and filter icons
* Multiple style variants per set
* Size control (12px - 256px)
* Custom icon color
* Background color
* Padding and margin controls
* Stroke width adjustment
* Drop shadow effect
* **Performance Optimized**
* Icons are inline SVG - no additional HTTP requests
* No frontend JavaScript or CSS
* Works in RSS feeds and email
* **Accessible**
* Decorative icons automatically hidden from screen readers
* Optional accessible labels for meaningful icons
* Follows WCAG guidelines
= How It Works =
1. Go to Settings → Maple Icons
2. Download one or more icon sets
3. Set one icon set as active
4. In the Gutenberg editor, add a "Maple Icon" block
5. Search and select an icon
6. Customize size, color, and other settings
= Icon Storage =
Downloaded icons are stored in `wp-content/maple-icons/`. Each icon set is stored in its own subdirectory with the SVG files organized by style.
= Compatibility =
* WordPress 6.5+
* PHP 7.4+
* WooCommerce (HPOS compatible)
* Works with all properly coded themes
* Compatible with popular page builders that support Gutenberg blocks
== Installation ==
1. Upload the `maple-icons` folder to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Go to Settings → Maple Icons to download icon sets
4. Start using the "Maple Icon" block in your content
== Frequently Asked Questions ==
= Are the icons free to use? =
Yes! All icon sets included are open-source with permissive licenses (MIT, ISC, or Apache 2.0) that allow commercial use.
= Can I use multiple icon sets? =
You can download multiple icon sets, but only one can be active at a time. The active set is what appears in the Gutenberg block icon picker.
= Where are the icons stored? =
Icons are downloaded from CDN (jsdelivr.net) and stored locally in `wp-content/maple-icons/`. Once downloaded, no external requests are made to display icons.
= How do I change the icon color? =
Icons automatically inherit the text color from your theme. You can also set a custom color in the block settings panel.
= Will icons work in RSS feeds? =
Yes! Icons are saved as inline SVG in your post content, so they work in RSS feeds, email newsletters, and anywhere else your content is displayed.
= What happens if I uninstall the plugin? =
Existing icons in your content will remain as inline SVG. However, you won't be able to add new icons or use the block settings. The downloaded icon files in `wp-content/maple-icons/` will be removed on uninstall.
= Is this plugin GDPR compliant? =
Yes. Maple Icons does not collect any user data, set cookies, or make external requests after icons are downloaded. All icon files are stored locally on your server.
== Screenshots ==
1. Settings page - Download and manage icon sets
2. Icon picker modal in Gutenberg editor
3. Block settings panel with customization options
4. Icon block rendered on the frontend
== Changelog ==
= 1.0.0 =
* Initial release
* Support for Heroicons, Lucide, Feather, Phosphor, and Material Symbols
* Gutenberg block with full customization options
* Local icon storage
== Upgrade Notice ==
= 1.0.0 =
Initial release of Maple Icons.

View file

@ -1,5 +0,0 @@
<?php
// Silence is golden.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

View file

@ -1,62 +0,0 @@
<?php
/**
* Maple Icons Uninstall
*
* Runs when the plugin is uninstalled (deleted).
* Cleans up all plugin data including downloaded icons and settings.
*
* @package MapleIcons
*/
// Exit if not called by WordPress.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
/**
* Remove plugin options.
*/
delete_option( 'maple_icons_settings' );
/**
* Remove transients.
*/
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mi_%' OR option_name LIKE '_transient_timeout_mi_%'"
);
/**
* Remove downloaded icons directory.
*/
$icons_dir = WP_CONTENT_DIR . '/maple-icons/';
if ( is_dir( $icons_dir ) ) {
/**
* Recursively delete a directory and its contents.
*
* @param string $dir Directory path.
* @return bool True on success, false on failure.
*/
function mi_delete_directory( $dir ) {
if ( ! is_dir( $dir ) ) {
return false;
}
$files = array_diff( scandir( $dir ), array( '.', '..' ) );
foreach ( $files as $file ) {
$path = $dir . '/' . $file;
if ( is_dir( $path ) ) {
mi_delete_directory( $path );
} else {
unlink( $path );
}
}
return rmdir( $dir );
}
mi_delete_directory( $icons_dir );
}

Some files were not shown because too many files have changed in this diff Show more